欧美free性护士vide0shd,老熟女,一区二区三区,久久久久夜夜夜精品国产,久久久久久综合网天天,欧美成人护士h版

首頁綜合 正文
目錄

柚子快報(bào)激活碼778899分享:快速學(xué)習(xí)GO語言總結(jié)

柚子快報(bào)激活碼778899分享:快速學(xué)習(xí)GO語言總結(jié)

http://yzkb.51969.com/

干貨分享,感謝您的閱讀!備注:本博客將自己初步學(xué)習(xí)GO的總結(jié)進(jìn)行分享,希望大家通過本博客可以在短時(shí)間內(nèi)快速掌握GO的基本程序編碼能力,如有錯誤請留言指正,謝謝!

一、初步了解Go語言

(一)Go語言誕生的主要問題和目標(biāo)

多核硬件架構(gòu): 隨著計(jì)算機(jī)硬件的發(fā)展,多核處理器成為主流,使得并行計(jì)算變得普遍。然而,傳統(tǒng)的編程語言在處理多核并行性時(shí)可能面臨困難,因?yàn)樗鼈內(nèi)狈线m的原生支持。Go語言通過引入輕量級的協(xié)程(goroutine)和通道(channel)機(jī)制,使得并發(fā)編程變得更加容易。開發(fā)者可以輕松地創(chuàng)建數(shù)千個并發(fā)執(zhí)行的協(xié)程,而無需擔(dān)心線程管理的復(fù)雜性。 超大規(guī)模分布式計(jì)算集群: 隨著云計(jì)算和分布式系統(tǒng)的崛起,構(gòu)建和維護(hù)超大規(guī)模的分布式計(jì)算集群變得越來越常見。這些集群需要能夠高效處理大量的請求、數(shù)據(jù)共享和協(xié)調(diào)。Go語言的并發(fā)特性和通道機(jī)制使得編寫分布式系統(tǒng)變得更加容易,開發(fā)者可以使用協(xié)程和通道來處理并發(fā)任務(wù)、消息傳遞和協(xié)調(diào)工作。 Web模式導(dǎo)致的開發(fā)規(guī)模和更新速度增加: Web應(yīng)用的興起帶來了前所未有的開發(fā)規(guī)模和持續(xù)更新的需求。傳統(tǒng)的編程語言在開發(fā)大型Web應(yīng)用時(shí)可能會面臨可維護(hù)性、性能和開發(fā)效率等問題。Go語言通過其簡潔的語法、高效的編譯速度以及并發(fā)支持,使得開發(fā)者能夠更快速地迭代和部署Web應(yīng)用,同時(shí)也能夠更好地處理高并發(fā)的網(wǎng)絡(luò)請求。

綜合來看,Go語言在誕生時(shí)確實(shí)著重解決了多核硬件架構(gòu)、超大規(guī)模分布式計(jì)算集群和Web模式下的開發(fā)規(guī)模與速度等技術(shù)挑戰(zhàn),它的設(shè)計(jì)目標(biāo)之一是提供一種適應(yīng)現(xiàn)代軟件開發(fā)需求的編程語言,使開發(fā)者能夠更好地應(yīng)對這些挑戰(zhàn)。

(二)Go語言應(yīng)用典型代表

Go語言在當(dāng)下應(yīng)用開發(fā)中已經(jīng)得到廣泛應(yīng)用,許多知名公司和項(xiàng)目都使用Go語言來構(gòu)建各種類型的應(yīng)用。以下是一些代表性的產(chǎn)品和項(xiàng)目,它們使用了Go語言作為核心開發(fā)語言:

這些僅僅是Go語言應(yīng)用的一小部分示例,實(shí)際上還有許多其他的項(xiàng)目和產(chǎn)品也在使用Go語言來構(gòu)建高性能、可靠且易于維護(hù)的應(yīng)用程序。這表明Go語言在現(xiàn)代應(yīng)用開發(fā)中發(fā)揮了重要作用,特別是在分布式系統(tǒng)、云計(jì)算和高性能應(yīng)用領(lǐng)域。

(三)Java、C++、C程序員在學(xué)習(xí)編寫Go時(shí)存在的誤區(qū)

當(dāng)Java、C++、C等編程語言的程序員開始學(xué)習(xí)編寫Go語言時(shí),可能會遇到一些誤區(qū),因?yàn)镚o在某些方面與這些傳統(tǒng)語言有所不同。以下是一些常見的誤區(qū):

過度使用傳統(tǒng)的并發(fā)模型: 傳統(tǒng)的編程語言如Java、C++、C在處理并發(fā)時(shí)通常使用線程和鎖來實(shí)現(xiàn),但在Go中,使用協(xié)程(goroutine)和通道(channel)是更好的方式。新學(xué)習(xí)Go的程序員可能會繼續(xù)使用傳統(tǒng)的并發(fā)模型,而不充分利用Go的輕量級協(xié)程和通道,從而失去了Go的并發(fā)優(yōu)勢。 過度使用指針: C和C++等語言強(qiáng)調(diào)指針的使用,但Go語言在設(shè)計(jì)時(shí)避免了過多的指針操作。新學(xué)習(xí)Go的程序員可能會過度使用指針,導(dǎo)致代碼變得復(fù)雜。在Go中,盡量避免使用指針,除非真正需要對值進(jìn)行修改。 忽視錯誤處理: Go鼓勵顯式地處理錯誤,而不是簡單地忽略它們。這與一些其他語言的習(xí)慣不同,其中錯誤往往被忽略或簡單地拋出。新學(xué)習(xí)Go的程序員可能會忽視錯誤處理,導(dǎo)致潛在的問題未被檢測到。 過度使用全局變量: 在C和C++等語言中,全局變量可能是常見的做法。然而,在Go中,全局變量的使用被視為不良實(shí)踐。Go鼓勵使用局部變量和傳遞參數(shù)的方式來傳遞數(shù)據(jù),以避免引入不必要的耦合和副作用。 不熟悉切片和映射: Go中的切片和映射是強(qiáng)大的數(shù)據(jù)結(jié)構(gòu),但對于其他語言的程序員來說可能不太熟悉。學(xué)習(xí)如何正確使用切片和映射是很重要的,因?yàn)樗鼈冊贕o中廣泛用于集合和數(shù)據(jù)處理。 錯誤的Go風(fēng)格: 每種語言都有其獨(dú)特的編碼風(fēng)格和慣例。新學(xué)習(xí)Go的程序員可能會在Go代碼中應(yīng)用其他語言的編碼風(fēng)格,這可能會使代碼難以閱讀和理解。

為了避免這些誤區(qū),學(xué)習(xí)Go的程序員應(yīng)該投入時(shí)間去理解Go語言的核心概念,包括并發(fā)模型、錯誤處理、數(shù)據(jù)結(jié)構(gòu)等,同時(shí)積極參與Go社區(qū),閱讀Go的官方文檔和示例代碼,以便更好地適應(yīng)Go的設(shè)計(jì)理念和最佳實(shí)踐。

二、環(huán)境準(zhǔn)備(以Mac說明)

(一)環(huán)境設(shè)置

在macOS上設(shè)置Go語言開發(fā)環(huán)境非常簡單,可以按照以下步驟進(jìn)行操作:

使用Homebrew安裝: 如果您使用Homebrew包管理器,這是最方便的方法。打開終端,并運(yùn)行以下命令來安裝Go語言: brew install go 手動安裝: 如果想手動安裝Go語言,可以按照以下步驟操作: a. 訪問官方網(wǎng)站下載安裝包`goX.X.X.darwin-amd64.pkg b. 雙擊下載的安裝包,按照指示運(yùn)行安裝程序。按照默認(rèn)設(shè)置即可,安裝路徑通常是/usr/local/go。 設(shè)置環(huán)境變量: 一旦安裝完成,需要將Go語言的二進(jìn)制路徑添加到自己的終端配置文件中的PATH環(huán)境變量中。這樣就可以在終端中直接運(yùn)行Go命令。 a. 打開終端,并使用文本編輯器(如nano、vim或任何您喜歡的編輯器)編輯終端配置文件。例如: nano ~/.bash_profile b. 在文件中添加以下行(根據(jù)安裝路徑進(jìn)行調(diào)整),然后保存并退出編輯器: export PATH=$PATH:/usr/local/go/bin c. 使配置生效,可以運(yùn)行以下命令或者重啟終端: source ~/.bash_profile 驗(yàn)證安裝: 打開終端,輸入以下命令來驗(yàn)證Go是否已正確安裝: go version 如果看到了Go的版本號,表示安裝成功。

(二)IDE選擇說明

我個人使用的GoLand,直接官網(wǎng)下載后,上網(wǎng)購買破解版即可,這里不在多說!

三、Go語言程序?qū)W習(xí)

創(chuàng)建自己的工程目錄/Users/zyf/zyfcodes/go/go-learning,新建src目錄。

(一)第一個Go語言編寫

src目錄下創(chuàng)建chapter1/hello目錄,新建hello.go文件,編寫代碼如下:

package main

import (

"fmt"

"os"

)

/**

* @author zhangyanfeng

* @description 第一個godaima

* @date 2023/8/20 23:45

* @param

* @return

**/

func main() {

if len(os.Args) > 1 {

fmt.Println("Hello World", os.Args[1])

}

}

這段代碼是一個簡單的Go語言程序,它接受命令行參數(shù)并打印出一條帶參數(shù)的 "Hello World" 消息。下面是對代碼的逐行分析:

package main: 聲明這個文件屬于名為 "main" 的包,這是一個Go程序的入口包名。 import ("fmt" "os"): 引入了兩個標(biāo)準(zhǔn)庫包,分別是 "fmt" 用于格式化輸出,和 "os" 用于與操作系統(tǒng)交互。 func main() { ... }: 這是程序的入口函數(shù),它會在程序運(yùn)行時(shí)首先被調(diào)用。 if len(os.Args) > 1 { ... }: 這個條件語句檢查命令行參數(shù)的數(shù)量是否大于1,也就是判斷是否有參數(shù)傳遞給程序。os.Args 是一個字符串切片,它包含了所有的命令行參數(shù),第一個參數(shù)是程序的名稱。 fmt.Println("Hello World", os.Args[1]): 如果有參數(shù)傳遞給程序,就會執(zhí)行這行代碼。它使用 fmt.Println 函數(shù)打印一條消息,消息由字符串 "Hello World" 和 os.Args[1] 組成,os.Args[1] 表示傳遞給程序的第一個參數(shù)。

綜上所述,這段代碼涵蓋了以下知識點(diǎn):

包導(dǎo)入和使用標(biāo)準(zhǔn)庫:通過 import 關(guān)鍵字導(dǎo)入 "fmt" 和 "os" 包,然后在代碼中使用這些包提供的函數(shù)和類型。 命令行參數(shù)獲?。菏褂?os.Args 獲取命令行參數(shù)。 條件語句:使用 if 條件語句來判斷是否有命令行參數(shù)傳遞給程序。 字符串操作:使用字符串連接操作將 "Hello World" 與命令行參數(shù)拼接在一起。 格式化輸出:使用 fmt.Println 函數(shù)將消息輸出到標(biāo)準(zhǔn)輸出。

注意:如果沒有傳遞參數(shù)給程序,那么這段代碼不會打印任何消息。如果傳遞了多個參數(shù),代碼只會使用第一個參數(shù)并忽略其他參數(shù)。

在該目錄下執(zhí)行“go run hello.go ZYF”,運(yùn)行結(jié)果為“Hello World ZYF”。

(二)基本程序結(jié)構(gòu)編寫學(xué)習(xí)

src目錄下創(chuàng)建chapter2

1.變量

前提:chapter2目錄下創(chuàng)建variables,學(xué)習(xí)總結(jié)如下:

變量聲明: 使用var關(guān)鍵字聲明一個變量,例如:var x int。類型推斷: 可以使用:=操作符進(jìn)行變量聲明和賦值,Go會根據(jù)右側(cè)的值自動推斷變量類型,例如:y := 5。變量賦值: 使用賦值操作符=給變量賦值,例如:x = 10。多變量聲明: 可以同時(shí)聲明多個變量,例如:var a, b, c int。變量初始化: 變量可以在聲明時(shí)進(jìn)行初始化,例如:var name string = "John"。零值: 未初始化的變量會被賦予零值,數(shù)字類型為0,布爾類型為false,字符串類型為空字符串等。短變量聲明: 在函數(shù)內(nèi)部,可以使用短變量聲明方式,例如:count := 10。

新建fib_test.go,背景:簡單實(shí)用斐波那契數(shù)列進(jìn)行練習(xí)

package variables

import "testing"

func TestFibList(t *testing.T) {

a := 1

b := 1

t.Log(a)

for i := 0; i < 5; i++ {

t.Log(" ", b)

tmp := a

a = b

b = tmp + a

}

}

func TestExchange(t *testing.T) {

a := 1

b := 2

// tmp := a

// a = b

// b = tmp

a, b = b, a

t.Log(a, b)

}

下面逐個解釋代碼中涉及的知識點(diǎn):

package variables: 聲明了一個名為 "variables" 的包,這是一個用于測試的包名。 import "testing": 導(dǎo)入了Go語言的測試框架 "testing" 包,用于編寫和運(yùn)行測試函數(shù)。 func TestFibList(t *testing.T) { ... }: 定義了一個測試函數(shù) "TestFibList",該函數(shù)用于測試斐波那契數(shù)列生成邏輯。這是一個測試函數(shù)的標(biāo)準(zhǔn)命名,以 "Test" 開頭,接著是被測試的函數(shù)名。 在測試函數(shù)內(nèi)部,聲明了兩個整數(shù)變量 a 和 b,并將它們初始化為 1,這是斐波那契數(shù)列的前兩個數(shù)。使用 t.Log(a) 打印變量 a 的值到測試日志中。使用循環(huán)來生成斐波那契數(shù)列的前 5 個數(shù),每次迭代都會將 b 的值打印到測試日志,并更新 a 和 b 的值以生成下一個數(shù)。 func TestExchange(t *testing.T) { ... }: 定義了另一個測試函數(shù) "TestExchange",該函數(shù)用于測試變量交換的邏輯。 在測試函數(shù)內(nèi)部,聲明了兩個整數(shù)變量 a 和 b,并分別將它們初始化為 1 和 2。使用注釋的方式展示了一種變量交換的寫法(通過中間變量),但實(shí)際上被注釋掉了。然后使用 a, b = b, a 這一行代碼來實(shí)現(xiàn) a 和 b 的交換,這是Go語言中的一種特有的交換方式,不需要額外的中間變量。使用 t.Log(a, b) 打印交換后的變量值到測試日志中。

2.常量

前提:chapter2目錄下創(chuàng)建constant,學(xué)習(xí)總結(jié)如下:

常量聲明: 使用const關(guān)鍵字聲明一個常量,例如:const pi = 3.14159。常量賦值: 常量的值在聲明時(shí)必須被賦值,一旦賦值后不可修改。枚舉常量: 可以使用一組常量來模擬枚舉,例如: const (

Monday = 1

Tuesday = 2

// ...

) 類型指定: 常量的類型也可以被指定,例如:const speed int = 300000。常量表達(dá)式: 常量可使用表達(dá)式計(jì)算,例如:const secondsInHour = 60 * 60。無類型常量: 常量可以是無類型的,根據(jù)上下文自動推斷類型。例如,const x = 5會被推斷為整數(shù)類型。

新建constant_test.go,寫代碼如下:

package constant

import "testing"

const (

Monday = 1 + iota

Tuesday

Wednesday

)

const (

Readable = 1 << iota

Writable

Executable

)

func TestConstant1(t *testing.T) {

t.Log(Monday, Tuesday)

}

func TestConstant2(t *testing.T) {

a := 1 //0001

t.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable)

}

下面逐個解釋代碼中涉及的知識點(diǎn):

package constant: 聲明了一個名為 "constant" 的包,這是一個用于測試的包名。 import "testing": 導(dǎo)入了Go語言的測試框架 "testing" 包,用于編寫和運(yùn)行測試函數(shù)。 const (...): 定義了兩個常量塊。 第一個常量塊中,使用了 iota 常量生成器來定義了一系列從 1 開始遞增的常量。在這個例子中,Monday 被賦值為 1,Tuesday 被賦值為 2,Wednesday 被賦值為 3。iota 在常量塊中每次被使用時(shí)會遞增一次,因此后續(xù)的常量會依次遞增;第二個常量塊中,使用了 iota 來定義了一系列按位左移的常量。在這個例子中,Readable 被賦值為 1,Writable 被賦值為 2(二進(jìn)制中的 10),Executable 被賦值為 4(二進(jìn)制中的 100)。位運(yùn)算中,左移操作可以將二進(jìn)制數(shù)向左移動指定的位數(shù)。 func TestConstant1(t *testing.T) { ... }: 定義了一個測試函數(shù) "TestConstant1",用于測試第一個常量塊中定義的常量。 使用 t.Log(Monday, Tuesday) 打印常量 Monday 和 Tuesday 的值到測試日志中。 func TestConstant2(t *testing.T) { ... }: 定義了另一個測試函數(shù) "TestConstant2",用于測試位運(yùn)算和常量的使用。 在測試函數(shù)內(nèi)部,聲明了一個整數(shù)變量 a,并將其初始化為 1,即二進(jìn)制中的 0001。使用位運(yùn)算和按位與操作來檢查變量 a 是否具有 Readable、Writable 和 Executable 屬性。例如,a&Readable == Readable 表達(dá)式檢查 a 的二進(jìn)制表示是否含有 Readable 標(biāo)志位。使用 t.Log() 打印三個表達(dá)式的結(jié)果到測試日志中。

3.數(shù)據(jù)類型

前提:chapter2目錄下創(chuàng)建 type,學(xué)習(xí)總結(jié)如下:

主要數(shù)據(jù)類型說明

Go語言具有豐富的內(nèi)置數(shù)據(jù)類型,這些數(shù)據(jù)類型用于表示不同類型的值和數(shù)據(jù)。以下是對Go語言中一些主要數(shù)據(jù)類型的總結(jié)分析:

整數(shù)類型(Integer Types):Go語言提供不同大小的整數(shù)類型,如int、int8、int16、int32和int64。無符號整數(shù)類型有uint、uint8、uint16、uint32和uint64。整數(shù)類型的大小取決于計(jì)算機(jī)的架構(gòu),例如32位或64位。 浮點(diǎn)數(shù)類型(Floating-Point Types):Go語言提供float32和float64兩種浮點(diǎn)數(shù)類型,分別對應(yīng)單精度和雙精度浮點(diǎn)數(shù)。 復(fù)數(shù)類型(Complex Types):Go語言提供complex64和complex128兩種復(fù)數(shù)類型,分別對應(yīng)由兩個浮點(diǎn)數(shù)構(gòu)成的復(fù)數(shù)。 布爾類型(Boolean Type):布爾類型用于表示真(true)和假(false)的值,用于條件判斷和邏輯運(yùn)算。 字符串類型(String Type):字符串類型表示一系列字符。字符串是不可變的,可以使用雙引號"或反引號`來定義。 字符類型(Rune Type):字符類型rune用于表示Unicode字符,它是int32的別名。通常使用單引號'來表示字符,如'A'。 數(shù)組類型(Array Types):數(shù)組是具有固定大小的同類型元素集合。聲明數(shù)組時(shí)需要指定元素類型和大小。 切片類型(Slice Types):切片是對數(shù)組的一層封裝,是動態(tài)長度的可變序列。切片不保存元素,只是引用底層數(shù)組的一部分。 映射類型(Map Types):映射是鍵值對的無序集合,用于存儲和檢索數(shù)據(jù)。鍵和值可以是任意類型,但鍵必須是可比較的。 結(jié)構(gòu)體類型(Struct Types):結(jié)構(gòu)體是一種用戶定義的復(fù)合數(shù)據(jù)類型,可以包含不同類型的字段,每個字段有一個名字和類型。 接口類型(Interface Types):接口是一種抽象類型,用于定義一組方法。類型實(shí)現(xiàn)了接口的方法集合即為實(shí)現(xiàn)了該接口。 函數(shù)類型(Function Types):函數(shù)類型表示函數(shù)的簽名,包括參數(shù)和返回值類型。函數(shù)可以作為參數(shù)傳遞和返回。 通道類型(Channel Types):通道是用于在協(xié)程之間進(jìn)行通信和同步的一種機(jī)制。通道有發(fā)送和接收操作。 指針類型(Pointer Types):指針類型表示變量的內(nèi)存地址。通過指針可以直接訪問和修改變量的值。

Go語言的數(shù)據(jù)類型具有清晰的語法和語義,支持豐富的內(nèi)置功能。合理選擇和使用不同的數(shù)據(jù)類型可以提高程序的效率和可讀性。

具體代碼展開分析

package main

import "fmt"

type Person struct {

FirstName string

LastName string

Age int

}

type Shape interface {

Area() float64

}

type Circle struct {

Radius float64

}

func (c Circle) Area() float64 {

return 3.14 * c.Radius * c.Radius

}

func add(a, b int) int {

return a + b

}

func subtract(a, b int) int {

return a - b

}

type Operation func(int, int) int

func main() {

fmt.Println("整數(shù)類型(Integer Types)")

var x int = 10

var y int64 = 100

fmt.Println(x)

fmt.Println(y)

fmt.Println("浮點(diǎn)數(shù)類型(Floating-Point Types)")

var a float32 = 3.14

var b float64 = 3.14159265359

fmt.Println(a)

fmt.Println(b)

fmt.Println("布爾類型(Boolean Type)")

var isTrue bool = true

var isFalse bool = false

fmt.Println(isTrue)

fmt.Println(isFalse)

fmt.Println("字符串類型(String Type)")

str1 := "Hello, "

str2 := "Go!"

concatenated := str1 + str2

fmt.Println(concatenated)

fmt.Println("切片類型(Slice Types)")

numbers := []int{1, 2, 3, 4, 5}

fmt.Println(numbers)

// 修改切片元素

numbers[0] = 10

fmt.Println(numbers)

// 切片操作

subSlice := numbers[1:4]

fmt.Println(subSlice)

fmt.Println("映射類型(Map Types)")

ages := map[string]int{

"Alice": 25,

"Bob": 30,

"Eve": 28,

}

fmt.Println(ages)

fmt.Println("Alice's age:", ages["Alice"])

// 添加新的鍵值對

ages["Charlie"] = 22

fmt.Println(ages)

fmt.Println("結(jié)構(gòu)體類型(Struct Types)")

person := Person{

FirstName: "John",

LastName: "Doe",

Age: 30,

}

fmt.Println(person)

fmt.Println("Name:", person.FirstName, person.LastName)

fmt.Println("接口類型(Interface Types)")

var shape Shape

circle := Circle{Radius: 5}

shape = circle

fmt.Println("Circle Area:", shape.Area())

fmt.Println("函數(shù)類型(Function Types)")

var op Operation

op = add

result := op(10, 5)

fmt.Println("Addition:", result)

op = subtract

result = op(10, 5)

fmt.Println("Subtraction:", result)

fmt.Println("通道類型(Channel Types)")

messages := make(chan string)

go func() {

messages <- "Hello, Go!"

}()

msg := <-messages

fmt.Println(msg)

fmt.Println("指針類型(Pointer Types)")

x = 10

var ptr *int

ptr = &x

fmt.Println("Value of x:", x)

fmt.Println("Value stored in pointer:", *ptr)

*ptr = 20

fmt.Println("Updated value of x:", x)

}

下面逐個解釋代碼中涉及的知識點(diǎn):

type Person struct { ... }: 定義了一個結(jié)構(gòu)體類型 Person,表示一個人的信息,包括 FirstName、LastName 和 Age 字段。 type Shape interface { ... }: 定義了一個接口類型 Shape,該接口要求實(shí)現(xiàn)一個方法 Area() 返回一個 float64 類型。 type Circle struct { ... }: 定義了一個結(jié)構(gòu)體類型 Circle,表示一個圓的半徑。 func (c Circle) Area() float64 { ... }:為 Circle 類型實(shí)現(xiàn)了 Shape 接口的 Area() 方法,用于計(jì)算圓的面積。 func add(a, b int) int { ... }: 定義了一個函數(shù) add,用于執(zhí)行整數(shù)相加操作。 func subtract(a, b int) int { ... }: 定義了一個函數(shù) subtract,用于執(zhí)行整數(shù)相減操作。 type Operation func(int, int) int: 定義了一個函數(shù)類型 Operation,它接受兩個整數(shù)參數(shù)并返回一個整數(shù)結(jié)果。 main() { ... }: 程序的入口函數(shù)。

定義了多種不同類型的變量,包括整數(shù)、浮點(diǎn)數(shù)、布爾、字符串、切片、映射、結(jié)構(gòu)體、接口、函數(shù)、通道和指針類型。演示了不同類型變量的初始化、賦值、訪問以及基本操作。使用切片操作提取部分切片。演示了映射的使用,包括添加新的鍵值對和訪問鍵值對。演示了結(jié)構(gòu)體的定義和初始化,并訪問結(jié)構(gòu)體字段。展示了接口的使用,將 Circle 類型賦值給 Shape 類型變量,并調(diào)用接口方法。演示了函數(shù)類型的定義和使用,將不同函數(shù)賦值給 Operation 類型變量,并進(jìn)行調(diào)用。使用通道來實(shí)現(xiàn)并發(fā)通信,通過匿名函數(shù)在 goroutine 中發(fā)送和接收消息。演示了指針的使用,包括創(chuàng)建指針變量、通過指針修改變量的值等操作。

Go語言中類型轉(zhuǎn)換說明

Go語言支持類型轉(zhuǎn)換,但需要注意一些規(guī)則和限制。類型轉(zhuǎn)換用于將一個數(shù)據(jù)類型的值轉(zhuǎn)換為另一個數(shù)據(jù)類型,以便在不同的上下文中使用。以下是有關(guān)Go語言中類型轉(zhuǎn)換的一些重要信息:

基本類型之間的轉(zhuǎn)換: 可以在基本數(shù)據(jù)類型之間進(jìn)行轉(zhuǎn)換,但是必須注意類型的兼容性和可能導(dǎo)致的數(shù)據(jù)丟失。例如,從int到float64的轉(zhuǎn)換是安全的,但從float64到int可能導(dǎo)致小數(shù)部分被截?cái)唷? 顯示類型轉(zhuǎn)換: 在Go中,使用強(qiáng)制類型轉(zhuǎn)換來顯式指定將一個值轉(zhuǎn)換為另一個類型。語法是:destinationType(expression)。例如:float64(10)。 非兼容類型之間的轉(zhuǎn)換: 對于不兼容的類型,編譯器不會自動進(jìn)行轉(zhuǎn)換。例如,不能直接將一個string類型轉(zhuǎn)換為int類型。 類型別名的轉(zhuǎn)換: 如果有類型別名(Type Alias),在轉(zhuǎn)換時(shí)需要注意使用別名的兼容性。

以下是一些示例來展示類型轉(zhuǎn)換:

package main

import "fmt"

func main() {

// 顯式類型轉(zhuǎn)換

var x int = 10

var y float64 = float64(x)

fmt.Println(y)

// 類型別名的轉(zhuǎn)換

type Celsius float64

type Fahrenheit float64

c := Celsius(25)

f := Fahrenheit(c*9/5 + 32)

fmt.Println(f)

}

4.運(yùn)算符

前提:chapter2目錄下創(chuàng)建 operator,學(xué)習(xí)總結(jié)如下:

其實(shí)這部分和其他語言都差不多,個人覺得沒啥可復(fù)習(xí)鞏固的。Go語言支持多種運(yùn)算符,用于執(zhí)行各種算術(shù)、邏輯和比較操作。

常規(guī)運(yùn)算符

以下是一些常見的運(yùn)算符及其在Go中的使用方式和知識點(diǎn):

算術(shù)運(yùn)算符(Arithmetic Operators):

+:加法-:減法*:乘法/:除法%:取模(取余數(shù))

賦值運(yùn)算符(Assignment Operators):

=:賦值+=:加法賦值-=:減法賦值*=:乘法賦值/=:除法賦值%=:取模賦值

邏輯運(yùn)算符(Logical Operators):

&&:邏輯與(AND)||:邏輯或(OR)!:邏輯非(NOT)

比較運(yùn)算符(Comparison Operators):

==:等于!=:不等于<:小于>:大于<=:小于等于>=:大于等于

位運(yùn)算符(Bitwise Operators):

&:按位與(AND)|:按位或(OR)^:按位異或(XOR)<<:左移>>:右移

其他運(yùn)算符:

&:取地址運(yùn)算符*:指針運(yùn)算符++:自增運(yùn)算符--:自減運(yùn)算符

在使用運(yùn)算符時(shí),需要考慮以下幾點(diǎn):

運(yùn)算符的操作數(shù)必須與運(yùn)算符的預(yù)期類型匹配。某些運(yùn)算符具有更高的優(yōu)先級,需要使用括號來明確優(yōu)先級。運(yùn)算符的操作數(shù)可以是變量、常量、表達(dá)式等。

新建operator_test.go,以下是一些示例來展示運(yùn)算符的使用:

package operator

import (

"fmt"

"testing"

)

const (

Readable = 1 << iota

Writable

Executable

)

func TestOperatorBasic(t *testing.T) {

// 算術(shù)運(yùn)算符

a := 10

b := 5

fmt.Println("Sum:", a+b)

fmt.Println("Difference:", a-b)

fmt.Println("Product:", a*b)

fmt.Println("Quotient:", a/b)

fmt.Println("Remainder:", a%b)

// 邏輯運(yùn)算符

x := true

y := false

fmt.Println("AND:", x && y)

fmt.Println("OR:", x || y)

fmt.Println("NOT:", !x)

// 比較運(yùn)算符

fmt.Println("Equal:", a == b)

fmt.Println("Not Equal:", a != b)

fmt.Println("Greater Than:", a > b)

fmt.Println("Less Than:", a < b)

fmt.Println("Greater Than or Equal:", a >= b)

fmt.Println("Less Than or Equal:", a <= b)

}

func TestCompareArray(t *testing.T) {

a := [...]int{1, 2, 3, 4}

b := [...]int{1, 3, 2, 4}

// c := [...]int{1, 2, 3, 4, 5}

d := [...]int{1, 2, 3, 4}

t.Log(a == b)

//t.Log(a == c)

t.Log(a == d)

}

func TestBitClear(t *testing.T) {

a := 7 //0111

a = a &^ Readable

a = a &^ Executable

t.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable)

}

下面逐個解釋代碼中涉及的知識點(diǎn):

const (...): 定義了三個常量 Readable、Writable 和 Executable,使用位移操作生成不同的值。 func TestOperatorBasic(t *testing.T) { ... }: 定義了一個測試函數(shù) "TestOperatorBasic",用于測試基本運(yùn)算符的使用。 算術(shù)運(yùn)算符:展示了加法、減法、乘法、除法和取余運(yùn)算;邏輯運(yùn)算符:展示了邏輯與、邏輯或和邏輯非運(yùn)算;比較運(yùn)算符:展示了等于、不等于、大于、小于、大于等于和小于等于運(yùn)算。 func TestCompareArray(t *testing.T) { ... }: 定義了一個測試函數(shù) "TestCompareArray",用于測試數(shù)組的比較。 聲明了兩個整數(shù)數(shù)組 a 和 b,以及另一個數(shù)組 d,其中數(shù)組 a 和數(shù)組 d 的內(nèi)容相同;使用比較運(yùn)算符 == 檢查數(shù)組 a 和 b 是否相等,以及數(shù)組 a 和 d 是否相等。 func TestBitClear(t *testing.T) { ... }: 定義了一個測試函數(shù) "TestBitClear",用于測試位清除操作。 聲明一個整數(shù)變量 a,并將其初始化為 7,即二進(jìn)制表示 0111;使用位清除操作 &^ 將 a 中的 Readable 和 Executable 位清除;使用按位與運(yùn)算 & 檢查 a 是否具有 Readable、Writable 和 Executable 屬性。

按位清除運(yùn)算符 &^

在Go語言中,&^ 是按位清除運(yùn)算符(Bit Clear Operator)。它用于將某些位置上的位清零,即將指定位置上的位設(shè)置為0。&^ 運(yùn)算符在處理二進(jìn)制位操作時(shí)非常有用。

&^ 運(yùn)算符執(zhí)行以下操作:

對于每個位,如果右側(cè)操作數(shù)的對應(yīng)位為 0,則結(jié)果位與左側(cè)操作數(shù)相同。對于每個位,如果右側(cè)操作數(shù)的對應(yīng)位為 1,則結(jié)果位被強(qiáng)制設(shè)置為 0。

這意味著,&^ 運(yùn)算符用于“清除”左側(cè)操作數(shù)的特定位,使其與右側(cè)操作數(shù)的相應(yīng)位不受影響。寫個代碼驗(yàn)證下:

func TestOther(t *testing.T) {

var a uint8 = 0b11001100 // 二進(jìn)制表示,十進(jìn)制為 204

var b uint8 = 0b00110011 // 二進(jìn)制表示,十進(jìn)制為 51

result := a &^ b

fmt.Printf("a: %08b\n", a) // 輸出:11001100

fmt.Printf("b: %08b\n", b) // 輸出:00110011

fmt.Printf("Result: %08b\n", result) // 輸出:11000000

fmt.Println("Result (Decimal):", result) // 輸出:192

}

5.條件語句(Conditional Statements)

前提:chapter2目錄下創(chuàng)建 condition,學(xué)習(xí)總結(jié)如下:

if 語句

if 語句用于基于條件來決定是否執(zhí)行某段代碼。它的基本語法如下:

if condition {

// 代碼塊

} else if anotherCondition {

// 代碼塊

} else {

// 代碼塊

}

switch 語句

switch 語句用于基于表達(dá)式的不同值執(zhí)行不同的代碼分支。與其他語言不同,Go的switch可以自動匹配第一個滿足條件的分支,而無需使用break語句。它的語法如下:

switch expression {

case value1:

// 代碼塊

case value2:

// 代碼塊

default:

// 代碼塊

}

創(chuàng)建condition_test.go進(jìn)行驗(yàn)證分析, 具體代碼如下:

package condition

import (

"fmt"

"testing"

)

func TestConditionIf(t *testing.T) {

age := 18

if age < 18 {

fmt.Println("You are a minor.")

} else if age >= 18 && age < 60 {

fmt.Println("You are an adult.")

} else {

fmt.Println("You are a senior citizen.")

}

}

func TestConditionSwitch(t *testing.T) {

dayOfWeek := 3

switch dayOfWeek {

case 1:

fmt.Println("Monday")

case 2:

fmt.Println("Tuesday")

case 3:

fmt.Println("Wednesday")

case 4:

fmt.Println("Thursday")

case 5:

fmt.Println("Friday")

default:

fmt.Println("Weekend")

}

}

func TestSwitchMultiCase(t *testing.T) {

for i := 0; i < 5; i++ {

switch i {

case 0, 2:

t.Logf("%d is Even", i)

case 1, 3:

t.Logf("%d is Odd", i)

default:

t.Logf("%d is not 0-3", i)

}

}

}

func TestSwitchCaseCondition(t *testing.T) {

for i := 0; i < 5; i++ {

switch {

case i%2 == 0:

t.Logf("%d is Even", i)

case i%2 == 1:

t.Logf("%d is Odd", i)

default:

t.Logf("%d is unknow", i)

}

}

}

下面逐個解釋每個測試函數(shù)的內(nèi)容:

func TestConditionIf(t *testing.T) { ... }:測試 if 語句的使用。 根據(jù)年齡的不同情況,通過 if、else if 和 else 分支判斷是否為未成年人、成年人或老年人。 func TestConditionSwitch(t *testing.T) { ... }:測試 switch 語句的使用。根據(jù) dayOfWeek 的值,使用 switch 語句輸出對應(yīng)的星期幾。 func TestSwitchMultiCase(t *testing.T) { ... }:測試 switch 語句多個 case 值的情況。使用 switch 語句判斷每個數(shù)字的奇偶性,并輸出相應(yīng)的信息。 func TestSwitchCaseCondition(t *testing.T) { ... }:測試 switch 語句中的條件表達(dá)式。使用 switch 語句通過對數(shù)字取余判斷數(shù)字的奇偶性,并輸出相應(yīng)的信息。

這些測試函數(shù)展示了Go語言中條件語句的不同用法,包括基于條件的分支判斷和多個 case 值的處理,以及在 switch 語句中使用條件表達(dá)式的情況。

6.循環(huán)語句(Loop Statements)

前提:chapter2目錄下創(chuàng)建 loop,學(xué)習(xí)總結(jié)如下:

for 循環(huán)

for 循環(huán)用于重復(fù)執(zhí)行代碼塊,支持初始化語句、循環(huán)條件和循環(huán)后的語句。它的基本形式如下:

for initialization; condition; post {

// 代碼塊

}

在初始化語句中,您可以初始化循環(huán)變量,然后在循環(huán)體中使用條件來控制循環(huán),最后在 post 語句中執(zhí)行遞增或遞減操作。

for 循環(huán)的簡化形式

Go語言的 for 循環(huán)還可以簡化成只有循環(huán)條件部分,類似于其他語言中的 while 循環(huán):

for condition {

// 代碼塊

}

range 循環(huán)

range 循環(huán)用于迭代數(shù)組、切片、映射、字符串等可迭代的數(shù)據(jù)結(jié)構(gòu)。它返回每次迭代的索引和值。示例:

for index, value := range iterable {

// 使用 index 和 value

}

創(chuàng)建loop_test.go進(jìn)行驗(yàn)證分析, 具體代碼如下:

package loop

import (

"fmt"

"testing"

)

func TestLoopFor(t *testing.T) {

for i := 1; i <= 5; i++ {

fmt.Println("Iteration:", i)

}

}

func TestLoopForBasic(t *testing.T) {

i := 1

for i <= 5 {

fmt.Println("Iteration:", i)

i++

}

}

func TestLoopForRange(t *testing.T) {

numbers := []int{1, 2, 3, 4, 5}

for index, value := range numbers {

fmt.Printf("Index: %d, Value: %d\n", index, value)

}

}

func TestLoopForUnLimit(t *testing.T) {

i := 1

for {

fmt.Println("Iteration:", i)

i++

if i > 5 {

break

}

}

}

下面逐個解釋每個測試函數(shù)的內(nèi)容:

func TestLoopFor(t *testing.T) { ... }:測試基本的 for 循環(huán)。使用 for 循環(huán),從 1 到 5 迭代輸出循環(huán)迭代次數(shù)。 func TestLoopForBasic(t *testing.T) { ... }:測試不帶初始化語句的 for 循環(huán)。使用 for 循環(huán),從 1 到 5 迭代輸出循環(huán)迭代次數(shù),但沒有在循環(huán)頭部聲明初始化語句。 func TestLoopForRange(t *testing.T) { ... }:測試使用 for range 迭代切片。定義一個整數(shù)切片 numbers,使用 for range 循環(huán)迭代切片中的每個元素,輸出元素的索引和值。 func TestLoopForUnLimit(t *testing.T) { ... }:測試無限循環(huán)及 break 語句。使用無限循環(huán)和 break 語句,在循環(huán)體內(nèi)部判斷是否終止循環(huán),當(dāng) i 大于 5 時(shí)退出循環(huán)。

這些測試函數(shù)展示了Go語言中不同類型的 for 循環(huán)的用法,包括標(biāo)準(zhǔn)的計(jì)數(shù)循環(huán)、不帶初始化語句的循環(huán)、遍歷切片以及無限循環(huán)與循環(huán)終止條件。

7.跳轉(zhuǎn)語句(Jump Statements)

前提:chapter2目錄下創(chuàng)建 jump,學(xué)習(xí)總結(jié)如下:

Go語言也支持幾種跳轉(zhuǎn)語句,用于在循環(huán)和條件中控制流程:

break:跳出循環(huán)。continue:跳過本次循環(huán)迭代,繼續(xù)下一次迭代。goto:在代碼中直接跳轉(zhuǎn)到指定標(biāo)簽處(不推薦使用)。

創(chuàng)建jump_test.go進(jìn)行驗(yàn)證分析, 具體代碼如下:

package jump

import (

"fmt"

"testing"

)

func TestJumpBreak(t *testing.T) {

for i := 1; i <= 5; i++ {

if i == 3 {

break

}

fmt.Println("Iteration:", i)

}

}

func TestJumpContinue(t *testing.T) {

for i := 1; i <= 5; i++ {

if i == 3 {

continue

}

fmt.Println("Iteration:", i)

}

}

func TestJumpGoto(t *testing.T) {

i := 1

start:

fmt.Println("Iteration:", i)

i++

if i <= 5 {

goto start

}

}

下面逐個解釋每個測試函數(shù)的內(nèi)容:

func TestJumpBreak(t *testing.T) { ... }:測試 break 語句的使用。使用 for 循環(huán)迭代從 1 到 5,但當(dāng)?shù)兞?i 等于 3 時(shí),使用 break 語句終止循環(huán)。 func TestJumpContinue(t *testing.T) { ... }:測試 continue 語句的使用。使用 for 循環(huán)迭代從 1 到 5,但當(dāng)?shù)兞?i 等于 3 時(shí),使用 continue 語句跳過該次迭代繼續(xù)下一次迭代。 func TestJumpGoto(t *testing.T) { ... }:測試 goto 語句的使用。使用 goto 語句實(shí)現(xiàn)了一個無限循環(huán),即使用標(biāo)簽 start 和 goto start 在循環(huán)體內(nèi)部跳轉(zhuǎn)到循環(huán)的起始位置。循環(huán)的終止條件是當(dāng) i 大于 5 時(shí)。

這些測試函數(shù)展示了Go語言中的循環(huán)控制跳轉(zhuǎn)語句,包括用于終止循環(huán)的 break、用于跳過當(dāng)前迭代的 continue,以及用于無限循環(huán)的 goto 語句。

(三)常用集合和字符串

src目錄下創(chuàng)建chapter3,在Go語言中,集合是存儲一組值的數(shù)據(jù)結(jié)構(gòu)。常用的集合類型包括數(shù)組、切片、映射和通道。

1.數(shù)組

前提:chapter3目錄下創(chuàng)建 array,學(xué)習(xí)總結(jié)如下:

Go語言中的數(shù)組是一種固定長度、同類型元素的集合。

數(shù)組的特點(diǎn)

數(shù)組的長度在聲明時(shí)指定,且在創(chuàng)建后不可更改。數(shù)組是值類型,當(dāng)數(shù)組被賦值給新變量或作為參數(shù)傳遞時(shí),會創(chuàng)建一個新的副本。數(shù)組在內(nèi)存中是連續(xù)存儲的,支持隨機(jī)訪問。

數(shù)組的聲明和初始化

var arrayName [size]dataType

arrayName:數(shù)組的名稱。size:數(shù)組的長度,必須是一個常量表達(dá)式。dataType:數(shù)組存儲的元素類型。

數(shù)組的初始化方式

// 使用指定的值初始化數(shù)組

var arr = [5]int{1, 2, 3, 4, 5}

// 根據(jù)索引初始化數(shù)組

var arr [5]int

arr[0] = 10

arr[1] = 20

// 部分初始化

var arr = [5]int{1, 2}

// 自動推斷數(shù)組長度

arr := [...]int{1, 2, 3, 4, 5}

數(shù)組的訪問和遍歷

// 訪問單個元素

value := arr[index]

// 遍歷數(shù)組

for index, value := range arr {

fmt.Printf("Index: %d, Value: %d\n", index, value)

}

數(shù)組作為函數(shù)參數(shù)

數(shù)組在函數(shù)參數(shù)傳遞時(shí)會創(chuàng)建副本,因此對函數(shù)內(nèi)的數(shù)組修改不會影響原始數(shù)組。如果需要在函數(shù)內(nèi)修改原始數(shù)組,可以傳遞指向數(shù)組的指針。

func modifyArray(arr [5]int) {

arr[0] = 100

}

func modifyArrayByPointer(arr *[5]int) {

arr[0] = 100

}

多維數(shù)組

Go語言支持多維數(shù)組,例如二維數(shù)組和三維數(shù)組。多維數(shù)組的初始化和訪問與一維數(shù)組類似,只需要指定多個索引。

var matrix [3][3]int = [3][3]int{

{1, 2, 3},

{4, 5, 6},

{7, 8, 9},

}

數(shù)組在存儲固定數(shù)量的同類型元素時(shí)非常有用,但由于其固定長度的限制,通常在實(shí)際開發(fā)中更常用的是切片,它具有動態(tài)長度的特性。切片可以根據(jù)需要進(jìn)行增加、刪除和重新分配,更加靈活。

創(chuàng)建array_test.go進(jìn)行驗(yàn)證分析, 具體代碼如下:

package array

import "testing"

func TestArrayInit(t *testing.T) {

var arr [3]int

arr1 := [4]int{1, 2, 3, 4}

arr3 := [...]int{1, 3, 4, 5}

arr1[1] = 5

t.Log(arr[1], arr[2])

t.Log(arr1, arr3)

}

func TestArrayTravel(t *testing.T) {

arr3 := [...]int{1, 3, 4, 5}

for i := 0; i < len(arr3); i++ {

t.Log(arr3[i])

}

for _, e := range arr3 {

t.Log(e)

}

}

func TestArraySection(t *testing.T) {

arr3 := [...]int{1, 2, 3, 4, 5}

arr3_sec := arr3[:]

t.Log(arr3_sec)

}

下面逐個解釋每個測試函數(shù)的內(nèi)容:

func TestArrayInit(t *testing.T) { ... }:測試數(shù)組的初始化。 使用不同的方式初始化數(shù)組 arr,arr1 和 arr3;修改 arr1 的第二個元素為 5;使用 t.Log() 輸出不同數(shù)組的元素值和內(nèi)容。 func TestArrayTravel(t *testing.T) { ... }:測試數(shù)組的遍歷。 使用 for 循環(huán)遍歷數(shù)組 arr3,分別輸出每個元素的值;使用 for range 循環(huán)遍歷數(shù)組 arr3,同樣輸出每個元素的值。 func TestArraySection(t *testing.T) { ... }:測試數(shù)組切片的使用。 創(chuàng)建一個數(shù)組切片 arr3_sec,基于整個數(shù)組 arr3;使用 t.Log() 輸出數(shù)組切片 arr3_sec 的內(nèi)容。

2.切片

前提:chapter3目錄下創(chuàng)建 slice,學(xué)習(xí)總結(jié)如下:

Go語言中的切片(Slice)是對數(shù)組的一層封裝,提供了更靈活的動態(tài)長度序列。

切片的特點(diǎn)

切片是引用類型,它不保存數(shù)據(jù),只是引用底層數(shù)組的一部分。切片是動態(tài)長度的,可以根據(jù)需要進(jìn)行擴(kuò)容或縮減。切片是可索引的,并且可以通過切片索引進(jìn)行切割。

切片的聲明和初始化

var sliceName []elementType

切片的初始化方式

// 聲明切片并初始化

var slice = []int{1, 2, 3, 4, 5}

// 使用 make 函數(shù)創(chuàng)建切片

var slice = make([]int, 5) // 創(chuàng)建長度為 5 的 int 類型切片

// 使用切片切割已有數(shù)組或切片

newSlice := oldSlice[startIndex:endIndex] // 包括 startIndex,但不包括 endIndex

切片的內(nèi)置函數(shù)和操作

len(slice):返回切片的長度。cap(slice):返回切片的容量,即底層數(shù)組的長度。append(slice, element):將元素追加到切片末尾,并返回新的切片。copy(destination, source):將源切片中的元素復(fù)制到目標(biāo)切片。

切片的遍歷

for index, value := range slice {

// 使用 index 和 value

}

切片作為函數(shù)參數(shù)

切片作為參數(shù)傳遞給函數(shù)時(shí),函數(shù)內(nèi)部對切片的修改會影響到原始切片。

func modifySlice(s []int) {

s[0] = 100

}

func main() {

numbers := []int{1, 2, 3, 4, 5}

modifySlice(numbers)

fmt.Println(numbers) // 輸出:[100 2 3 4 5]

}

切片在Go語言中廣泛用于處理動態(tài)數(shù)據(jù)集,例如集合、列表、隊(duì)列等。它提供了方便的方法來管理元素,同時(shí)避免了固定數(shù)組的限制。在實(shí)際應(yīng)用中,切片經(jīng)常被用于存儲和處理變長數(shù)據(jù)。

創(chuàng)建slice_test.go進(jìn)行驗(yàn)證分析, 具體代碼如下:

package slice

import (

"fmt"

"testing"

)

func TestSlice(t *testing.T) {

// 聲明和初始化切片

numbers := []int{1, 2, 3, 4, 5}

fmt.Println("Original Slice:", numbers)

// 使用 make 函數(shù)創(chuàng)建切片

slice := make([]int, 3)

fmt.Println("Initial Make Slice:", slice)

// 添加元素到切片

slice = append(slice, 10)

slice = append(slice, 20, 30)

fmt.Println("After Append:", slice)

// 復(fù)制切片

copySlice := make([]int, len(slice))

copy(copySlice, slice)

fmt.Println("Copied Slice:", copySlice)

// 切片切割

subSlice := numbers[1:3]

fmt.Println("Subslice:", subSlice)

// 修改切片的值會影響底層數(shù)組和其他切片

subSlice[0] = 100

fmt.Println("Modified Subslice:", subSlice)

fmt.Println("Original Slice:", numbers)

fmt.Println("Copied Slice:", copySlice)

// 遍歷切片

for index, value := range slice {

fmt.Printf("Index: %d, Value: %d\n", index, value)

}

}

下面逐個解釋每個測試函數(shù)的內(nèi)容:

func TestSlice(t *testing.T) { ... }:測試切片的基本操作。

聲明和初始化切片 numbers,輸出初始切片內(nèi)容。使用 make 函數(shù)創(chuàng)建初始容量為 3 的切片 slice,輸出初始切片內(nèi)容。使用 append 函數(shù)向切片 slice 添加元素。使用 copy 函數(shù)復(fù)制切片 slice 到新的切片 copySlice。使用切片 numbers 進(jìn)行切片切割,創(chuàng)建子切片 subSlice。修改 subSlice 的第一個元素為 100,輸出修改后的切片和原始切片,以及復(fù)制的切片。使用 for range 循環(huán)遍歷切片 slice,輸出每個元素的索引和值。

這個測試函數(shù)展示了Go語言中切片的各種操作,包括切片的創(chuàng)建、添加元素、復(fù)制切片、切片切割、修改切片元素等。

3.Map

前提:chapter3目錄下創(chuàng)建 map,學(xué)習(xí)總結(jié)如下:

Go語言中的映射(Map)是鍵值對的無序集合,也被稱為關(guān)聯(lián)數(shù)組或字典。

映射的特點(diǎn)

映射用于存儲一組鍵值對,其中每個鍵都是唯一的。映射是無序的,無法保證鍵值對的順序。鍵可以是任何可比較的類型,值可以是任意類型。映射是引用類型,可以被賦值和傳遞給函數(shù)。

映射的聲明和初始化

var mapName map[keyType]valueType

映射的初始化方式

// 聲明和初始化映射

var ages = map[string]int{

"Alice": 25,

"Bob": 30,

"Eve": 28,

}

// 使用 make 函數(shù)創(chuàng)建映射

var ages = make(map[string]int)

映射的操作

添加鍵值對:ages["Charlie"] = 35刪除鍵值對:delete(ages, "Eve")獲取值:value := ages["Alice"]

映射的遍歷

for key, value := range ages {

fmt.Printf("Name: %s, Age: %d\n", key, value)

}

映射作為函數(shù)參數(shù)

映射作為參數(shù)傳遞給函數(shù)時(shí),函數(shù)內(nèi)部對映射的修改會影響到原始映射。

func modifyMap(m map[string]int) {

m["Alice"] = 30

}

func main() {

ages := map[string]int{

"Alice": 25,

"Bob": 30,

}

modifyMap(ages)

fmt.Println(ages) // 輸出:map[Alice:30 Bob:30]

}

映射在Go語言中用于存儲和檢索數(shù)據(jù),是一種非常常用的數(shù)據(jù)結(jié)構(gòu)。它在存儲一組關(guān)聯(lián)的鍵值對時(shí)非常有用,比如存儲姓名與年齡的對應(yīng)關(guān)系、單詞與定義的對應(yīng)關(guān)系等。在實(shí)際應(yīng)用中,映射是處理和存儲鍵值數(shù)據(jù)的重要工具。

創(chuàng)建map_test.go進(jìn)行驗(yàn)證分析, 具體代碼如下:

package my_map

import (

"fmt"

"testing"

)

func TestBasic(t *testing.T) {

// 聲明和初始化映射

ages := map[string]int{

"Alice": 25,

"Bob": 30,

"Eve": 28,

}

fmt.Println("Original Map:", ages)

// 添加新的鍵值對

ages["Charlie"] = 35

fmt.Println("After Adding:", ages)

// 修改已有鍵的值

ages["Bob"] = 31

fmt.Println("After Modification:", ages)

// 刪除鍵值對

delete(ages, "Eve")

fmt.Println("After Deletion:", ages)

// 獲取值和檢查鍵是否存在

age, exists := ages["Alice"]

if exists {

fmt.Println("Alice's Age:", age)

} else {

fmt.Println("Alice not found")

}

// 遍歷映射

for name, age := range ages {

fmt.Printf("Name: %s, Age: %d\n", name, age)

}

}

type Student struct {

Name string

Age int

Grade string

}

func TestComplex(t *testing.T) {

// 聲明和初始化映射,用于存儲學(xué)生信息和成績

studentScores := make(map[string]int)

studentInfo := make(map[string]Student)

// 添加學(xué)生信息和成績

studentInfo["Alice"] = Student{Name: "Alice", Age: 18, Grade: "A"}

studentScores["Alice"] = 95

studentInfo["Bob"] = Student{Name: "Bob", Age: 19, Grade: "B"}

studentScores["Bob"] = 85

// 查找學(xué)生信息和成績

aliceInfo := studentInfo["Alice"]

aliceScore := studentScores["Alice"]

fmt.Printf("Name: %s, Age: %d, Grade: %s, Score: %d\n", aliceInfo.Name, aliceInfo.Age, aliceInfo.Grade, aliceScore)

// 遍歷學(xué)生信息和成績

for name, info := range studentInfo {

score, exists := studentScores[name]

if exists {

fmt.Printf("Name: %s, Age: %d, Grade: %s, Score: %d\n", info.Name, info.Age, info.Grade, score)

} else {

fmt.Printf("No score available for %s\n", name)

}

}

}

下面逐個解釋每個測試函數(shù)的內(nèi)容:

func TestBasic(t *testing.T) { ... }:測試映射的基本操作。 聲明和初始化映射 ages,存儲人名和年齡的鍵值對;輸出初始映射內(nèi)容;使用 ages["Charlie"] 添加新的鍵值對;使用 ages["Bob"] 修改已有鍵的值;使用 delete 函數(shù)刪除鍵值對;使用 age, exists 來獲取值并檢查鍵是否存在;使用 for range 循環(huán)遍歷映射,輸出每個鍵值對的信息。 type Student struct { ... }:定義了一個名為 Student 的結(jié)構(gòu)體,用于存儲學(xué)生信息。 func TestComplex(t *testing.T) { ... }:測試包含復(fù)雜值的映射操作。 聲明和初始化兩個映射,studentScores 用于存儲學(xué)生分?jǐn)?shù),studentInfo 用于存儲學(xué)生信息;添加學(xué)生信息和分?jǐn)?shù)到映射;使用 studentInfo["Alice"] 獲取學(xué)生信息,使用 studentScores["Alice"] 獲取學(xué)生分?jǐn)?shù);使用 for range 循環(huán)遍歷映射,輸出每個學(xué)生的信息和分?jǐn)?shù)。

這些測試函數(shù)展示了Go語言中映射的各種操作,包括創(chuàng)建、添加、修改、刪除鍵值對,檢查鍵是否存在,以及遍歷映射的鍵值對。

4.實(shí)現(xiàn)Set

前提:chapter3目錄下創(chuàng)建 set,學(xué)習(xí)總結(jié)如下:

在Go語言中,雖然標(biāo)準(zhǔn)庫沒有提供內(nèi)置的Set類型,但你可以使用多種方式來實(shí)現(xiàn)Set的功能。以下是幾種常見的實(shí)現(xiàn)Set的方式介紹:

使用切片

創(chuàng)建set_slice_test.go練習(xí)

使用切片來存儲元素,通過遍歷切片來檢查元素是否存在。這是一個簡單的實(shí)現(xiàn)方式,適用于小型的集合。

package set

import (

"fmt"

"testing"

)

type IntSet struct {

elements []int

}

func (s *IntSet) Add(element int) {

if !s.Contains(element) {

s.elements = append(s.elements, element)

}

}

func (s *IntSet) Contains(element int) bool {

for _, e := range s.elements {

if e == element {

return true

}

}

return false

}

func TestSet(t *testing.T) {

set := IntSet{}

set.Add(1)

set.Add(2)

set.Add(3)

set.Add(2) // Adding duplicate, should be ignored

fmt.Println("Set:", set.elements) // Output: [1 2 3]

}

使用映射

創(chuàng)建set_map_test.go練習(xí)

使用映射來存儲元素,映射的鍵代表集合的元素,值可以是任意類型。這樣的實(shí)現(xiàn)方式更快速,適用于大型的集合,因?yàn)橛成涞牟檎覐?fù)雜度為 O(1)。

package set

import (

"fmt"

"testing"

)

type Set map[int]bool

func (s Set) Add(element int) {

s[element] = true

}

func (s Set) Contains(element int) bool {

return s[element]

}

func TestSetMap(t *testing.T) {

set := make(Set)

set.Add(1)

set.Add(2)

set.Add(3)

set.Add(2) // Adding duplicate, should be ignored

fmt.Println("Set:", set) // Output: map[1:true 2:true 3:true]

}

使用第三方庫

創(chuàng)建set_third_test.go練習(xí)

為了避免自行實(shí)現(xiàn),你可以使用一些第三方庫,例如 github.com/deckarep/golang-set,它提供了更豐富的Set功能。

添加個代理:go env -w GOPROXY=https://goproxy.io,direct

然后安裝包:go get github.com/deckarep/golang-set

package set

import (

"fmt"

"github.com/deckarep/golang-set"

"testing"

)

func TestSetThird(t *testing.T) {

intSet := mapset.NewSet()

intSet.Add(1)

intSet.Add(2)

intSet.Add(3)

intSet.Add(2) // Adding duplicate, will be ignored

fmt.Println("Set:", intSet) // Output: Set: Set{1, 2, 3}

}

以上是幾種實(shí)現(xiàn)Set的方式,你可以根據(jù)需求和性能考慮選擇適合的實(shí)現(xiàn)方式。第三方庫可以提供更多功能和性能優(yōu)化,適用于大規(guī)模的數(shù)據(jù)集合。

5.字符串

前提:chapter3目錄下創(chuàng)建 string,學(xué)習(xí)總結(jié)如下:

字符串的聲明與初始化

在Go語言中,字符串是由一系列字符組成的,可以使用雙引號 " 或反引號 ``` 來聲明和初始化字符串。

package main

import "fmt"

func main() {

str1 := "Hello, World!" // 使用雙引號聲明

str2 := `Go Programming` // 使用反引號聲明

fmt.Println(str1) // Output: Hello, World!

fmt.Println(str2) // Output: Go Programming

}

字符串的長度

使用內(nèi)置函數(shù) len() 可以獲取字符串的長度,即字符串中字符的個數(shù)。

package main

import "fmt"

func main() {

str := "Hello, 世界!"

length := len(str)

fmt.Println("String Length:", length) // Output: String Length: 9

}

字符串的索引與切片

字符串中的字符可以通過索引訪問,索引從0開始??梢允褂们衅僮鱽慝@取字符串的子串。

package main

import "fmt"

func main() {

str := "Hello, World!"

// 獲取第一個字符

firstChar := str[0]

fmt.Println("First Character:", string(firstChar)) // Output: First Character: H

// 獲取子串

substring := str[7:12]

fmt.Println("Substring:", substring) // Output: Substring: World

}

字符串拼接

使用 + 運(yùn)算符可以將兩個字符串連接成一個新的字符串。另外,strings.Join 函數(shù)用于將字符串切片連接成一個新的字符串,可以用來拼接多個字符串。

最后,使用字節(jié)緩沖可以在不產(chǎn)生多余字符串副本的情況下進(jìn)行高效的字符串拼接。

package main

import (

"fmt"

"strings"

"bytes"

)

func main() {

str1 := "Hello, "

str2 := "World!"

result := str1 + str2

fmt.Println("Concatenated String:", result) // Output: Concatenated String: Hello, World!

strSlice := []string{"Hello", " ", "World!"}

result := strings.Join(strSlice, "")

fmt.Println(result) // Output: Hello World!

var buffer bytes.Buffer

buffer.WriteString(str1)

buffer.WriteString(str2)

result := buffer.String()

fmt.Println(result) // Output: Hello, World!

}

多行字符串

使用反引號 ``` 來創(chuàng)建多行字符串。

package main

import "fmt"

func main() {

multiLineStr := `

This is a

multi-line

string.

`

fmt.Println(multiLineStr)

}

字符串迭代

使用 for range 循環(huán)迭代字符串的每個字符。

package main

import "fmt"

func main() {

str := "Go語言"

for _, char := range str {

fmt.Printf("%c ", char) // Output: G o 語 言

}

}

字符串和字節(jié)數(shù)組之間的轉(zhuǎn)換

在Go語言中,字符串和字節(jié)數(shù)組之間可以進(jìn)行相互轉(zhuǎn)換。

package main

import "fmt"

func main() {

str := "Hello"

bytes := []byte(str) // 轉(zhuǎn)換為字節(jié)數(shù)組

strAgain := string(bytes) // 字節(jié)數(shù)組轉(zhuǎn)換為字符串

fmt.Println("Bytes:", bytes) // Output: Bytes: [72 101 108 108 111]

fmt.Println("String Again:", strAgain) // Output: String Again: Hello

}

字符串比較

字符串的比較可以使用 == 和 != 運(yùn)算符。當(dāng)然還有其他函數(shù)類型的直接應(yīng)用的:strings.Compare 函數(shù)用于比較兩個字符串,并根據(jù)比較結(jié)果返回一個整數(shù)。

也可以使用自定義的比較函數(shù)來比較字符串,根據(jù)自己的需求定義比較邏輯。

package main

import (

"fmt"

"strings"

)

func customCompare(str1, str2 string) bool {

// 自定義比較邏輯

return str1 == str2

}

func main() {

str1 := "Hello"

str2 := "World"

if str1 == str2 {

fmt.Println("Strings are equal")

} else {

fmt.Println("Strings are not equal") // Output: Strings are not equal

}

result := strings.Compare(str1, str2)

if result == 0 {

fmt.Println("Strings are equal")

} else if result < 0 {

fmt.Println("str1 is less than str2")

} else {

fmt.Println("str1 is greater than str2") // Output: str1 is less than str2

}

if customCompare(str1, str2) {

fmt.Println("Strings are equal")

} else {

fmt.Println("Strings are not equal") // Output: Strings are not equal

}

}

這些基本概念和操作可以幫助你更好地理解和使用Go語言中的字符串。要注意字符串的不可變性,以及與其他數(shù)據(jù)類型的轉(zhuǎn)換和比較。

創(chuàng)建string_test.go練習(xí)

package string

import (

"strconv"

"strings"

"testing"

)

func TestString(t *testing.T) {

var s string

t.Log(s) //初始化為默認(rèn)零值“”

s = "hello"

t.Log(len(s))

//s[1] = '3' //string是不可變的byte slice

//s = "\xE4\xB8\xA5" //可以存儲任何二進(jìn)制數(shù)據(jù)

s = "\xE4\xBA\xBB\xFF"

t.Log(s)

t.Log(len(s))

s = "中"

t.Log(len(s)) //是byte數(shù)

c := []rune(s)

t.Log(len(c))

// t.Log("rune size:", unsafe.Sizeof(c[0]))

t.Logf("中 unicode %x", c[0])

t.Logf("中 UTF8 %x", s)

}

func TestStringToRune(t *testing.T) {

s := "中華人民共和國"

for _, c := range s {

t.Logf("%[1]c %[1]x", c)

}

}

func TestStringFn(t *testing.T) {

s := "A,B,C"

parts := strings.Split(s, ",")

for _, part := range parts {

t.Log(part)

}

t.Log(strings.Join(parts, "-"))

}

func TestConv(t *testing.T) {

s := strconv.Itoa(10)

t.Log("str" + s)

if i, err := strconv.Atoi("10"); err == nil {

t.Log(10 + i)

}

}

下面逐個解釋每個測試函數(shù)的內(nèi)容:

func TestString(t *testing.T) { ... }:測試字符串的基本操作。 聲明一個字符串變量 s,輸出其默認(rèn)零值;將字符串賦值為 "hello",輸出字符串長度;嘗試修改字符串的某個字符,但會報(bào)錯,因?yàn)樽址遣豢勺?;使用字符串存儲二進(jìn)制數(shù)據(jù)和 Unicode 編碼;使用字符串存儲一個中文字符,并輸出其長度;將字符串轉(zhuǎn)換為 rune 類型切片,輸出切片長度和中文字符的 Unicode 和 UTF-8 編碼。 func TestStringToRune(t *testing.T) { ... }:測試字符串到 rune 的轉(zhuǎn)換。 聲明一個包含中文字符的字符串 s,通過 range 遍歷將字符串轉(zhuǎn)換為 rune 類型并輸出。 func TestStringFn(t *testing.T) { ... }:測試字符串相關(guān)的函數(shù)。 聲明一個包含逗號分隔的字符串 s,使用 strings.Split 函數(shù)拆分字符串并輸出每個部分。使用 strings.Join 函數(shù)將拆分的部分合并為一個新的字符串,并輸出。 func TestConv(t *testing.T) { ... }:測試字符串與其他類型的轉(zhuǎn)換。 使用 strconv.Itoa 將整數(shù)轉(zhuǎn)換為字符串;拼接字符串和整數(shù),并輸出結(jié)果;使用 strconv.Atoi 將字符串轉(zhuǎn)換為整數(shù),并進(jìn)行加法運(yùn)算,處理錯誤情況。

這些測試函數(shù)展示了Go語言中字符串的各種操作,包括字符串長度、UTF-8 編碼、rune 類型轉(zhuǎn)換、字符串拆分和合并,以及字符串與其他類型的轉(zhuǎn)換。

(四)函數(shù)

src目錄下創(chuàng)建chapter4,在Go語言中,函數(shù)是一種用于執(zhí)行特定任務(wù)的代碼塊,可以被多次調(diào)用。

1.函數(shù)的聲明

在Go中,函數(shù)的聲明由關(guān)鍵字 func 開始,后面跟著函數(shù)名、參數(shù)列表、返回值和函數(shù)體。

func functionName(parameters) returnType {

// 函數(shù)體

// 可以包含多個語句

return returnValue

}

2.函數(shù)參數(shù)

函數(shù)可以有零個或多個參數(shù),參數(shù)由參數(shù)名和參數(shù)類型組成。參數(shù)之間使用逗號分隔。

func greet(name string) {

fmt.Printf("Hello, %s!\n", name)

}

3.多返回值

Go語言的函數(shù)可以返回多個值。返回值用括號括起來,逗號分隔。

func divide(a, b float64) (float64, error) {

if b == 0 {

return 0, errors.New("division by zero")

}

return a / b, nil

}

4.命名返回值

函數(shù)可以聲明命名的返回值,在函數(shù)體內(nèi)可以直接使用這些名稱進(jìn)行賦值,最后不需要顯式使用 return 關(guān)鍵字。

func divide(a, b float64) (result float64, err error) {

if b == 0 {

err = errors.New("division by zero")

return

}

result = a / b

return

}

5.可變數(shù)量的參數(shù)

Go語言支持使用 ... 語法來表示可變數(shù)量的參數(shù)。這些參數(shù)在函數(shù)體內(nèi)作為切片使用。

func sum(numbers ...int) int {

total := 0

for _, num := range numbers {

total += num

}

return total

}

6.函數(shù)作為參數(shù)

在Go語言中,函數(shù)可以作為參數(shù)傳遞給其他函數(shù)。

func applyFunction(fn func(int, int) int, a, b int) int {

return fn(a, b)

}

func add(a, b int) int {

return a + b

}

func main() {

result := applyFunction(add, 3, 4)

fmt.Println(result) // Output: 7

}

7.匿名函數(shù)和閉包

Go語言支持匿名函數(shù),也稱為閉包。這些函數(shù)可以在其他函數(shù)內(nèi)部定義,并訪問外部函數(shù)的變量。

func main() {

x := 5

fn := func() {

fmt.Println(x) // 閉包訪問外部變量

}

fn() // Output: 5

}

8.defer語句

defer 語句用于延遲執(zhí)行函數(shù),通常用于在函數(shù)返回前執(zhí)行一些清理操作。

func main() {

defer fmt.Println("World")

fmt.Println("Hello")

}

以上是一些關(guān)于Go語言函數(shù)的基本知識點(diǎn)。函數(shù)在Go中扮演著非常重要的角色,用于組織代碼、實(shí)現(xiàn)功能模塊化和提高代碼的可維護(hù)性。

驗(yàn)證一:基本使用用例驗(yàn)證

在chapter4下新建basic,在創(chuàng)建func_basic_test.go練習(xí)

package basic

import (

"errors"

"fmt"

"testing"

)

// 普通函數(shù)

func greet(name string) {

fmt.Printf("Hello, %s!\n", name)

}

// 多返回值函數(shù)

func divide(a, b int) (int, error) {

if b == 0 {

return 0, errors.New("division by zero")

}

return a / b, nil

}

// 命名返回值函數(shù)

func divideNamed(a, b int) (result int, err error) {

if b == 0 {

err = errors.New("division by zero")

return

}

result = a / b

return

}

// 可變數(shù)量的參數(shù)函數(shù)

func sum(numbers ...int) int {

total := 0

for _, num := range numbers {

total += num

}

return total

}

// 函數(shù)作為參數(shù)

func applyFunction(fn func(int, int) int, a, b int) int {

return fn(a, b)

}

// 匿名函數(shù)和閉包

func closureExample() {

x := 5

fn := func() {

fmt.Println(x)

}

fn() // Output: 5

}

// defer語句

func deferExample() {

defer fmt.Println("World")

fmt.Println("Hello") // Output: Hello World

}

func TestBasic(t *testing.T) {

greet("Alice") // Output: Hello, Alice!

q, err := divide(10, 2)

if err != nil {

fmt.Println("Error:", err)

} else {

fmt.Println("Quotient:", q) // Output: Quotient: 5

}

qNamed, errNamed := divideNamed(10, 0)

if errNamed != nil {

fmt.Println("Error:", errNamed) // Output: Error: division by zero

} else {

fmt.Println("Quotient:", qNamed)

}

total := sum(1, 2, 3, 4, 5)

fmt.Println("Sum:", total) // Output: Sum: 15

addResult := applyFunction(func(a, b int) int {

return a + b

}, 3, 4)

fmt.Println("Addition:", addResult) // Output: Addition: 7

closureExample()

deferExample()

}

驗(yàn)證二:業(yè)務(wù)小舉例

在chapter4下新建biz,在創(chuàng)建func_biz_test.go練習(xí),假設(shè)你正在開發(fā)一個簡單的訂單處理系統(tǒng),需要計(jì)算訂單中商品的總價(jià)和應(yīng)用折扣。你可以使用函數(shù)來處理這些業(yè)務(wù)邏輯。以下是一個簡單的示例:

package biz

import (

"fmt"

"testing"

)

type Product struct {

Name string

Price float64

}

func calculateTotal(products []Product) float64 {

total := 0.0

for _, p := range products {

total += p.Price

}

return total

}

func applyDiscount(amount, discount float64) float64 {

return amount * (1 - discount)

}

func TestBiz(t *testing.T) {

products := []Product{

{Name: "Product A", Price: 10.0},

{Name: "Product B", Price: 20.0},

{Name: "Product C", Price: 30.0},

}

total := calculateTotal(products)

fmt.Printf("Total before discount: $%.2f\n", total)

discountedTotal := applyDiscount(total, 0.1)

fmt.Printf("Total after 10%% discount: $%.2f\n", discountedTotal)

}

(五)面向?qū)ο缶幊?/p>

src目錄下創(chuàng)建chapter5,Go語言支持面向?qū)ο缶幊蹋∣bject-Oriented Programming,OOP),盡管與一些傳統(tǒng)的面向?qū)ο缶幊陶Z言(如Java和C++)相比,Go的實(shí)現(xiàn)方式可能略有不同。在Go語言中,沒有類的概念,但可以通過結(jié)構(gòu)體和方法來實(shí)現(xiàn)面向?qū)ο蟮奶匦浴?/p>

1.結(jié)構(gòu)體定義

在Go語言中,結(jié)構(gòu)體是一種自定義的數(shù)據(jù)類型,用于組合不同類型的字段(成員變量)以創(chuàng)建一個新的數(shù)據(jù)類型。創(chuàng)建struct目錄,編寫struct_test.go,以下是結(jié)構(gòu)體的定義、使用和驗(yàn)證示例:

package _struct

import (

"fmt"

"testing"

)

// 定義一個結(jié)構(gòu)體

type Person struct {

FirstName string

LastName string

Age int

}

func TestStruct(t *testing.T) {

// 創(chuàng)建結(jié)構(gòu)體實(shí)例并初始化字段

person1 := Person{

FirstName: "Alice",

LastName: "Smith",

Age: 25,

}

// 訪問結(jié)構(gòu)體字段

fmt.Println("First Name:", person1.FirstName) // Output: First Name: Alice

fmt.Println("Last Name:", person1.LastName) // Output: Last Name: Smith

fmt.Println("Age:", person1.Age) // Output: Age: 25

// 修改結(jié)構(gòu)體字段的值

person1.Age = 26

fmt.Println("Updated Age:", person1.Age) // Output: Updated Age: 26

}

結(jié)構(gòu)體的定義可以包含多個字段,每個字段可以是不同的數(shù)據(jù)類型。你還可以在結(jié)構(gòu)體中嵌套其他結(jié)構(gòu)體,形成更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。編寫struct_cmpx_test.go示例:

package _struct

import (

"fmt"

"testing"

)

type Address struct {

Street string

City string

ZipCode string

}

type PersonNew struct {

FirstName string

LastName string

Age int

Address Address

}

func TestCmpxStruct(t *testing.T) {

person2 := PersonNew{

FirstName: "Bob",

LastName: "Johnson",

Age: 30,

Address: Address{

Street: "123 Main St",

City: "Cityville",

ZipCode: "12345",

},

}

fmt.Println("Full Name:", person2.FirstName, person2.LastName)

fmt.Println("Address:", person2.Address.Street, person2.Address.City, person2.Address.ZipCode)

}

2.實(shí)例創(chuàng)建及初始化

在Go語言中,可以通過多種方式來創(chuàng)建和初始化結(jié)構(gòu)體實(shí)例。創(chuàng)建creatinit目錄,以下是幾種常見的實(shí)例創(chuàng)建和初始化方法,具體代碼為creatinit_test.go

字面量初始化:可以使用花括號 {} 來初始化結(jié)構(gòu)體實(shí)例的字段。部分字段初始化: 如果你只想初始化結(jié)構(gòu)體的部分字段,可以省略其他字段。使用字段名初始化: 可以根據(jù)字段名來指定字段的值,無需按順序初始化。默認(rèn)值初始化: 結(jié)構(gòu)體的字段可以根據(jù)其類型的默認(rèn)值進(jìn)行初始化。使用 new 函數(shù): 可以使用 new 函數(shù)來創(chuàng)建一個指向結(jié)構(gòu)體的指針,并返回其指針。字段順序初始化: 可以選擇性地省略字段名,但是這時(shí)候需要按照結(jié)構(gòu)體字段的順序進(jìn)行賦值。

package creatinit

import (

"fmt"

"testing"

)

type Person struct {

FirstName string

LastName string

Age int

}

/**

* @author zhangyanfeng

* @description 字面量初始化

* @date 2023/8/26 15:09

**/

func TestCreateObj1(t *testing.T) {

person1 := Person{

FirstName: "Alice",

LastName: "Smith",

Age: 25,

}

fmt.Println(person1.FirstName, person1.LastName, person1.Age) // Output: Alice Smith 25

}

/**

* @author zhangyanfeng

* @description 部分字段初始化

* @date 2023/8/26 15:10

**/

func TestCreateObj2(t *testing.T) {

person2 := Person{

FirstName: "Bob",

Age: 30,

}

fmt.Println(person2.FirstName, person2.LastName, person2.Age) // Output: Bob 30

}

/**

* @author zhangyanfeng

* @description 使用字段名初始化

* @date 2023/8/26 15:12

**/

func TestCreateObj3(t *testing.T) {

person3 := Person{

LastName: "Johnson",

FirstName: "Chris",

Age: 28,

}

fmt.Println(person3.FirstName, person3.LastName, person3.Age) // Output: Chris Johnson 28

}

/**

* @author zhangyanfeng

* @description 默認(rèn)值初始化

* @date 2023/8/26 15:13

**/

func TestCreateObj4(t *testing.T) {

var person4 Person

fmt.Println(person4.FirstName, person4.LastName, person4.Age) // Output: 0

}

/**

* @author zhangyanfeng

* @description 使用 new 函數(shù)

* @date 2023/8/26 15:14

**/

func TestCreateObj5(t *testing.T) {

person5 := new(Person)

person5.FirstName = "David"

person5.Age = 22

fmt.Println(person5.FirstName, person5.LastName, person5.Age) // Output: David 22

}

/**

* @author zhangyanfeng

* @description 字段順序初始化

* @date 2023/8/26 15:24

**/

func TestCreateObj6(t *testing.T) {

// 使用字段順序初始化

person := Person{"Alice", "Smith", 25}

fmt.Println(person.FirstName, person.LastName, person.Age) // Output: Alice Smith 25

}

3.行為(方法)定義

在Go語言中,方法是與特定類型相關(guān)聯(lián)的函數(shù),它可以在這個類型的實(shí)例上調(diào)用。方法使得類型的操作能夠與該類型的定義放在一起,提高了代碼的可讀性和可維護(hù)性。

創(chuàng)建method目錄進(jìn)行代碼練習(xí),以下是關(guān)于Go語言方法的定義、使用和分析:

方法的定義

在Go語言中,方法是通過為函數(shù)添加接收者(receiver)來定義的。接收者是一個普通的參數(shù),但它在方法名前放置,用于指定該方法與哪種類型相關(guān)聯(lián)。創(chuàng)建method_define_test.go

package method

import (

"fmt"

"testing"

)

type Circle struct {

Radius float64

}

// 定義 Circle 類型的方法

func (c Circle) Area() float64 {

return 3.14159 * c.Radius * c.Radius

}

func TestMethodDef(t *testing.T) {

c := Circle{Radius: 5}

area := c.Area()

fmt.Printf("Circle area: %.2f\n", area) // Output: Circle area: 78.54

}

在上述示例中,我們定義了一個 Circle 結(jié)構(gòu)體,然后為其定義了一個名為 Area 的方法。這個方法可以通過 c.Area() 的方式調(diào)用,其中 c 是一個 Circle 類型的實(shí)例。

方法的調(diào)用

方法調(diào)用的語法為 實(shí)例.方法名(),即通過實(shí)例來調(diào)用方法。創(chuàng)建method_rpc_test.go

package method

import (

"fmt"

"testing"

)

type Rectangle struct {

Width float64

Height float64

}

func (r Rectangle) Area() float64 {

return r.Width * r.Height

}

func TestMethonRpc(t *testing.T) {

rect := Rectangle{Width: 3, Height: 4}

area := rect.Area()

fmt.Printf("Rectangle area: %.2f\n", area) // Output: Rectangle area: 12.00

}

指針接收者

Go語言支持使用指針作為方法的接收者,這樣可以修改接收者實(shí)例的字段值。創(chuàng)建method_rec_test.go

package method

import (

"fmt"

"testing"

)

type Counter struct {

Count int

}

func (c *Counter) Increment() {

c.Count++

}

func TestMethonRec(t *testing.T) {

counter := Counter{Count: 0}

counter.Increment()

fmt.Println("Count:", counter.Count) // Output: Count: 1

}

在上述示例中,Increment 方法使用了指針接收者,這樣調(diào)用方法后,Count 字段的值會被修改。

方法與函數(shù)的區(qū)別

方法與函數(shù)的主要區(qū)別在于方法是特定類型的函數(shù),它與類型的關(guān)系更加緊密,可以訪問類型的字段和其他方法。函數(shù)則是獨(dú)立于特定類型的代碼塊。方法通常用于實(shí)現(xiàn)特定類型的行為,而函數(shù)可以用于通用的操作。

通過定義方法,你可以使類型的操作更加自然和一致,提高代碼的可讀性和模塊化。

這里可以說明一下,method_rpc_test.go中,我們?yōu)?Rectangle 結(jié)構(gòu)體定義了一個名為 Area 的方法,該方法可以通過 rect.Area() 的方式調(diào)用。方法直接與類型 Rectangle 關(guān)聯(lián),可以訪問 Rectangle 的字段(Width 和 Height)。

我們?yōu)榱伺c方法作對比,在對應(yīng)方法體中創(chuàng)建一個方法如下

// 定義一個函數(shù)來計(jì)算矩形的面積

func CalculateArea(r Rectangle) float64 {

return r.Width * r.Height

}

在這個示例中,我們定義了一個名為 CalculateArea 的函數(shù),它接受一個 Rectangle 類型的參數(shù)來計(jì)算矩形的面積。函數(shù)是獨(dú)立于 Rectangle 類型的,因此它無法直接訪問 Rectangle 的字段。

總結(jié): 方法與函數(shù)的區(qū)別在于方法是特定類型的函數(shù),與類型的關(guān)系更加緊密,可以訪問類型的字段和其他方法。而函數(shù)是獨(dú)立于特定類型的代碼塊,通常用于通用的操作。在上述示例中,方法與矩形相關(guān)聯(lián),可以直接訪問矩形的字段;函數(shù)則是一個獨(dú)立的計(jì)算過程,不與任何特定類型直接關(guān)聯(lián)。

通過使用方法,我們可以使代碼更加自然和一致,提高代碼的可讀性和模塊化,特別是在實(shí)現(xiàn)特定類型的行為時(shí)。

4.接口定義使用

在Go語言中,接口是一種定義方法集合的方式,它規(guī)定了一組方法的簽名,而不涉及實(shí)現(xiàn)細(xì)節(jié)。通過接口,可以實(shí)現(xiàn)多態(tài)性和代碼解耦,使不同類型的對象能夠按照一致的方式進(jìn)行操作。

創(chuàng)建interface目錄用于后續(xù)練習(xí),以下是關(guān)于Go語言接口的講解:

定義接口

接口是一組方法的集合,通過 type 關(guān)鍵字定義。接口定義了一組方法簽名,但不包含方法的實(shí)現(xiàn)。創(chuàng)建interface_test.go進(jìn)行代碼練習(xí)

package interface_test

import (

"fmt"

"testing"

)

// 定義一個簡單的接口

type Shape interface {

Area() float64

}

// 定義兩個實(shí)現(xiàn) Shape 接口的結(jié)構(gòu)體

type Circle struct {

Radius float64

}

func (c Circle) Area() float64 {

return 3.14159 * c.Radius * c.Radius

}

type Rectangle struct {

Width float64

Height float64

}

func (r Rectangle) Area() float64 {

return r.Width * r.Height

}

func TestInterface(t *testing.T) {

shapes := []Shape{

Circle{Radius: 2},

Rectangle{Width: 3, Height: 4},

}

for _, shape := range shapes {

fmt.Printf("Area of %T: %.2f\n", shape, shape.Area())

}

}

在上面的示例中,我們定義了一個名為 Shape 的接口,該接口要求實(shí)現(xiàn)一個 Area 方法,用于計(jì)算圖形的面積。然后,我們定義了兩個結(jié)構(gòu)體 Circle 和 Rectangle,并分別實(shí)現(xiàn)了 Area 方法。通過使用接口,我們可以將不同類型的圖形對象放入同一個切片中,然后通過循環(huán)調(diào)用它們的 Area 方法。

接口的實(shí)現(xiàn)

任何類型只要實(shí)現(xiàn)了接口中定義的所有方法,就被認(rèn)為是實(shí)現(xiàn)了該接口。接口的實(shí)現(xiàn)是隱式的,不需要顯式聲明。只要方法的簽名和接口中的方法簽名相同,類型就被視為實(shí)現(xiàn)了接口。

接口的多態(tài)性

由于接口的多態(tài)性,我們可以將實(shí)現(xiàn)了接口的對象視為接口本身。在上面的示例中,shapes 切片中存儲了不同類型的對象,但它們都實(shí)現(xiàn)了 Shape 接口,因此可以通過統(tǒng)一的方式調(diào)用 Area 方法。

通過使用接口,可以實(shí)現(xiàn)代碼的抽象和解耦,使得代碼更加靈活和可擴(kuò)展。接口在Go語言中被廣泛應(yīng)用,用于定義通用的行為和約束。

5.擴(kuò)展和復(fù)用

在Go語言中,擴(kuò)展和復(fù)用代碼的方式與傳統(tǒng)的面向?qū)ο笳Z言(如Java)有所不同。Go鼓勵使用組合、接口和匿名字段等特性來實(shí)現(xiàn)代碼的擴(kuò)展和復(fù)用,而不是通過類繼承。

創(chuàng)建extend目錄用于后續(xù)練習(xí),以下是關(guān)于Go語言中擴(kuò)展和復(fù)用的詳細(xì)講解:

組合和嵌套

Go語言中的組合(composition)允許你將一個結(jié)構(gòu)體類型嵌套在另一個結(jié)構(gòu)體類型中,從而實(shí)現(xiàn)代碼的復(fù)用。嵌套的結(jié)構(gòu)體可以通過字段名直接訪問其成員。創(chuàng)建composition_test.go

package extend

import (

"fmt"

"testing"

)

type Engine struct {

Model string

}

type Car struct {

Engine

Brand string

}

func TestComposition(t *testing.T) {

car := Car{

Engine: Engine{Model: "V6"},

Brand: "Toyota",

}

fmt.Println("Car brand:", car.Brand)

fmt.Println("Car engine model:", car.Model) // 直接訪問嵌套結(jié)構(gòu)體的字段

}

在這個示例中,我們使用了組合來創(chuàng)建 Car 結(jié)構(gòu)體,其中嵌套了 Engine 結(jié)構(gòu)體。通過嵌套,Car 結(jié)構(gòu)體可以直接訪問 Engine 結(jié)構(gòu)體的字段。

接口實(shí)現(xiàn)

通過接口,可以定義一組方法,然后不同的類型可以實(shí)現(xiàn)這些方法。這樣可以實(shí)現(xiàn)多態(tài)性和代碼解耦,使得不同類型的對象可以通過相同的接口進(jìn)行操作。創(chuàng)建interface_ext_test.go

package extend

import (

"fmt"

"math"

"testing"

)

// 定義 Shape 接口

type Shape interface {

Area() float64

Perimeter() float64

}

// 定義 Circle 結(jié)構(gòu)體

type Circle struct {

Radius float64

}

// 實(shí)現(xiàn) Circle 結(jié)構(gòu)體的方法,以滿足 Shape 接口

func (c Circle) Area() float64 {

return math.Pi * c.Radius * c.Radius

}

func (c Circle) Perimeter() float64 {

return 2 * math.Pi * c.Radius

}

// 定義 Rectangle 結(jié)構(gòu)體

type Rectangle struct {

Width float64

Height float64

}

// 實(shí)現(xiàn) Rectangle 結(jié)構(gòu)體的方法,以滿足 Shape 接口

func (r Rectangle) Area() float64 {

return r.Width * r.Height

}

func (r Rectangle) Perimeter() float64 {

return 2 * (r.Width + r.Height)

}

func TestInterfaceExt(t *testing.T) {

circle := Circle{Radius: 3}

rectangle := Rectangle{Width: 4, Height: 5}

shapes := []Shape{circle, rectangle}

for _, shape := range shapes {

fmt.Printf("Shape Type: %T\n", shape)

fmt.Printf("Area: %.2f\n", shape.Area())

fmt.Printf("Perimeter: %.2f\n", shape.Perimeter())

fmt.Println("------------")

}

}

在上述示例中,我們定義了一個名為 Shape 的接口,它有兩個方法 Area() 和 Perimeter(),分別用于計(jì)算形狀的面積和周長。然后,我們分別實(shí)現(xiàn)了 Circle 和 Rectangle 結(jié)構(gòu)體的這兩個方法,使它們滿足了 Shape 接口。

通過將不同類型的形狀實(shí)例放入一個 []Shape 切片中,我們可以使用統(tǒng)一的方式調(diào)用 Area() 和 Perimeter() 方法,實(shí)現(xiàn)了代碼的多態(tài)性和解耦。這樣,無論我們后續(xù)添加新的形狀,只要它們實(shí)現(xiàn)了 Shape 接口的方法,就可以無縫地集成到計(jì)算器中。

匿名字段和方法重用

通過使用匿名字段,一個結(jié)構(gòu)體可以繼承另一個結(jié)構(gòu)體的字段和方法。創(chuàng)建other_ext_test.go

package extend

import (

"fmt"

"testing"

)

type Animal struct {

Name string

}

func (a Animal) Speak() {

fmt.Println("Animal speaks")

}

type Dog struct {

Animal

Breed string

}

func TestOtherExt(t *testing.T) {

dog := Dog{

Animal: Animal{Name: "Buddy"},

Breed: "Golden Retriever",

}

fmt.Println("Dog name:", dog.Name)

dog.Speak() // 繼承了 Animal 的 Speak 方法

}

在上述示例中,Dog 結(jié)構(gòu)體嵌套了 Animal 結(jié)構(gòu)體,從而繼承了 Animal 的字段和方法。

通過這些方式,你可以在Go語言中實(shí)現(xiàn)代碼的擴(kuò)展和復(fù)用。盡管Go不像傳統(tǒng)的面向?qū)ο笳Z言那樣強(qiáng)調(diào)類繼承,但通過組合、接口和匿名字段等特性,你仍然可以實(shí)現(xiàn)類似的效果,使代碼更靈活、可讀性更高,并保持低耦合性。

6.空接口和斷言

空接口和斷言是Go語言中用于處理不確定類型和類型轉(zhuǎn)換的重要概念。

創(chuàng)建emptyassert目錄用于后續(xù)練習(xí),下面是關(guān)于空接口和斷言的學(xué)習(xí)總結(jié):

空接口(Empty Interface)

空接口是Go語言中最基礎(chǔ)的接口,它不包含任何方法聲明。因此,空接口可以用來表示任何類型的值??战涌诘穆暶鞣绞綖?interface{}。

空接口的主要用途是在需要處理不確定類型的場景中。通過使用空接口,可以接受和存儲任何類型的值,類似于其他編程語言中的動態(tài)類型。但需要注意的是,使用空接口可能會導(dǎo)致類型安全性降低,因?yàn)榫幾g時(shí)無法檢查具體類型。

斷言(Type Assertion)

斷言是一種在空接口中恢復(fù)具體類型的機(jī)制,它允許我們在運(yùn)行時(shí)檢查空接口中的值的實(shí)際類型,并將其轉(zhuǎn)換為相應(yīng)的類型。斷言的語法為 value.(Type),其中 value 是接口值,Type 是要斷言的具體類型。

創(chuàng)建emptyassert_test.go進(jìn)行驗(yàn)證:

package emptyassert

import (

"fmt"

"testing"

)

func DoSomething(p interface{}) {

switch v := p.(type) {

case int:

fmt.Println("Integer", v)

case string:

fmt.Println("String", v)

default:

fmt.Println("Unknow Type")

}

}

func TestEmptyInterfaceAssertion(t *testing.T) {

DoSomething(10)

DoSomething("10")

}

func TestEmptyAssert(t *testing.T) {

var x interface{} = "hello"

str, ok := x.(string)

if ok {

fmt.Println("String:", str)

} else {

fmt.Println("Not a string")

}

}

下面逐個解釋每個測試函數(shù)的內(nèi)容:

func DoSomething(p interface{}) { ... }:定義了一個函數(shù) DoSomething,該函數(shù)接受一個空接口參數(shù) p,然后根據(jù)接口值的實(shí)際類型進(jìn)行類型斷言,根據(jù)不同的類型輸出不同的信息。 func TestEmptyInterfaceAssertion(t *testing.T) { ... }:測試空接口的斷言操作。 調(diào)用 DoSomething(10),將整數(shù) 10 傳遞給函數(shù),函數(shù)根據(jù)類型斷言輸出整數(shù)類型信息。調(diào)用 DoSomething("10"),將字符串 "10" 傳遞給函數(shù),函數(shù)根據(jù)類型斷言輸出字符串類型信息。 func TestEmptyAssert(t *testing.T) { ... }:測試空接口的類型斷言操作。 聲明一個空接口變量 x,并將字符串 "hello" 賦值給它。使用類型斷言 x.(string) 判斷 x 是否為字符串類型,如果是,將其賦值給變量 str,并輸出字符串值;否則輸出 "Not a string"。

這些測試函數(shù)展示了Go語言中空接口的斷言操作,通過類型斷言可以判斷空接口中的具體類型,并執(zhí)行相應(yīng)的操作。

總結(jié): 空接口和斷言是Go語言中處理不確定類型和類型轉(zhuǎn)換的強(qiáng)大工具??战涌谠试S存儲任何類型的值,而斷言允許我們在運(yùn)行時(shí)檢查和轉(zhuǎn)換接口值的實(shí)際類型。使用這些機(jī)制,可以在需要處理不同類型的值時(shí)實(shí)現(xiàn)更靈活和通用的代碼。但在使用空接口和斷言時(shí),要注意維護(hù)類型安全性,并進(jìn)行適當(dāng)?shù)腻e誤處理。

7.GO 接口最佳實(shí)踐

在Go語言中,使用接口的最佳實(shí)踐可以提高代碼的可讀性、可維護(hù)性和靈活性。

小接口與大接口: 盡量設(shè)計(jì)小接口,一個接口應(yīng)該只包含少量的方法,而不是設(shè)計(jì)一個大而全的接口。這樣可以避免實(shí)現(xiàn)接口時(shí)不必要的負(fù)擔(dān),并使接口更具通用性。基于使用場景設(shè)計(jì)接口: 設(shè)計(jì)接口時(shí)應(yīng)該考慮使用場景,而不是從具體的實(shí)現(xiàn)出發(fā)。思考在你的應(yīng)用程序中如何使用接口,以及接口應(yīng)該提供哪些方法來滿足這些使用場景。使用合適的命名: 為接口和方法使用清晰的命名,使其能夠表達(dá)出其用途和功能。命名應(yīng)該具有可讀性和表達(dá)性,讓其他開發(fā)者能夠輕松理解接口的用途。避免不必要的接口: 不要為每個類型都創(chuàng)建一個接口,只有在多個類型之間確實(shí)存在共享的行為和功能時(shí)才使用接口。不要過度使用接口,以免導(dǎo)致不必要的復(fù)雜性。使用接口作為函數(shù)參數(shù)和返回值: 使用接口作為函數(shù)參數(shù)和返回值,可以使函數(shù)更加通用,允許傳入不同類型的參數(shù),并返回不同類型的結(jié)果。這可以提高代碼的復(fù)用性和擴(kuò)展性。注釋和文檔: 為接口提供清晰的文檔和注釋,說明接口的用途、方法的功能和預(yù)期行為。這可以幫助其他開發(fā)者更好地理解接口的使用方式。用例驅(qū)動設(shè)計(jì): 在設(shè)計(jì)接口時(shí),可以從使用的角度出發(fā),先考慮接口在實(shí)際場景中如何被調(diào)用,然后再設(shè)計(jì)接口的方法和簽名。將接口的實(shí)現(xiàn)與定義分離: 將接口的實(shí)現(xiàn)與接口的定義分開,這樣可以使實(shí)現(xiàn)更靈活,可以在不修改接口定義的情況下實(shí)現(xiàn)新的類型。默認(rèn)實(shí)現(xiàn): 在接口定義中,可以為某些方法提供默認(rèn)實(shí)現(xiàn),從而減少實(shí)現(xiàn)接口時(shí)的工作量。這對于可選方法或者某些方法的默認(rèn)行為很有用。使用空接口謹(jǐn)慎: 使用空接口(interface{})應(yīng)謹(jǐn)慎,因?yàn)樗鼤档皖愋桶踩?。只有在確實(shí)需要處理不同類型的值時(shí)才使用空接口,同時(shí)要注意類型斷言和錯誤處理。

設(shè)計(jì)和使用接口時(shí)要根據(jù)實(shí)際需求和項(xiàng)目的特點(diǎn)來選擇合適的方案。

(六)編寫好錯誤機(jī)制

src目錄下創(chuàng)建chapter6,Go語言中的錯誤處理機(jī)制是通過返回錯誤值來實(shí)現(xiàn)的,而不是使用異常。這種錯誤處理機(jī)制非常清晰、可控,使得開發(fā)者能夠精確地處理各種錯誤情況。

1.基本使用介紹

創(chuàng)建basic目錄,編寫basic_error_test.go

錯誤類型

在Go中,錯誤被表示為一個實(shí)現(xiàn)了 error 接口的類型。error 接口只有一個方法,即 Error() string,它返回一個描述錯誤的字符串。

type error interface {

Error() string

}

返回錯誤值

當(dāng)一個函數(shù)遇到錯誤情況時(shí),通常會返回一個錯誤值。這個錯誤值可以是一個實(shí)現(xiàn)了 error 接口的自定義類型,也可以是Go標(biāo)準(zhǔn)庫中預(yù)定義的錯誤類型,如 errors.New() 創(chuàng)建的錯誤。

錯誤檢查

調(diào)用者通常需要顯式地檢查函數(shù)返回的錯誤,以判斷是否發(fā)生了錯誤。這可以通過在調(diào)用函數(shù)后使用 if 語句來實(shí)現(xiàn)。

以上兩個直接寫代碼如下:

package basic

import (

"errors"

"fmt"

"testing"

)

var LessThanTwoError = errors.New("n should be not less than 2")

var LargerThenHundredError = errors.New("n should be not larger than 100")

func GetFibonacci(n int) ([]int, error) {

if n < 2 {

return nil, LessThanTwoError

}

if n > 100 {

return nil, LargerThenHundredError

}

fibList := []int{1, 1}

for i := 2; /*短變量聲明 := */ i < n; i++ {

fibList = append(fibList, fibList[i-2]+fibList[i-1])

}

return fibList, nil

}

func TestGetFibonacci(t *testing.T) {

if v, err := GetFibonacci(1); err != nil {

if err == LessThanTwoError {

fmt.Println("It is less.")

}

t.Error(err)

} else {

t.Log(v)

}

}

2.錯誤鏈

創(chuàng)建chain目錄,編寫error_chain_test.go

在某些情況下,錯誤可以包含附加信息,以便更好地理解錯誤的原因??梢酝ㄟ^ fmt.Errorf() 函數(shù)來創(chuàng)建包含附加信息的錯誤。

假設(shè)我們正在構(gòu)建一個文件操作的庫,其中包含文件讀取和寫入功能。有時(shí),在文件讀取或?qū)懭脒^程中可能會出現(xiàn)各種錯誤,例如文件不存在、權(quán)限問題等。我們希望能夠提供有關(guān)錯誤的更多上下文信息。

package chain

import (

"errors"

"fmt"

"testing"

)

// 自定義文件操作錯誤類型

type FileError struct {

Op string // 操作類型("read" 或 "write")

Path string // 文件路徑

Err error // 原始錯誤

}

// 實(shí)現(xiàn) error 接口的 Error() 方法

func (e *FileError) Error() string {

return fmt.Sprintf("%s %s: %v", e.Op, e.Path, e.Err)

}

// 模擬文件讀取操作

func ReadFile(path string) ([]byte, error) {

// 模擬文件不存在的情況

return nil, &FileError{Op: "read", Path: path, Err: errors.New("file not found")}

}

func TestChain(t *testing.T) {

filePath := "/path/to/nonexistent/file.txt"

_, err := ReadFile(filePath)

if err != nil {

fmt.Println("Error:", err)

// 在這里,我們可以檢查錯誤類型,提取上下文信息

if fileErr, ok := err.(*FileError); ok {

fmt.Printf("Operation: %s\n", fileErr.Op)

fmt.Printf("File Path: %s\n", fileErr.Path)

fmt.Printf("Original Error: %v\n", fileErr.Err)

}

}

}

下面是代碼的解釋:

FileError 結(jié)構(gòu)體:定義了一個自定義錯誤類型 FileError,包含以下字段: Op:操作類型,表示是讀取("read")還是寫入("write")操作;Path:文件路徑,表示涉及哪個文件;Err:原始錯誤,包含底層的錯誤信息。 Error() 方法:為 FileError 結(jié)構(gòu)體實(shí)現(xiàn)了 error 接口的 Error() 方法,用于生成錯誤的文本描述。 ReadFile() 函數(shù):模擬文件讀取操作。在這個示例中,該函數(shù)返回一個 FileError 類型的錯誤,模擬了文件不存在的情況。 TestChain() 測試函數(shù):演示如何在錯誤處理中使用自定義錯誤類型。 定義了一個文件路徑 filePath,并調(diào)用 ReadFile(filePath) 函數(shù)來模擬文件讀取操作;檢查錯誤,如果發(fā)生錯誤,輸出錯誤信息;在錯誤處理中,通過類型斷言檢查錯誤是否為 *FileError 類型,如果是,則可以提取更多上下文信息,如操作類型、文件路徑和原始錯誤信息。

3.Panic 和 Recover

在Go語言中,panic 和 recover 是用于處理異常情況的機(jī)制,但它們應(yīng)該謹(jǐn)慎使用,僅用于特定的情況,而不是替代正常的錯誤處理機(jī)制。以下是對 panic 和 recover 的詳細(xì)解釋,并給出一個具體用例:

panic

創(chuàng)建panic目錄,編寫panic_test.go。panic 是一個內(nèi)置函數(shù),用于引發(fā)運(yùn)行時(shí)恐慌。當(dāng)程序遇到無法繼續(xù)執(zhí)行的致命錯誤時(shí),可以使用 panic 來中斷程序的正常流程。但應(yīng)該避免濫用 panic,因?yàn)樗鼤?dǎo)致程序崩潰,不會提供友好的錯誤信息。典型情況下,panic 用于表示程序中的不可恢復(fù)錯誤,例如切片索引越界。

package panic

import (

"fmt"

"testing"

)

func TestPanic(t *testing.T) {

arr := []int{1, 2, 3}

index := 4

if index >= len(arr) {

panic("Index out of range")

}

element := arr[index]

fmt.Println("Element:", element)

}

在上述示例中,如果索引 index 超出了切片 arr 的范圍,會觸發(fā) panic,導(dǎo)致程序崩潰。這種情況下,panic 用于表示程序的不可恢復(fù)錯誤。

recover

創(chuàng)建recover目錄,編寫recover_test.go。recover 也是一個內(nèi)置函數(shù),用于恢復(fù) panic 引發(fā)的運(yùn)行時(shí)恐慌。它只能在延遲函數(shù)(defer)內(nèi)部使用,并且用于恢復(fù)程序的控制流,而不是用于處理錯誤。通常,在發(fā)生 panic 后,recover 可以在延遲函數(shù)中捕獲 panic,并執(zhí)行一些清理工作,然后程序會繼續(xù)執(zhí)行。

package recover

import (

"fmt"

"testing"

)

func cleanup() {

if r := recover(); r != nil {

fmt.Println("Recovered from panic:", r)

}

}

func TestRecover(t *testing.T) {

defer cleanup()

panic("Something went wrong")

fmt.Println("This line will not be executed")

}

在上述示例中,panic 觸發(fā)后,cleanup 函數(shù)中的 recover 捕獲了 panic,并打印了錯誤消息。然后程序會繼續(xù)執(zhí)行,但需要注意的是,控制流不會回到觸發(fā) panic 的地方,因此 fmt.Println 不會被執(zhí)行。

總之,panic 和 recover 應(yīng)該謹(jǐn)慎使用,只用于特殊情況,如不可恢復(fù)的錯誤或在延遲函數(shù)中進(jìn)行清理操作。在大多數(shù)情況下,應(yīng)該優(yōu)先使用錯誤返回值來處理異常情況,因?yàn)檫@種方式更安全、可控,能夠提供更好的錯誤信息和錯誤處理。只有在特定的情況下,例如遇到不可恢復(fù)的錯誤時(shí),才應(yīng)該考慮使用 panic 和 recover。

4.自定義錯誤類型

創(chuàng)建define目錄,編寫error_define_test.go。

在Go中,你可以根據(jù)需要定義自己的錯誤類型,只需滿足 error 接口的要求即可。這允許你創(chuàng)建更具描述性和上下文的錯誤類型。

在Go中,自定義錯誤類型是一種強(qiáng)大的方式,可以創(chuàng)建更具描述性和上下文的錯誤,以提供更好的錯誤信息。自定義錯誤類型必須滿足 error 接口的要求,即實(shí)現(xiàn) Error() string 方法。以下是一個示例,展示如何自定義錯誤類型和驗(yàn)證其用例:

package define

import (

"fmt"

"testing"

"time"

)

// 自定義錯誤類型

type TimeoutError struct {

Operation string // 操作名稱

Timeout time.Time // 超時(shí)時(shí)間

}

// 實(shí)現(xiàn) error 接口的 Error() 方法

func (e TimeoutError) Error() string {

return fmt.Sprintf("Timeout error during %s operation. Timeout at %s", e.Operation, e.Timeout.Format("2006-01-02 15:04:05"))

}

// 模擬執(zhí)行某個操作,可能會超時(shí)

func PerformOperation() error {

// 模擬操作超時(shí)

timeout := time.Now().Add(5 * time.Second)

if time.Now().After(timeout) {

return TimeoutError{Operation: "PerformOperation", Timeout: timeout}

}

// 模擬操作成功

return nil

}

func TestDefineError(t *testing.T) {

err := PerformOperation()

if err != nil {

// 檢查錯誤類型并打印錯誤信息

if timeoutErr, ok := err.(TimeoutError); ok {

fmt.Println("Error Type:", timeoutErr.Operation)

fmt.Println("Timeout At:", timeoutErr.Timeout)

}

fmt.Println("Error:", err)

} else {

fmt.Println("Operation completed successfully.")

}

}

下面是代碼的解釋:

TimeoutError 結(jié)構(gòu)體:定義了一個自定義錯誤類型 TimeoutError,包含以下字段: Operation:操作名稱,表示哪個操作超時(shí);Timeout:超時(shí)時(shí)間,表示操作發(fā)生超時(shí)的時(shí)間點(diǎn)。 Error() 方法:為 TimeoutError 結(jié)構(gòu)體實(shí)現(xiàn)了 error 接口的 Error() 方法,用于生成錯誤的文本描述。 PerformOperation() 函數(shù):模擬執(zhí)行某個操作,可能會超時(shí)。在這個示例中,如果當(dāng)前時(shí)間超過了超時(shí)時(shí)間,則返回一個 TimeoutError 類型的錯誤。 TestDefineError() 測試函數(shù):演示如何在錯誤處理中使用自定義錯誤類型。 調(diào)用 PerformOperation() 函數(shù)來模擬操作,并檢查是否發(fā)生了錯誤;如果發(fā)生錯誤,首先檢查錯誤類型是否為 TimeoutError,如果是,則提取超時(shí)操作和超時(shí)時(shí)間,并輸出相關(guān)信息;最后,無論是否發(fā)生錯誤,都會輸出錯誤信息或成功完成的消息。

這個示例展示了如何自定義錯誤類型以及如何在錯誤處理中利用這些自定義錯誤類型來提供更多的上下文信息,使錯誤處理更加有信息和靈活。在這里,TimeoutError 提供了有關(guān)超時(shí)操作和超時(shí)時(shí)間的額外信息。

(七)包和依賴管理

src目錄下創(chuàng)建chapter7,Go 語言的包和依賴管理主要通過其內(nèi)置的模塊系統(tǒng)(Go Modules)來實(shí)現(xiàn)。Go Modules 于 Go 1.11 版本首次引入,并在 Go 1.13 版本中成為默認(rèn)的依賴管理方式。

1.package(包)的基本知識點(diǎn)

基本復(fù)用模塊單元

在 Go 語言中,package 是代碼復(fù)用的基本單元。一個 package 可以包含多個 Go 源文件,這些文件可以共享同一個包中的代碼,并通過包的導(dǎo)入機(jī)制被其他包使用。

包的可見性:在 Go 語言中,通過首字母大寫來表明一個標(biāo)識符(如變量、函數(shù)、類型等)可以被包外的代碼訪問。反之,首字母小寫的標(biāo)識符只能在包內(nèi)使用。

// mypackage.go

package mypackage

// 公有函數(shù),其他包可以訪問

func PublicFunction() {

// 實(shí)現(xiàn)細(xì)節(jié)

}

// 私有函數(shù),僅在當(dāng)前包內(nèi)可訪問

func privateFunction() {

// 實(shí)現(xiàn)細(xì)節(jié)

}

代碼的 package 可以和所在的目錄不一致

Go 語言的文件組織結(jié)構(gòu)鼓勵但不強(qiáng)制 package 名稱與其所在目錄名稱一致。通常情況下,開發(fā)者會遵循這種約定以保持代碼的一致性和可讀性,但 Go 并不強(qiáng)制執(zhí)行這一規(guī)則。

實(shí)際應(yīng)用:你可以在 chapter7 目錄下創(chuàng)建多個文件,并在這些文件中定義相同的包名 mypackage,也可以選擇一個不同于目錄名的包名。

// src目錄下的代碼

// src/chapter7/utility.go

package utility // 包名與所在目錄名不同

func UtilityFunction() {

// 實(shí)現(xiàn)細(xì)節(jié)

}

同一目錄里的 Go 代碼的 package 要保持一致

在同一目錄中的所有 Go 文件必須聲明相同的 package 名稱。這是 Go 語言的一個基本規(guī)則,確保同一目錄下的所有文件都屬于同一個包,從而能夠互相訪問這些文件中聲明的標(biāo)識符。

違例情況:如果你在同一目錄下使用不同的 package 名稱,Go 編譯器將會報(bào)錯,提示包聲明不一致。這個在上面的案例中也可以直接看到。

2.構(gòu)建一個自身可復(fù)用的package

src目錄下創(chuàng)建chapter7后,再次新建series,編寫my_series.go如下:

package series

import "fmt"

func init() {

fmt.Println("init1")

}

func init() {

fmt.Println("init2")

}

func Square(n int) int {

return n * n

}

func GetFibonacciSerie(n int) []int {

ret := []int{1, 1}

for i := 2; i < n; i++ {

ret = append(ret, ret[i-2]+ret[i-1])

}

return ret

}

然后在chapter7中新建client,編寫package_test.go將上面的內(nèi)容引入:

package client

import (

"go-learning/src/chapter7/series"

"testing"

)

func TestPackage(t *testing.T) {

t.Log(series.GetFibonacciSerie(5))

t.Log(series.Square(5))

}

通過在 chapter7 目錄下創(chuàng)建一個名為 series 的包,把與數(shù)學(xué)相關(guān)的函數(shù)(如求平方和斐波那契數(shù)列)集中在一起。這樣在其他地方需要使用這些功能時(shí),只需引入這個包即可,不必重復(fù)編寫相同的代碼。

知識點(diǎn):包的初始化

利用 Go 語言中的 init() 函數(shù)機(jī)制進(jìn)行包的初始化操作。在 Go 中,每個包可以有多個 init() 函數(shù),這些函數(shù)會在包第一次被加載時(shí)自動執(zhí)行,且執(zhí)行順序按照代碼順序。在 series 包中編寫了兩個 init() 函數(shù),它們會在包被引入時(shí)自動執(zhí)行。這種機(jī)制可以用于在包加載時(shí)執(zhí)行一些必要的初始化工作(如設(shè)置默認(rèn)值、加載配置等),或者用來調(diào)試包的加載過程。

3.導(dǎo)入和應(yīng)用遠(yuǎn)程依賴(即外部包)

獲取和更新遠(yuǎn)程依賴

使用 go get 命令來下載并添加遠(yuǎn)程依賴到項(xiàng)目中。Go Modules 會自動管理這些依賴,并更新 go.mod 和 go.sum 文件。如果需要強(qiáng)制從網(wǎng)絡(luò)獲取最新版本的依賴,可以使用 -u 參數(shù):

示例:go get -u github.com/user/repo 這將更新指定包及其依賴項(xiàng)到最新的次要版本或修訂版本。

代碼在 GitHub 上的組織形式

確保代碼庫的目錄結(jié)構(gòu)直接反映包的導(dǎo)入路徑,而不要使用 src 目錄作為根目錄。這使得項(xiàng)目更容易與 Go 的依賴管理工具兼容,確保導(dǎo)入路徑的簡潔和一致性。 github.com/username/project/

├── mypackage/

│ └── mypackage.go

└── anotherpackage/

└── anotherpackage.go

最佳實(shí)踐:在 GitHub 上組織代碼時(shí),目錄結(jié)構(gòu)應(yīng)與包名匹配,例如:這樣可以避免導(dǎo)入路徑中的多余層級,并確保使用 go get 時(shí)能正確定位包。

按照該思路我們進(jìn)行驗(yàn)證,在在 chapter7 目錄下創(chuàng)建一個名為 remote_package 的包,我們先進(jìn)行下載“go get github.com/easierway/concurrent_map”的下載,然后創(chuàng)建remote_package_test.go進(jìn)行驗(yàn)證:

package remote

import (

"fmt"

"testing"

cm "github.com/easierway/concurrent_map"

)

func TestConcurrentMap(t *testing.T) {

m := cm.CreateConcurrentMap(99)

m.Set(cm.StrKey("key"), 10)

value, ok := m.Get(cm.StrKey("key"))

if ok {

fmt.Println("Key found:", value)

t.Log(m.Get(cm.StrKey("key")))

}

}

concurrent_map的介紹:concurrent_map 是一個由 GitHub 用戶 easierway 創(chuàng)建的 Go 包,主要用于實(shí)現(xiàn)線程安全的并發(fā) map 數(shù)據(jù)結(jié)構(gòu)。這個包提供了一種簡單且高效的方式來處理并發(fā)環(huán)境下的 map 操作,避免了傳統(tǒng) map 在多 goroutine 訪問時(shí)出現(xiàn)的競爭問題。

功能/特點(diǎn)說明線程安全通過分段鎖機(jī)制(分片鎖)確保 map 在多 goroutine 并發(fā)訪問時(shí)的數(shù)據(jù)安全。高效的讀寫操作將 map 分成多個子 map,減少鎖的粒度,提高并發(fā)訪問的效率。簡單易用的 API提供類似標(biāo)準(zhǔn) map 的接口,如 Set、Get、Remove,使用方式簡單。動態(tài)擴(kuò)展根據(jù)使用需求動態(tài)擴(kuò)展或收縮分段,提高資源利用率。

4.包的依賴管理

Go 語言在早期的依賴管理中(使用 GOPATH)確實(shí)存在一些未解決的問題:

同一環(huán)境下,不同項(xiàng)目使用同一包的不同版本

在 Go Modules 引入之前,Go 的依賴管理依賴于 GOPATH 目錄。所有的項(xiàng)目共享同一個 GOPATH,這就導(dǎo)致了一個問題:如果兩個項(xiàng)目需要使用同一包的不同版本,由于 GOPATH 中同一個包只能有一個版本,無法同時(shí)滿足這兩個項(xiàng)目的需求。這種情況下,開發(fā)者往往需要手動管理和切換包版本,帶來了很大的麻煩和不確定性。

無法管理對包的特定版本的依賴

在沒有 Go Modules 之前,Go 的依賴管理缺乏對包版本的精確控制。通常情況下,開發(fā)者只能獲取最新版本的包,這就導(dǎo)致了以下問題:

當(dāng)某個包發(fā)布了不兼容的新版本時(shí),項(xiàng)目可能會因自動升級到新版本而導(dǎo)致編譯或運(yùn)行錯誤。難以重現(xiàn)歷史版本的構(gòu)建,因?yàn)闊o法確定項(xiàng)目依賴的具體版本。

Go Modules 如何解決這些問題

為了解決這些問題,Go 從 1.11 版本開始引入了 Go Modules,從根本上改變了 Go 的依賴管理方式。Go Modules 提供了版本控制和模塊隔離的機(jī)制,避免了上述問題。

不同項(xiàng)目使用同一包的不同版本

獨(dú)立的模塊空間:每個 Go 項(xiàng)目通過 go.mod 文件獨(dú)立管理其依賴關(guān)系。go.mod 文件定義了項(xiàng)目所依賴的所有包及其版本,這些包會被下載到 $GOPATH/pkg/mod 下,并且是根據(jù)模塊名和版本號來隔離的。因此,不同項(xiàng)目可以使用同一包的不同版本,而不會相互干擾。無需全局 GOPATH:Go Modules 擺脫了對全局 GOPATH 的依賴,轉(zhuǎn)而使用模塊級的依賴管理。每個項(xiàng)目的依賴包版本在項(xiàng)目目錄下獨(dú)立管理,避免了版本沖突。

管理對包的特定版本的依賴

精確的版本控制:在 go.mod 文件中,你可以指定依賴包的具體版本。Go Modules 支持語義化版本控制(Semantic Versioning),你可以通過 @ 符號指定某個依賴包的版本號(如 v1.2.3),或者使用 go get @ 命令來更新某個依賴的版本。這樣,你可以明確指定和鎖定項(xiàng)目依賴的版本,確保項(xiàng)目的可重現(xiàn)性。版本兼容性和依賴解析:Go Modules 通過 go.mod 和 go.sum 文件管理版本依賴,確保項(xiàng)目構(gòu)建過程中使用的依賴版本是可預(yù)測且穩(wěn)定的。即使某個依賴包發(fā)布了新版本,你的項(xiàng)目仍會使用 go.mod 中指定的版本,除非你主動升級。

雖然 Go Modules 解決了許多依賴管理問題,但它也帶來了一些新的挑戰(zhàn):

多模塊項(xiàng)目的管理:在一些大型項(xiàng)目中,可能會有多個模塊,這些模塊之間的依賴管理需要謹(jǐn)慎處理,特別是當(dāng)這些模塊之間存在依賴關(guān)系時(shí)。依賴沖突:如果不同的依賴項(xiàng)依賴于同一個包的不同版本,Go Modules 會嘗試找到一個可用的共同版本,但這可能并不總是理想的解決方案。

Go Modules 通過模塊化和版本控制,基本解決了 Go 語言早期依賴管理中的主要問題,如同一環(huán)境下不同項(xiàng)目使用同一包的不同版本,以及對包的特定版本的依賴管理問題。然而,盡管如此,隨著項(xiàng)目規(guī)模的擴(kuò)大和依賴關(guān)系的復(fù)雜化,依賴管理仍然需要開發(fā)者謹(jǐn)慎對待。

(八)并發(fā)編程

src目錄下創(chuàng)建chapter8,展開后續(xù)的學(xué)習(xí)。

1.協(xié)程機(jī)制

Thread vs?Goroutine

Java 的線程(Thread)與 Go 語言的協(xié)程(Goroutine)在設(shè)計(jì)哲學(xué)和實(shí)現(xiàn)細(xì)節(jié)上有很大的不同,主要表現(xiàn)在棧大小及與內(nèi)核空間實(shí)體(KSE)的對應(yīng)關(guān)系方面:

比較項(xiàng)Java ThreadGoroutine棧的初始大小1MB(JDK5 及以后版本)2KB棧的增長方式固定大小,超出時(shí)拋出 StackOverflowError動態(tài)增長,最大可擴(kuò)展到 1GB與內(nèi)核線程的對應(yīng)關(guān)系1:1 模型,每個 Java 線程對應(yīng)一個內(nèi)核線程M模型,多個 Goroutine 對應(yīng)少量內(nèi)核線程調(diào)度方式由操作系統(tǒng)調(diào)度由 Go 運(yùn)行時(shí)調(diào)度創(chuàng)建和調(diào)度的開銷較大,創(chuàng)建和切換線程的開銷較高較小,創(chuàng)建和調(diào)度 Goroutine 的開銷非常低并發(fā)處理能力創(chuàng)建大量線程時(shí)可能影響系統(tǒng)性能可以高效創(chuàng)建和管理大量 Goroutine

Goroutine 的調(diào)度原理

左側(cè)圖示展示了 Goroutine 在正常調(diào)度情況下的工作原理:

M(System Thread):代表操作系統(tǒng)的線程,圖中有兩個系統(tǒng)線程,M0 和 M1。 M0 正在執(zhí)行某個 Goroutine。 P(Processor):代表處理器,這里是 Go 的調(diào)度器中的一個抽象概念,而不是實(shí)際的 CPU 核心。P 負(fù)責(zé)執(zhí)行 Goroutine 的隊(duì)列,并將它們映射到系統(tǒng)線程(M)上。 圖中有一個 P,它將 Goroutine 分配給 M 進(jìn)行執(zhí)行。 G(Goroutine):代表 Goroutine,圖中 G0, G1, G2 等分別表示不同的 Goroutine。 G0 正在被 M0 執(zhí)行。G1, G2 仍在等待被調(diào)度執(zhí)行。

右側(cè)圖示展示了 Goroutine 發(fā)生系統(tǒng)調(diào)用(Syscall)時(shí)的工作原理:

Syscall:當(dāng) Goroutine 需要執(zhí)行系統(tǒng)調(diào)用時(shí),執(zhí)行該 Goroutine 的系統(tǒng)線程會被阻塞。 這里 M0 在處理 G0 的系統(tǒng)調(diào)用,因此 M0 被阻塞在系統(tǒng)調(diào)用中。 M1:系統(tǒng)線程 M1 被調(diào)度來繼續(xù)處理 P 中的其他 Goroutine。 P 調(diào)度了其他的 Goroutine(如 G1, G2)到新的系統(tǒng)線程 M1 上繼續(xù)執(zhí)行,從而避免了因?yàn)橐粋€ Goroutine 阻塞而導(dǎo)致整個線程阻塞的情況。

調(diào)度機(jī)制總結(jié)

Go 運(yùn)行時(shí)調(diào)度器通過 M:P:G 模型實(shí)現(xiàn)了 Goroutine 的高效調(diào)度。M(系統(tǒng)線程)可以執(zhí)行多個 G(Goroutine),而 P(Processor)則決定哪些 Goroutine 應(yīng)該運(yùn)行在 M 上。當(dāng)一個 Goroutine 被阻塞時(shí)(如執(zhí)行系統(tǒng)調(diào)用),Go 運(yùn)行時(shí)會將該系統(tǒng)線程從調(diào)度隊(duì)列中移除,并將剩余的 Goroutine 調(diào)度到其他空閑的系統(tǒng)線程上繼續(xù)執(zhí)行。這樣可以有效地利用系統(tǒng)資源,避免線程阻塞導(dǎo)致的資源浪費(fèi),體現(xiàn)了 Goroutine 的輕量化和高效性。

這張圖很直觀地展示了 Go 語言中 Goroutine 的 M 模型,如何通過 M, P, G 之間的協(xié)作,實(shí)現(xiàn)高效的并發(fā)調(diào)度。

直接的代碼展示

直接在chapter8下新建groutine,編寫groutine_test.go代碼如下:

package groutine

import (

"fmt"

"testing"

"time"

)

func sayHello() {

fmt.Println("Hello, Goroutine!")

}

func TestGroutine(t *testing.T) {

for i := 0; i < 10; i++ {

go func(i int) {

//time.Sleep(time.Second * 1)

fmt.Println(i)

}(i)

}

time.Sleep(time.Millisecond * 50)

}

func TestSayHello(t *testing.T) {

go sayHello() // 啟動一個新的 Goroutine 執(zhí)行 sayHello 函數(shù)

// 主函數(shù)等待一段時(shí)間,確保 Goroutine 有機(jī)會執(zhí)行

time.Sleep(time.Millisecond * 10)

fmt.Println("Main function finished")

}

TestGroutine 展示了如何在循環(huán)中創(chuàng)建多個 Goroutine 并并發(fā)執(zhí)行任務(wù),同時(shí)說明了 Goroutine 的變量捕獲問題。TestSayHello 展示了如何使用 Goroutine 并發(fā)執(zhí)行一個簡單的函數(shù),并突出 Goroutine 的非阻塞特性。

通過這兩個函數(shù),可以更好地理解 Go 語言中 Goroutine 的基本使用方式以及它們在并發(fā)編程中的作用。

2.共享內(nèi)存并發(fā)機(jī)制

在chapter8下新建share_mem,我們可以先寫下面的代碼share_mem_test.go來體驗(yàn)共享內(nèi)存并發(fā)機(jī)制的控制:

未引入同步處理情況

package share_mem

import (

"testing"

"time"

)

func TestCounter(t *testing.T) {

counter := 0

for i := 0; i < 5000; i++ {

go func() {

counter++

}()

}

time.Sleep(1 * time.Second)

t.Logf("counter = %d", counter)

}

運(yùn)行結(jié)果為:

=== RUN TestCounter

share_mem_test.go:17: counter = 4426

--- PASS: TestCounter (1.01s)

PASS

在代碼中,5000 個 goroutine 同時(shí)對 counter 變量進(jìn)行遞增操作(counter++),但 counter++ 并不是原子操作,它實(shí)際上包含了三步:

讀取 counter 的當(dāng)前值。對 counter 的值加 1。將新的值寫回 counter。

在并發(fā)環(huán)境下,不同的 goroutine 可能在同一時(shí)間讀取 counter,并且在寫入時(shí)產(chǎn)生沖突。舉個例子,兩個 goroutine 可能在幾乎同一時(shí)刻讀取到 counter 的值為 100,然后都試圖將 counter 更新為 101。由于沒有同步機(jī)制,其中一個更新可能會被覆蓋,從而導(dǎo)致 counter 的實(shí)際值小于預(yù)期的 5000。

注意,雖然程序使用了 time.Sleep(1 * time.Second) 來等待 goroutine 執(zhí)行完畢,但這并不能解決數(shù)據(jù)競爭的問題。即使所有 goroutine 都在一秒內(nèi)完成,counter 變量的遞增操作依然存在競爭。

sync.Mutex

要解決這個問題,需要引入同步機(jī)制來保護(hù)對共享變量的訪問。常見的同步方法包括:

使用 sync.Mutex 來確保每次只有一個 goroutine 能夠修改 counter。使用 sync/atomic 包中的原子操作,如 atomic.AddInt32 或 atomic.AddInt64。

我們使用sync.Mutex修改代碼實(shí)現(xiàn)如下:

func TestCounterThreadSafe(t *testing.T) {

var mut sync.Mutex

counter := 0

for i := 0; i < 5000; i++ {

go func() {

mut.Lock() // 先獲取鎖

defer mut.Unlock() // 確保函數(shù)退出時(shí)解鎖

counter++ // 安全遞增

}()

}

time.Sleep(1 * time.Second)

t.Logf("counter = %d", counter) // 輸出最終的 counter 值

}

這個時(shí)候運(yùn)行結(jié)果符合我們的預(yù)期:

=== RUN TestCounterThreadSafe

share_mem_test.go:32: counter = 5000

--- PASS: TestCounterThreadSafe (1.00s)

PASS

sync.WaitGroup

上面的代碼中去掉 time.Sleep(1 * time.Second) 后,雖然代碼本身有加鎖保護(hù) counter 的訪問,但依然出現(xiàn)錯誤的運(yùn)行結(jié)果,這與并發(fā) goroutine 的執(zhí)行時(shí)機(jī)有關(guān):

goroutine 的非阻塞執(zhí)行:在 Go 語言中,go 關(guān)鍵字啟動的 goroutine 是并發(fā)運(yùn)行的,而不是同步運(yùn)行的。啟動 goroutine 后,主程序并不會等待它們執(zhí)行完畢就繼續(xù)執(zhí)行。也就是說,go func() 語句啟動的 5000 個 goroutine 是異步執(zhí)行的。而 t.Logf("counter = %d", counter) 是在主函數(shù)中執(zhí)行的。當(dāng)主函數(shù)到達(dá) t.Logf 時(shí),主程序并沒有等待這些 goroutine 執(zhí)行完畢,而是直接打印了 counter 的值。主程序過早結(jié)束:由于去掉了 time.Sleep(1 * time.Second),主程序并不會等到所有 goroutine 執(zhí)行完成,而是可能在 goroutine 尚未執(zhí)行或部分執(zhí)行完時(shí)就已經(jīng)輸出了 counter 的值。因此,counter 的值通常會小于 5000,因?yàn)椴⒉皇撬械?goroutine 都有機(jī)會執(zhí)行 counter++ 操作。

我們需要一種機(jī)制來確保主程序等待所有 goroutine 完成后再輸出 counter。可以使用 sync.WaitGroup 來實(shí)現(xiàn)這個目標(biāo),具體代碼如下:

func TestCounterWaitGroup(t *testing.T) {

var mut sync.Mutex

counter := 0

var wg sync.WaitGroup // 聲明 WaitGroup

for i := 0; i < 5000; i++ {

wg.Add(1) // 每啟動一個 goroutine,WaitGroup 計(jì)數(shù)器加 1

go func() {

defer wg.Done() // 當(dāng) goroutine 完成時(shí),計(jì)數(shù)器減 1

mut.Lock() // 先獲取鎖

defer mut.Unlock() // 確保函數(shù)退出時(shí)解鎖

counter++ // 安全遞增

}()

}

wg.Wait() // 等待所有 goroutine 完成

t.Logf("counter = %d", counter) // 輸出最終的 counter 值

}

此時(shí)運(yùn)行結(jié)果正常。WaitGroup 是 Go 語言中的一種用于并發(fā)控制的同步機(jī)制,主要用于等待一組 Goroutines 完成執(zhí)行。當(dāng)你有多個 Goroutines 需要同時(shí)執(zhí)行并等待它們?nèi)客瓿蓵r(shí),WaitGroup 提供了一種簡單的方式來實(shí)現(xiàn)這一需求。

WaitGroup 主要有三個方法:

Add(delta int):添加或減少等待的 Goroutines 計(jì)數(shù)。參數(shù) delta 表示增加或減少的 Goroutines 數(shù)量,通常是正數(shù)增加等待數(shù)量,負(fù)數(shù)減少等待數(shù)量。Done():表示一個 Goroutine 完成了工作,通常在 Goroutine 結(jié)束時(shí)調(diào)用,等價(jià)于 Add(-1)。Wait():阻塞當(dāng)前 Goroutine,直到 WaitGroup 計(jì)數(shù)為零,也就是所有添加的 Goroutines 都完成了工作。

WaitGroup 非常適合用于多個 Goroutines 并發(fā)執(zhí)行并且需要在主程序中等待它們?nèi)客瓿傻膱鼍啊@纾?/p>

并發(fā)下載文件后統(tǒng)一處理。并行處理多個任務(wù),最終匯總結(jié)果。

這種機(jī)制在 Go 語言中非常常用,結(jié)合 Goroutines,可以極大地提高程序的并發(fā)能力。

3.CSP 并發(fā)機(jī)制

Go 語言的并發(fā)機(jī)制是基于 CSP(Communicating Sequential Processes,通信順序進(jìn)程) 模型。CSP 是一種并發(fā)模型,允許多個獨(dú)立的進(jìn)程通過消息傳遞進(jìn)行通信,而不是通過共享內(nèi)存來交換數(shù)據(jù)。Go 通過 Goroutines 和 Channels 實(shí)現(xiàn)了這種并發(fā)機(jī)制,使得并發(fā)編程變得更加簡單和安全。

基本原理

Goroutine在上面已經(jīng)了解到了,現(xiàn)在理解一下Channels,Channels 是 Go 中用于 Goroutines 之間通信的機(jī)制,可以通過 chan 關(guān)鍵字定義,它們允許一個 Goroutine 發(fā)送數(shù)據(jù),另一個 Goroutine 接收數(shù)據(jù)。Channels 可以是無緩沖的(阻塞式)或帶緩沖的(非阻塞式):

左邊的圖片展示了 Goroutines(標(biāo)記為“GR”)使用 無緩沖通道 進(jìn)行通信的過程。在無緩沖通道中,發(fā)送和接收操作是阻塞的,意味著發(fā)送方必須等待接收方準(zhǔn)備好接收,反之亦然。 在第一張圖中,Goroutine 1 和 Goroutine 2 之間的通道是空的,表示發(fā)送或接收操作的阻塞狀態(tài)。在后面的圖中,Goroutines 通過 Channel 成功交換了數(shù)據(jù)。 右邊的圖片展示了 有緩沖通道 的情況。與無緩沖通道不同,有緩沖通道允許在不阻塞的情況下存儲一定數(shù)量的數(shù)據(jù)。 每個緩沖通道都有一定數(shù)量的存儲槽位(圖中綠色塊代表緩沖區(qū))。發(fā)送者可以連續(xù)發(fā)送多個數(shù)據(jù),直到緩沖區(qū)被填滿。例如,在第一張圖中,通道緩沖區(qū)未滿,Goroutine 能繼續(xù)向緩沖區(qū)發(fā)送數(shù)據(jù)。當(dāng)緩沖區(qū)滿時(shí),發(fā)送方將阻塞,直到接收方消費(fèi)數(shù)據(jù)。無緩沖通道:發(fā)送和接收必須同步,發(fā)送方和接收方必須同時(shí)準(zhǔn)備好進(jìn)行通信。有緩沖通道:發(fā)送方可以發(fā)送多條消息,直到緩沖區(qū)滿后阻塞,接收方可以在緩沖區(qū)不為空時(shí)接收消息。

Goroutines 使用 channel 進(jìn)行通信時(shí),可以通過 <- 操作符發(fā)送或接收數(shù)據(jù):

發(fā)送數(shù)據(jù):ch <- value接收數(shù)據(jù):value := <-ch

通過這種方式,Goroutines 之間通過消息傳遞進(jìn)行同步,無需顯式的鎖機(jī)制。

代碼體驗(yàn)

我們在chapter8下新建csp,寫async_service_test.go如下:

package concurrency

import (

"fmt"

"testing"

"time"

)

func service() string {

time.Sleep(time.Millisecond * 50)

return "Done"

}

func otherTask() {

fmt.Println("working on something else")

time.Sleep(time.Millisecond * 100)

fmt.Println("Task is done.")

}

func TestService(t *testing.T) {

fmt.Println(service())

otherTask()

}

func AsyncService() chan string {

retCh := make(chan string, 1)

//retCh := make(chan string, 1)

go func() {

ret := service()

fmt.Println("returned result.")

retCh <- ret

fmt.Println("service exited.")

}()

return retCh

}

func TestAsynService(t *testing.T) {

retCh := AsyncService()

otherTask()

fmt.Println(<-retCh)

time.Sleep(time.Second * 1)

}

同步服務(wù)調(diào)用 (service 和 otherTask): 函數(shù) service() 模擬了一個耗時(shí)的操作,使用 time.Sleep 來模擬延遲。這個函數(shù)同步返回結(jié)果。otherTask() 是另一個任務(wù),在 service() 完成之前啟動,顯示了 Go 中任務(wù)的順序執(zhí)行。在 TestService 測試函數(shù)中,service() 會首先執(zhí)行,之后才執(zhí)行 otherTask()。這表明在沒有并發(fā)情況下的順序執(zhí)行。 異步服務(wù)調(diào)用 (AsyncService): AsyncService() 函數(shù)使用了 Goroutine 來并發(fā)執(zhí)行 service()。返回值通過一個帶緩沖的 Channel (retCh) 返回給調(diào)用方。這里的 go func() 表示 Goroutine 是并發(fā)執(zhí)行的;retCh <- ret 將 service() 的結(jié)果通過 Channel 傳遞給主 Goroutine,從而實(shí)現(xiàn)異步操作。帶緩沖的 Channel(大小為 1)保證了即使主 Goroutine沒有及時(shí)接收,數(shù)據(jù)依然可以被發(fā)送 Goroutine 存入緩沖區(qū)。 異步服務(wù)的測試 (TestAsynService): 在 TestAsynService() 測試函數(shù)中,首先通過 AsyncService() 啟動異步任務(wù)。同時(shí),otherTask() 開始執(zhí)行,展示了異步執(zhí)行 service() 的同時(shí)還能執(zhí)行其他任務(wù)。最后通過 <-retCh 來等待 AsyncService 的結(jié)果。

AsyncService() 和 TestAsynService() 的設(shè)計(jì)展示了如何在執(zhí)行某個任務(wù)的同時(shí),能夠處理其他任務(wù)。通過 Channel 傳遞結(jié)果的機(jī)制,主 Goroutine 不必等待 service() 的完成,可以同時(shí)進(jìn)行其他工作,然后通過 Channel 來獲取異步任務(wù)的結(jié)果。

整個并發(fā)過程沒有使用任何鎖,而是通過 Channels 保證了數(shù)據(jù)的安全傳遞,避免了共享內(nèi)存的競爭問題。這符合 Go 并發(fā)模型的核心思想:“不要通過共享內(nèi)存來通信,而要通過通信來共享內(nèi)存”。

4.多路選擇和超時(shí)

多路選擇(select)和超時(shí)是 Go 并發(fā)編程中非常重要的特性

多路選擇(select)

Go 提供了 select 語句,它類似于 switch,但專門用于處理 Channel 操作。通過 select,可以在多個 Channel 操作中等待,哪個 Channel 準(zhǔn)備好就處理哪個。select 使得處理多個 Goroutines 和 Channel 的通信變得更加簡潔和高效。select 的基本語法:

select {

case msg1 := <-chan1:

fmt.Println("Received", msg1)

case msg2 := <-chan2:

fmt.Println("Received", msg2)

default:

fmt.Println("No channel is ready")

}

case 語句:每個 case 都包含一個 Channel 操作。select 會等待第一個準(zhǔn)備好的 Channel 并執(zhí)行相應(yīng)的 case。一旦選擇了一個 case,其他的 case 將不會被執(zhí)行。也就是說,select 語句每次只會執(zhí)行一個 case,然后退出 select 語句。default:當(dāng)所有的 Channel 都沒有準(zhǔn)備好時(shí),default 會被執(zhí)行。它可以用于防止 select 阻塞。

我們在chapter8下新建multiplexing,新建multiplexing_test.go驗(yàn)證:

package multiplexing

import (

"testing"

"time"

)

func TestMultiplexing(t *testing.T) {

ch1 := make(chan string)

ch2 := make(chan string)

go func() {

time.Sleep(2 * time.Second)

ch1 <- "result from ch1"

}()

go func() {

time.Sleep(1 * time.Second)

ch2 <- "result from ch2"

}()

select {

case msg1 := <-ch1:

t.Log("msg1 := <-ch1:" + msg1)

if msg1 != "result from ch1" {

t.Errorf("Expected 'result from ch1', got %s", msg1)

}

case msg2 := <-ch2:

t.Log("msg2 := <-ch2:" + msg2)

if msg2 != "result from ch2" {

t.Errorf("Expected 'result from ch2', got %s", msg2)

}

}

}

運(yùn)行無異常,符合我們的預(yù)期。

超時(shí)機(jī)制

在并發(fā)編程中,有時(shí)我們希望設(shè)置一個操作的最大等待時(shí)間。如果超過指定時(shí)間,程序不再等待,而是進(jìn)行其他操作。Go 中的 select 可以結(jié)合 time.After() 實(shí)現(xiàn)超時(shí)控制:

time.After(d) 返回一個 Channel,d 時(shí)間后會向該 Channel 發(fā)送一個時(shí)間值。當(dāng) select 等待時(shí),如果其他 Channels 沒有準(zhǔn)備好,而超時(shí)時(shí)間已經(jīng)到了,select 會選擇執(zhí)行 time.After 對應(yīng)的 case。

我們在chapter8下新建timeout,新建time_test.go驗(yàn)證:

package timeout

import (

"testing"

"time"

)

func TestTimeout(t *testing.T) {

ch := make(chan string)

go func() {

time.Sleep(3 * time.Second)

ch <- "result"

}()

select {

case result := <-ch:

t.Errorf("Expected timeout, but received %s", result)

case <-time.After(2 * time.Second):

// Test passes if timeout occurs

t.Log("Test passed: Operation timed out as expected")

}

}

這樣運(yùn)行結(jié)果如下,符合我們的預(yù)期:

=== RUN TestTimeout

time_test.go:21: Test passed: Operation timed out as expected

--- PASS: TestTimeout (2.00s)

PASS

5.channel的關(guān)閉和廣播

在 Go 語言中,通道(channel)的關(guān)閉和廣播是兩個重要的概念。它們分別涉及如何管理通道的生命周期和如何向多個接收者發(fā)送相同的數(shù)據(jù)。

通道的關(guān)閉(Channel Closure)

通道的關(guān)閉是一種信號,表示沒有更多的數(shù)據(jù)會被發(fā)送到通道。關(guān)閉通道可以用來通知接收方,表示數(shù)據(jù)流的結(jié)束。關(guān)閉通道不會影響已經(jīng)存在的接收操作,但會阻止進(jìn)一步的發(fā)送操作。

通道的關(guān)閉操作使用 close 函數(shù)來實(shí)現(xiàn):

close(ch)

關(guān)閉通道的作用:

通知接收方:關(guān)閉通道可以通知接收方數(shù)據(jù)已經(jīng)發(fā)送完畢,沒有更多的數(shù)據(jù)會發(fā)送到通道。防止發(fā)送:一旦通道被關(guān)閉,任何試圖向通道發(fā)送數(shù)據(jù)的操作都會導(dǎo)致 panic。在某些情況下,關(guān)閉通道可以幫助避免死鎖,因?yàn)榻邮辗娇梢詸z測到通道是否已經(jīng)關(guān)閉,從而采取適當(dāng)?shù)男袆印?/p>

如何檢測通道是否關(guān)閉: 接收通道的值時(shí),可以通過兩個返回值來檢測通道是否關(guān)閉(ok為bool 值, true 表示正常接受,false 表示通道關(guān)閉):

msg, ok := <-ch

if !ok {

// 通道已關(guān)閉

}

測試通道的安全關(guān)閉和終止消費(fèi)者:我們在chapter8下新建closechannel,創(chuàng)建channelclose_consumer_termination_test.go驗(yàn)證通道關(guān)閉后的消費(fèi)者是否能夠正確地接收到所有數(shù)據(jù)并安全地退出:

package closechannel

import (

"fmt"

"testing"

"time"

)

func TestChannelCloseAndConsumerTermination(t *testing.T) {

dataCh := make(chan int)

doneCh := make(chan struct{}) // 用于通知消費(fèi)者結(jié)束

// 啟動生產(chǎn)者

go func() {

for i := 0; i < 5; i++ {

dataCh <- i

fmt.Printf("Produced: %d\n", i)

time.Sleep(500 * time.Millisecond)

}

close(dataCh) // 關(guān)閉數(shù)據(jù)通道

fmt.Println("Producer closed the channel")

}()

// 啟動消費(fèi)者

go func() {

for {

select {

case data, ok := <-dataCh:

if !ok {

// 數(shù)據(jù)通道已關(guān)閉,退出循環(huán)

doneCh <- struct{}{}

fmt.Println("Consumer detected channel closure")

return

}

fmt.Printf("Received data: %d\n", data)

}

}

}()

// 等待消費(fèi)者完成

<-doneCh

fmt.Println("Consumer has finished processing")

}

基本執(zhí)行如下:

=== RUN TestChannelCloseAndConsumerTermination

Produced: 0

Received data: 0

Produced: 1

Received data: 1

Received data: 2

Produced: 2

Produced: 3

Received data: 3

Produced: 4

Received data: 4

Consumer detected channel closure

Producer closed the channel

Consumer has finished processing

--- PASS: TestChannelCloseAndConsumerTermination (2.51s)

PASS

廣播(Broadcast)

廣播是指將一條消息發(fā)送到多個接收者。在 Go 中,通常通過以下方式實(shí)現(xiàn)廣播:

通過多個接收者的通道:創(chuàng)建多個 goroutine,每個 goroutine 都接收同一個通道的數(shù)據(jù)。 使用 sync.WaitGroup 等待所有接收者處理完成:確保在廣播完成后,所有的接收者都能處理完消息。

假設(shè)我們有一個廣播的場景,生產(chǎn)者發(fā)送消息到通道,并且所有消費(fèi)者都能接收到這些消息。我們可以用以下方式演示:

生產(chǎn)者:一個生產(chǎn)者將幾條消息發(fā)送到通道。消費(fèi)者:多個消費(fèi)者從通道接收消息,并輸出收到的消息。

我們在chapter8下新建broadcast,創(chuàng)建broadcast_test.go驗(yàn)證廣播功能,即一個生產(chǎn)者發(fā)出的消息可以被所有消費(fèi)者接收到:

package broadcast

import (

"fmt"

"sync"

"testing"

"time"

)

func TestSimpleBroadcast(t *testing.T) {

dataCh := make(chan int)

var wg sync.WaitGroup

numConsumers := 3

numMessages := 5

// 啟動消費(fèi)者

for i := 0; i < numConsumers; i++ {

wg.Add(1)

go func(consumerID int) {

defer wg.Done()

for data := range dataCh {

fmt.Printf("Consumer %d received: %d\n", consumerID, data)

}

fmt.Printf("Consumer %d finished\n", consumerID)

}(i)

}

// 啟動生產(chǎn)者

go func() {

for i := 0; i < numMessages; i++ {

dataCh <- i

fmt.Printf("Producer broadcasted: %d\n", i)

time.Sleep(500 * time.Millisecond)

}

close(dataCh)

fmt.Println("Producer finished, channel closed")

}()

// 等待所有消費(fèi)者完成

wg.Wait()

}

運(yùn)行結(jié)果可直觀感知到:

=== RUN TestSimpleBroadcast

Consumer 1 received: 0

Producer broadcasted: 0

Consumer 0 received: 1

Producer broadcasted: 1

Producer broadcasted: 2

Consumer 1 received: 2

Producer broadcasted: 3

Consumer 2 received: 3

Producer broadcasted: 4

Consumer 0 received: 4

Consumer 0 finished

Consumer 1 finished

Producer finished, channel closed

Consumer 2 finished

--- PASS: TestSimpleBroadcast (2.51s)

PASS

從結(jié)果來看,這個輸出并沒有完全實(shí)現(xiàn)預(yù)期的“廣播”效果。理想情況下,所有消費(fèi)者應(yīng)該接收到每一條生產(chǎn)者廣播的消息,但在實(shí)際結(jié)果中,每條消息似乎只被一個消費(fèi)者接收。這意味著通道中的消息并沒有被廣播到所有消費(fèi)者,而是被其中一個消費(fèi)者處理,這符合 Go 語言的通道默認(rèn)行為:點(diǎn)對點(diǎn)的通信模式,意味著每條消息只會被一個 goroutine(消費(fèi)者)接收。要實(shí)現(xiàn)真正的廣播(所有消費(fèi)者都能接收到每條消息),我們可以通過在每個消費(fèi)者中為每個接收者創(chuàng)建一個單獨(dú)的通道副本,或者借助 sync.Cond 或其他高級同步機(jī)制,來確保每個消費(fèi)者都能接收到相同的消息。

func TestImprovedBroadcast(t *testing.T) {

numConsumers := 3

numMessages := 5

// 為每個消費(fèi)者創(chuàng)建一個接收通道

channels := make([]chan int, numConsumers)

for i := range channels {

channels[i] = make(chan int)

}

var wg sync.WaitGroup

// 啟動消費(fèi)者

for i := 0; i < numConsumers; i++ {

wg.Add(1)

go func(consumerID int, ch <-chan int) {

defer wg.Done()

for data := range ch {

fmt.Printf("Consumer %d received: %d\n", consumerID, data)

}

fmt.Printf("Consumer %d finished\n", consumerID)

}(i, channels[i])

}

// 啟動生產(chǎn)者

go func() {

for i := 0; i < numMessages; i++ {

fmt.Printf("Producer broadcasted: %d\n", i)

// 將消息廣播給所有消費(fèi)者

for _, ch := range channels {

ch <- i

}

time.Sleep(500 * time.Millisecond)

}

// 關(guān)閉所有消費(fèi)者通道

for _, ch := range channels {

close(ch)

}

fmt.Println("Producer finished, channels closed")

}()

// 等待所有消費(fèi)者完成

wg.Wait()

}

從運(yùn)行結(jié)果看,確保了每個消費(fèi)者都能接收到生產(chǎn)者廣播的每一條消息。輸出應(yīng)類似于以下內(nèi)容:

=== RUN TestImprovedBroadcast

Producer broadcasted: 0

Consumer 1 received: 0

Consumer 2 received: 0

Consumer 0 received: 0

Producer broadcasted: 1

Consumer 2 received: 1

Consumer 0 received: 1

Consumer 1 received: 1

Producer broadcasted: 2

Consumer 2 received: 2

Consumer 0 received: 2

Consumer 1 received: 2

Producer broadcasted: 3

Consumer 2 received: 3

Consumer 0 received: 3

Consumer 1 received: 3

Producer broadcasted: 4

Consumer 0 received: 4

Consumer 2 received: 4

Consumer 1 received: 4

Producer finished, channels closed

Consumer 0 finished

Consumer 1 finished

Consumer 2 finished

--- PASS: TestImprovedBroadcast (2.51s)

PASS

通過為每個消費(fèi)者創(chuàng)建獨(dú)立的通道副本,你可以確保所有消費(fèi)者都能接收到廣播的所有消息。

6.簡單的任務(wù)取消機(jī)制

我們實(shí)現(xiàn)一個簡單的任務(wù)取消機(jī)制,主要依靠一個 cancelChan 來控制任務(wù)的終止。通過向該通道發(fā)送信號或者關(guān)閉通道,可以通知多個 goroutine 停止運(yùn)行。這種機(jī)制與 Go 中的 context 取消機(jī)制有相似之處,但它是手動實(shí)現(xiàn)的,功能上略微簡化。

我們在chapter8下新建simplecancel,然后寫simplecancel_test.go如下:

package simplecancel

import (

"fmt"

"testing"

"time"

)

func isCancelled(cancelChan chan struct{}) bool {

select {

case <-cancelChan:

return true

default:

return false

}

}

func cancel_1(cancelChan chan struct{}) {

cancelChan <- struct{}{}

}

func cancel_2(cancelChan chan struct{}) {

close(cancelChan)

}

func TestCancel(t *testing.T) {

cancelChan := make(chan struct{}, 0)

for i := 0; i < 5; i++ {

go func(i int, cancelCh chan struct{}) {

for {

if isCancelled(cancelCh) {

break

}

time.Sleep(time.Millisecond * 5)

}

fmt.Println(i, "Cancelled")

}(i, cancelChan)

}

cancel_1(cancelChan)

time.Sleep(time.Second * 1)

}

isCancelled 函數(shù):這個函數(shù)檢查 cancelChan 是否已經(jīng)關(guān)閉或是否有信號通過。通過 select 語句,非阻塞地檢查通道狀態(tài)。如果通道已被關(guān)閉或者已經(jīng)收到信號,則返回 true,表示任務(wù)應(yīng)當(dāng)取消。取消機(jī)制:cancel_1---這個函數(shù)通過向 cancelChan 發(fā)送一個空的結(jié)構(gòu)體,通知所有監(jiān)聽該通道的 goroutine 任務(wù)應(yīng)當(dāng)取消。每個 goroutine 會在 isCancelled 函數(shù)中收到此信號;cancel_2---這個函數(shù)通過關(guān)閉通道來通知所有監(jiān)聽的 goroutine 任務(wù)已取消。不同于 cancel_1,close 操作會通知所有等待在該通道上的接收者,不需要發(fā)送多個信號,因此更適用于廣播取消的場景。goroutine 的取消邏輯:在 TestCancel 中啟動?5 個 goroutine,每個 goroutine 不斷地檢查 cancelChan 來判斷是否需要停止。每個 goroutine 在循環(huán)中使用 isCancelled 函數(shù)檢查通道狀態(tài)。如果通道被關(guān)閉或接收到信號,它們將退出循環(huán),并打印消息確認(rèn)已被取消。time.Sleep(time.Millisecond * 5) 防止 goroutine 占用過多 CPU 資源,減少了空轉(zhuǎn)等待的開銷。

所以你會看到在TestCancel中使用cancel_1時(shí)只有一個 goroutine 能夠收到取消信號,原因是因?yàn)?cancel_1(cancelChan) 只向通道發(fā)送了一個信號,而不是關(guān)閉通道,換成cancel_2就會廣播給所有等待通道的 goroutine。

7.context與任務(wù)取消

在 Go 語言中,context 包是處理任務(wù)取消、超時(shí)控制和跨 API 邊界傳遞請求范圍數(shù)據(jù)的強(qiáng)大工具,特別是在并發(fā)編程和網(wǎng)絡(luò)應(yīng)用中。context 提供了一種簡潔的機(jī)制來管理多個 goroutine 之間的協(xié)作,尤其是在需要取消任務(wù)或控制超時(shí)時(shí),它能夠讓程序高效響應(yīng)用戶請求或系統(tǒng)事件。

(九)經(jīng)典并發(fā)任務(wù)

(十)測試

(十一)反射和Unsafe

(十二)常見架構(gòu)模式的實(shí)現(xiàn)

(十三)常見任務(wù)

(十四)性能調(diào)優(yōu)

(十五)高可用性服務(wù)設(shè)計(jì)

柚子快報(bào)激活碼778899分享:快速學(xué)習(xí)GO語言總結(jié)

http://yzkb.51969.com/

精彩文章

評論可見,查看隱藏內(nèi)容

本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場。

轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。

本文鏈接:http://gantiao.com.cn/post/19453344.html

發(fā)布評論

您暫未設(shè)置收款碼

請?jiān)谥黝}配置——文章設(shè)置里上傳

掃描二維碼手機(jī)訪問

文章目錄