概述
Go 是一種簡單而有趣的語言,但是,與任何其他語言一樣,它也有一些陷阱……其中許多陷阱并不完全是 Go 的錯。如果您來自另一種語言,其中一些錯誤是自然陷阱。其他是由于錯誤的假設和缺少細節。
初學者:
- 不能将左大括号放在單獨的行上
- 未使用的變量
- 未使用的進口
- 短變量聲明隻能在函數内部使用
- 使用短變量聲明重新聲明變量
- 不能使用短變量聲明來設定字段值
- 意外變量陰影
- 不能使用“nil”來初始化沒有顯式類型的變量
- 使用“nil”切片和映射
- 地圖容量
- 字元串不能為“nil”
- 數組函數參數
- 切片和數組“範圍”子句中的意外值
- 切片和數組是一維的
- 通路不存在的映射鍵
- 字元串是不可變的
- 字元串和位元組片之間的轉換
- 字元串和索引運算符
- 字元串并不總是 UTF8 文本
- 字元串長度
- 多行切片/數組/映射文字中缺少逗号
- log.Fatal 和 log.Panic 不僅僅是日志
- 内置資料結構操作不同步
- “範圍”子句中字元串的疊代值
- 使用“for range”子句周遊地圖
- “switch”語句中的失敗行為
- 增量和減量
- 按位非運算符
- 運算符優先級差異
- 未導出的結構字段未編碼
- 帶有活動 Goroutines 的應用程式退出
- 目标接收器準備好後立即傳回到無緩沖通道
- 發送到關閉的頻道會導緻恐慌
- 使用“零”通道
- 帶有值接收器的方法不能更改原始值
中級初學者:
- 關閉 HTTP 響應正文
- 關閉 HTTP 連接配接
- JSON 編碼器添加換行符
- JSON 包轉義鍵和字元串值中的特殊 HTML 字元
- 将 JSON 數字解組為接口值
- 十六進制或其他非 UTF8 轉義序列無法使用 JSON 字元串值
- 比較結構、數組、切片和映射
- 從恐慌中恢複
- 更新和引用切片、數組和映射“for range”子句中的項值
- 切片中的“隐藏”資料
- 切片資料損壞
- “陳舊”切片
- 類型聲明和方法
- 打破“for switch”和“for select”代碼塊
- “for”語句中的疊代變量和閉包
- 延遲函數調用參數評估
- 延遲函數調用執行
- 失敗的類型斷言
- 阻塞的 Goroutines 和資源洩漏
- 不同零大小變量的相同位址
- iota 的首次使用并不總是從零開始
進階初學者:
- 在值執行個體上使用指針接收器方法
- 更新 map 值字段
- “nil”接口和“nil”接口值
- 堆棧和堆變量
- GOMAXPROCS、并發和并行
- 讀寫操作重新排序
- 搶先排程
Cgo(又名勇敢的初學者):
- 導入 C 和多行導入塊
- Import C 和 Cgo 注釋之間沒有空行
- 不能使用可變參數調用 C 函數
陷阱和常見錯誤
1.不能将左大括号放在單獨的行上
級别:初學者
在大多數使用大括号的其他語言中,您可以選擇放置它們的位置。圍棋不一樣。您可以感謝這種行為的自動分号注入(沒有前瞻)。是的,Go 确實有分号 :-)
失敗:
package main
import "fmt"
func main()
{ //error, can't have the opening brace on a separate line
fmt.Println("hello there!")
}
編譯錯誤:
/tmp/sandbox826898458/main.go:6:文法錯誤:意外的分号或換行符之前 {
正确方式:
package main
import "fmt"
func main() {
fmt.Println("works!")
}
2.未使用的變量
級别:初學者
如果您有一個未使用的變量,您的代碼将無法編譯。不過有一個例外。您必須使用在函數内部聲明的變量,但如果您有未使用的全局變量,也可以。有未使用的函數參數也是可以的。
如果您為未使用的變量配置設定新值,您的代碼仍将無法編譯。您需要以某種方式使用變量值來使編譯器滿意。
失敗:
package main
var gvar int //not an error
func main() {
var one int //error, unused variable
two := 2 //error, unused variable
var three int //error, even though it's assigned 3 on the next line
three = 3
func(unused string) {
fmt.Println("Unused arg. No compile error")
}("what?")
}
編譯錯誤:
/tmp/sandbox473116179/main.go:6: 一個已聲明但未使用 /tmp/sandbox473116179/main.go:7: 兩個已聲明但未使用 /tmp/sandbox473116179/main.go:8: 三個已聲明但未使用
正确方式:
package main
import "fmt"
func main() {
var one int
_ = one
two := 2
fmt.Println(two)
var three int
three = 3
one = three
var four int
four = four
}
另一種選擇是注釋掉或删除未使用的變量:-)
3.未使用的 import
級别:初學者
如果您在不使用任何導出函數、接口、結構或變量的情況下導入包,您的代碼将無法編譯。
如果您确實需要導入的包,可以使用空白辨別符_, 作為其包名,以避免編譯失敗。空白辨別符用于導入包的副作用。
失敗:
package main
import (
"fmt"
"log"
"time"
)
func main() {
}
編譯錯誤:
/tmp/sandbox627475386/main.go:4:導入但未使用:“fmt”/tmp/sandbox627475386/main.go:5:導入但未使用:“log”/tmp/sandbox627475386/main.go:6:導入而未使用:“時間”
正确方式:
package main
import (
_ "fmt"
"log"
"time"
)
var _ = log.Println
func main() {
_ = time.Now
}
另一種選擇是删除或注釋掉未使用的導入 :-) 該 goimports工具可以幫助您。
4.短變量聲明隻能在函數内部使用
級别:初學者
失敗:
package main
myvar := 1 //error
func main() {
}
編譯錯誤:
/tmp/sandbox265716165/main.go:3:函數體外的非聲明語句
正确方式:
package main
var myvar = 1
func main() {
}
5.使用短變量聲明重新聲明變量
級别:初學者
您不能在獨立語句中重新聲明變量,但在至少聲明一個新變量的多變量聲明中是允許的。
重新聲明的變量必須在同一個塊中,否則您最終會得到一個陰影變量。
失敗:
package main
func main() {
one := 0
one := 1 //error
}
編譯錯誤:
/tmp/sandbox706333626/main.go:5: := 左側沒有新變量
正确方式:
package main
func main() {
one := 0
one, two := 1,2
one,two = two,one
}
6.不能使用短變量聲明來設定字段值
級别:初學者
失敗:
package main
import (
"fmt"
)
type info struct {
result int
}
func work() (int,error) {
return 13,nil
}
func main() {
var data info
data.result, err := work() //error
fmt.Printf("info: %+v\n",data)
}
編譯錯誤:
prog.go:18: non-name data.result on left side of :=
使用臨時變量或預先聲明所有變量并使用标準指派運算符。
正确方式:
package main
import (
"fmt"
)
type info struct {
result int
}
func work() (int,error) {
return 13,nil
}
func main() {
var data info
var err error
data.result, err = work() //ok
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("info: %+v\n",data) //prints: info: {result:13}
}
7.變量覆寫(Variable Shadowing)
級别:初學者
簡短的變量聲明文法非常友善(特别是對于那些來自動态語言的語言),很容易将其視為正常指派操作。如果您在新代碼塊中犯了這個錯誤,則不會出現編譯器錯誤,但您的應用程式不會按照您的預期執行。
package main
import "fmt"
func main() {
x := 1
fmt.Println(x) //prints 1
{
fmt.Println(x) //prints 1
x := 2
fmt.Println(x) //prints 2
}
fmt.Println(x) //prints 1 (bad if you need 2)
}
即使對于經驗豐富的 Go 開發人員來說,這也是一個非常常見的陷阱。它很容易制作,而且很難被發現。
您可以使用該vet指令來查找其中的一些問題。預設情況下,vet不會執行任何陰影變量檢查。確定使用-shadow标志:go tool vet -shadow your_file.go
請注意,該vet指令不會報告所有陰影變量。用于go-nyet更積極的陰影變量檢測。
8.不能使用“nil”來初始化沒有顯式類型的變量
級别:初學者
“nil”辨別符可用作接口、函數、指針、映射、切片和通道的“零值”。如果您不指定變量類型,編譯器将無法編譯您的代碼,因為它無法猜測類型。
失敗:
package main
func main() {
var x = nil //error
_ = x
}
編譯錯誤:
/tmp/sandbox188239583/main.go:4: 使用無類型 nil
正确方式:
package main
func main() {
var x interface{} = nil
_ = x
}
9.使用“nil”切片和映射
級别:初學者
可以将元素添加到“nil”切片,但對 map 執行相同操作會産生運作時panic 。
正确:
package main
func main() {
var s []int
s = append(s,1)
}
失敗:
package main
func main() {
var m map[string]int
m["one"] = 1 //error
}
10.map 容量
級别:初學者
您可以在建立 map 時指定容量,但不能使用cap(map)函數計算容量。
失敗:
package main
func main() {
m := make(map[string]int,99)
cap(m) //error
}
編譯錯誤:
/tmp/sandbox326543983/main.go:5: cap 的無效參數 m (type map[string]int)
11.字元串不能為“nil”
級别:初學者
對于習慣于将“nil”辨別符配置設定給字元串變量的開發人員來說,這是一個陷阱。
失敗:
package main
func main() {
var x string = nil //error
if x == nil { //error
x = "default"
}
}
編譯錯誤:
/tmp/sandbox630560459/main.go:4:不能在指派中使用 nil 作為類型字元串 /tmp/sandbox630560459/main.go:6:無效操作:x == nil(不比對的類型字元串和 nil)
正确方式:
package main
func main() {
var x string //defaults to "" (zero value)
if x == "" {
x = "default"
}
}
12.數組函數參數:數組指針類型
級别:初學者
如果您是 C 或 C++ 開發人員,那麼您的數組就是指針。當您将數組傳遞給函數時,函數引用相同的記憶體位置,是以它們可以更新原始資料。Go 中的數組是值,是以當您将數組傳遞給函數時,函數會擷取原始數組資料的副本。如果您嘗試更新數組資料,這可能是個問題。
package main
import "fmt"
func main() {
x := [3]int{1,2,3}
func(arr [3]int) {
arr[0] = 7
fmt.Println(arr) //prints [7 2 3]
}(x)
fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])
}
如果您需要更新原始數組資料,請使用數組指針類型。
package main
import "fmt"
func main() {
x := [3]int{1,2,3}
func(arr *[3]int) {
(*arr)[0] = 7
fmt.Println(arr) //prints &[7 2 3]
}(&x)
fmt.Println(x) //prints [7 2 3]
}
另一種選擇是使用切片。即使您的函數獲得了切片變量的副本,它仍然引用原始資料。
package main
import "fmt"
func main() {
x := []int{1,2,3}
func(arr []int) {
arr[0] = 7
fmt.Println(arr) //prints [7 2 3]
}(x)
fmt.Println(x) //prints [7 2 3]
}
13.切片和數組“範圍”子句中的意外值
級别:初學者
如果您習慣了其他語言中的“for-in”或“foreach”語句,就會發生這種情況。Go 中的“範圍”子句是不同的。它生成兩個值:第一個值是項目索引,而第二個值是項目資料。
壞的:
package main
import "fmt"
func main() {
x := []string{"a","b","c"}
for v := range x {
fmt.Println(v) //prints 0, 1, 2
}
}
好的:
package main
import "fmt"
func main() {
x := []string{"a","b","c"}
for _, v := range x {
fmt.Println(v) //prints a, b, c
}
}
14.切片和數組是一維的
級别:初學者
看起來 Go 似乎支援多元數組和切片,但事實并非如此。但是,可以建立數組數組或切片切片。對于依賴動态多元數組的數值計算應用程式,它在性能和複雜性方面遠非理想。
您可以使用原始一維數組、“獨立”切片的切片和“共享資料”切片的切片來建構動态多元數組。
如果您使用原始一維數組,您需要在數組需要增長時負責索引、邊界檢查和記憶體重新配置設定。
使用“獨立”切片的切片建立動态多元數組是一個兩步過程。首先,您必須建立外部切片。然後,您必須配置設定每個内部切片。内部切片彼此獨立。您可以在不影響其他内部切片的情況下擴充和收縮它們。
package main
func main() {
x := 2
y := 4
table := make([][]int,x)
for i:= range table {
table[i] = make([]int,y)
}
}
使用“共享資料”切片建立動态多元數組是一個三步過程。首先,您必須建立将儲存原始資料的資料“容器”切片。然後,您建立外部切片。最後,通過重新切片原始資料切片來初始化每個内部切片。
package main
import "fmt"
func main() {
h, w := 2, 4
raw := make([]int,h*w)
for i := range raw {
raw[i] = i
}
fmt.Println(raw,&raw[4])
//prints: [0 1 2 3 4 5 6 7] <ptr_addr_x>
table := make([][]int,h)
for i:= range table {
table[i] = raw[i*w:i*w + w]
}
fmt.Println(table,&table[1][0])
//prints: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>
}
有一個針對多元數組和切片的規範/建議,但目前看來它是一個低優先級的功能。
15.通路不存在的映射鍵
級别:初學者
對于希望獲得“nil”辨別符的開發人員來說,這是一個陷阱(就像在其他語言中所做的那樣)。如果相應資料類型的“零值”為“nil”,則傳回值為“nil”,但對于其他資料類型則不同。檢查适當的“零值”可用于确定映射記錄是否存在,但它并不總是可靠的(例如,如果您有一個布爾映射,其中“零值”為假,您會怎麼做)。了解給定地圖記錄是否存在的最可靠方法是檢查地圖通路操作傳回的第二個值。
壞的:
package main
import "fmt"
func main() {
x := map[string]string{"one":"a","two":"","three":"c"}
if v := x["two"]; v == "" { //incorrect
fmt.Println("no entry")
}
}
好的:
package main
import "fmt"
func main() {
x := map[string]string{"one":"a","two":"","three":"c"}
if _,ok := x["two"]; !ok {
fmt.Println("no entry")
}
}
16.字元串是不可變的
級别:初學者
嘗試使用索引運算符更新字元串變量中的單個字元将導緻失敗。字元串是隻讀位元組切片(帶有一些額外的屬性)。如果确實需要更新字元串,則在必要時使用位元組切片而不是将其轉換為字元串類型。
失敗:
package main
import "fmt"
func main() {
x := "text"
x[0] = 'T'
fmt.Println(x)
}
編譯錯誤:
/tmp/sandbox305565531/main.go:7: 不能配置設定給 x[0]
正确方式:
package main
import "fmt"
func main() {
x := "text"
xbytes := []byte(x)
xbytes[0] = 'T'
fmt.Println(string(xbytes)) //prints Text
}
請注意,這實際上并不是更新文本字元串中字元的正确方法,因為給定的字元可以存儲在多個位元組中。如果您确實需要對文本字元串進行更新,請先将其轉換為符文切片。即使使用符文切片,單個字元也可能跨越多個符文,例如,如果您有帶有重音的字元,就會發生這種情況。“字元”的這種複雜和模棱兩可的性質是 Go 字元串被表示為位元組序列的原因。
17.字元串和位元組片之間的轉換
級别:初學者
當您将字元串轉換為位元組切片(反之亦然)時,您将獲得原始資料的完整副本。它不像其他語言中的強制轉換操作,也不像重新切片新切片變量指向原始位元組切片使用的相同底層數組的位置。
Go 确實對[]bytetostring和stringto[]byte轉換進行了一些優化,以避免額外的配置設定(對 todo 清單進行了更多優化)。
當[]byte鍵用于查找map[string]集合中的條目時,第一個優化避免了額外的配置設定:m[string(key)].
第二個優化避免了for range字元串轉換為[]byte:的子句中的額外配置設定for i,v := range []byte(str) {...}。
18.字元串和索引運算符
級别:初學者
字元串上的索引運算符傳回一個位元組值,而不是一個字元(就像在其他語言中所做的那樣)。
package main
import "fmt"
func main() {
x := "text"
fmt.Println(x[0]) //print 116
fmt.Printf("%T",x[0]) //prints uint8
}
如果您需要通路特定的字元串“字元”(unicode 代碼點/符文),請使用該for range子句。官方的“unicode/utf8”包和實驗性的utf8string包(golang.org/x/exp/utf8string)也很有用。utf8string 包包含一個友善的At()方法。将字元串轉換為一片符文也是一種選擇。
19.字元串并不總是 UTF8 文本
級别:初學者
字元串值不需要是 UTF8 文本。它們可以包含任意位元組。字元串是 UTF8 的唯一時間是使用字元串文字時。即使這樣,它們也可以使用轉義序列包含其他資料。
要知道您是否有 UTF8 文本字元串,請使用ValidString()“unicode/utf8”包中的函數。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
data1 := "ABC"
fmt.Println(utf8.ValidString(data1)) //prints: true
data2 := "A\xfeC"
fmt.Println(utf8.ValidString(data2)) //prints: false
}
20.字元串長度
級别:初學者
假設您是一名 python 開發人員,并且您有以下代碼:
data = u'♥'
print(len(data)) #prints: 1
當您将其轉換為類似的 Go 代碼片段時,您可能會感到驚訝。
package main
import "fmt"
func main() {
data := "♥"
fmt.Println(len(data)) //prints: 3
}
内置len()函數傳回位元組數,而不是像 Python 中的 unicode 字元串那樣傳回字元數。
要在 Go 中獲得相同的結果,請使用 RuneCountInString() “unicode/utf8”包中的函數。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
data := "♥"
fmt.Println(utf8.RuneCountInString(data)) //prints: 1
從技術上講,該RuneCountInString()函數不傳回字元數,因為單個字元可能跨越多個符文。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
data := "é"
fmt.Println(len(data)) //prints: 3
fmt.Println(utf8.RuneCountInString(data)) //prints: 2
}
21.多行切片、數組和映射文字中缺少逗号
級别:初學者
失敗:
package main
func main() {
x := []int{
1,
2 //error
}
_ = x
}
編譯錯誤:
/tmp/sandbox367520156/main.go:6:文法錯誤:複合文字中的換行符之前需要尾随逗号/tmp/sandbox367520156/main.go:8:函數體外的非聲明語句/tmp/sandbox367520156/main.go:9 :文法錯誤:意外}
正确方式:
package main
func main() {
x := []int{
1,
2,
}
x = x
y := []int{3,4,} //no error
y = y
}
如果在将聲明折疊為一行時留下尾随逗号,則不會出現編譯器錯誤。
22.log.Fatal 和 log.Panic 不僅僅是日志
級别:初學者
日志庫通常提供不同的日志級别。Fatal*()不像那些日志庫,如果你調用它的和Panic*()函數,Go 中的日志包不僅僅做日志。當您的應用調用這些函數時,Go 也會終止您的應用 :-)
package main
import "log"
func main() {
log.Fatalln("Fatal Level: log entry") //app exits here
log.Println("Normal Level: log entry")
}
23.内置資料結構操作不同步
級别:初學者
盡管 Go 有許多原生支援并發的特性,但并發安全資料集合并不是其中之一 :-) 確定資料集合更新是原子的是您的責任。Goroutines 和 channels 是實作這些原子操作的推薦方式,但如果它對您的應用程式有意義,您也可以利用“sync”包。
24.“範圍”子句中字元串的疊代值
級别:初學者
索引值(“範圍”操作傳回的第一個值)是第二個值中傳回的目前“字元”(unicode 代碼點/符文)的第一個位元組的索引。它不是目前“字元”的索引,就像在其他語言中所做的那樣。請注意,一個實際角色可能由多個符文表示。如果您需要使用字元,請務必檢視“規範”包 (golang.org/x/text/unicode/norm)。
帶有字元串變量的for range子句将嘗試将資料解釋為 UTF8 文本。對于它不了解的任何位元組序列,它将傳回 0xfffd 符文(又名 unicode 替換字元)而不是實際資料。如果您在字元串變量中存儲了任意(非 UTF8 文本)資料,請確定将它們轉換為位元組切片以按原樣擷取所有存儲的資料。
package main
import "fmt"
func main() {
data := "A\xfe\x02\xff\x04"
for _,v := range data {
fmt.Printf("%#x ",v)
}
//prints: 0x41 0xfffd 0x2 0xfffd 0x4 (not ok)
fmt.Println()
for _,v := range []byte(data) {
fmt.Printf("%#x ",v)
}
//prints: 0x41 0xfe 0x2 0xff 0x4 (good)
}
25.使用“for range”子句周遊map
級别:初學者
如果您希望項目按特定順序排列(例如,按鍵值排序),這是一個問題。每次地圖疊代都會産生不同的結果。Go 運作時嘗試将疊代順序随機化,但它并不總是成功,是以您可能會得到幾個相同的地圖疊代。連續看到 5 次相同的疊代不要感到驚訝。
package main
import "fmt"
func main() {
m := map[string]int{"one":1,"two":2,"three":3,"four":4}
for k,v := range m {
fmt.Println(k,v)
}
}
如果您使用 Go Playground ( https://play.golang.org/ ),您将始終獲得相同的結果,因為除非您進行更改,否則它不會重新編譯代碼。
26.“switch”語句中的失敗行為
級别:初學者
預設情況下,“switch”語句中的“case”塊會中斷。這與其他語言不同,其他語言的預設行為是進入下一個“case”塊。
package main
import "fmt"
func main() {
isSpace := func(ch byte) bool {
switch(ch) {
case ' ': //error
case '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) //prints true (ok)
fmt.Println(isSpace(' ')) //prints false (not ok)
}
您可以通過在每個“case”塊末尾使用“fallthrough”語句來強制“case”塊通過。您還可以重寫您的 switch 語句以在“case”塊中使用表達式清單。
package main
import "fmt"
func main() {
isSpace := func(ch byte) bool {
switch(ch) {
case ' ', '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) //prints true (ok)
fmt.Println(isSpace(' ')) //prints true (ok)
}
27.增量和減量
級别:初學者
許多語言都有遞增和遞減運算符。與其他語言不同,Go 不支援操作的字首版本。您也不能在表達式中使用這兩個運算符。
失敗:
package main
import "fmt"
func main() {
data := []int{1,2,3}
i := 0
++i //error
fmt.Println(data[i++]) //error
}
編譯錯誤:
/tmp/sandbox101231828/main.go:8:文法錯誤:意外 ++ /tmp/sandbox101231828/main.go:9:文法錯誤:意外 ++,預期:
正确方式:
package main
import "fmt"
func main() {
data := []int{1,2,3}
i := 0
i++
fmt.Println(data[i])
}
28.按位非運算符
級别:初學者
許多語言使用~一進制 NOT 運算符(也稱為按位補碼),但 Go 重用了 XOR 運算符 ( ^)。
失敗:
package main
import "fmt"
func main() {
fmt.Println(~2) //error
}
編譯錯誤:
/tmp/sandbox965529189/main.go:6:按位補碼運算符是 ^
正确方式:
package main
import "fmt"
func main() {
var d uint8 = 2
fmt.Printf("%08b\n",^d)
}
Go 仍然使用^XOR 運算符,這可能會讓一些人感到困惑。
如果您願意,您可以NOT 0x02用二進制 XOR 運算(例如)來表示一進制 NOT 運算(例如0x02 XOR 0xff)。這可以解釋為什麼^要重用來表示一進制 NOT 操作。
Go 還有一個特殊的“AND NOT”位運算符 ( &^),這增加了 NOT 運算符的混淆。它看起來像是一個A AND (NOT B)不需要括号就可以支援的特殊功能/hack。
package main
import "fmt"
func main() {
var a uint8 = 0x82
var b uint8 = 0x02
fmt.Printf("%08b [A]\n",a)
fmt.Printf("%08b [B]\n",b)
fmt.Printf("%08b (NOT B)\n",^b)
fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n",b,0xff,b ^ 0xff)
fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n",a,b,a ^ b)
fmt.Printf("%08b & %08b = %08b [A AND B]\n",a,b,a & b)
fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n",a,b,a &^ b)
fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n",a,b,a & (^b))
}
29.運算符優先級差異
級别:初學者
除了“bit clear”操作符(&^)之外,Go 有一組标準操作符,許多其他語言都共享這些操作符。但是,運算符的優先級并不總是相同的。
package main
import "fmt"
func main() {
fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n",0x2 & 0x2 + 0x4)
//prints: 0x2 & 0x2 + 0x4 -> 0x6
//Go: (0x2 & 0x2) + 0x4
//C++: 0x2 & (0x2 + 0x4) -> 0x2
fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n",0x2 + 0x2 << 0x1)
//prints: 0x2 + 0x2 << 0x1 -> 0x6
//Go: 0x2 + (0x2 << 0x1)
//C++: (0x2 + 0x2) << 0x1 -> 0x8
fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n",0xf | 0x2 ^ 0x2)
//prints: 0xf | 0x2 ^ 0x2 -> 0xd
//Go: (0xf | 0x2) ^ 0x2
//C++: 0xf | (0x2 ^ 0x2) -> 0xf
}
30.未導出的結構字段未編碼
級别:初學者
以小寫字母開頭的結構字段不會被(json、xml、gob 等)編碼,是以當您解碼結構時,您最終會在那些未導出的字段中得到零值。
package main
import (
"fmt"
"encoding/json"
)
type MyData struct {
One int
two string
}
func main() {
in := MyData{1,"two"}
fmt.Printf("%#v\n",in) //prints main.MyData{One:1, two:"two"}
encoded,_ := json.Marshal(in)
fmt.Println(string(encoded)) //prints {"One":1}
var out MyData
json.Unmarshal(encoded,&out)
fmt.Printf("%#v\n",out) //prints main.MyData{One:1, two:""}
}
31.帶有活動 Goroutines 的應用程式退出
級别:初學者
該應用程式不會等待您所有的 goroutine 完成。對于一般初學者來說,這是一個常見的錯誤。每個人都從某個地方開始,是以犯新手錯誤并不可恥:-)
package main
import (
"fmt"
"time"
)
func main() {
workerCount := 2
for i := 0; i < workerCount; i++ {
go doit(i)
}
time.Sleep(1 * time.Second)
fmt.Println("all done!")
}
func doit(workerId int) {
fmt.Printf("[%v] is running\n",workerId)
time.Sleep(3 * time.Second)
fmt.Printf("[%v] is done\n",workerId)
}
你會看到的:
[0] 正在運作
[1] 正在運作
全部完成!
最常見的解決方案之一是使用“WaitGroup”變量。它将允許主 goroutine 等待,直到所有工作 goroutine 完成。如果您的應用程式有長時間運作的帶有消息處理循環的從業人員,您還需要一種方法來通知這些 goroutine 是時候退出了。您可以向每個從業人員發送“殺死”消息。另一種選擇是關閉所有勞工正在接收的頻道。這是一次向所有 goroutine 發出信号的簡單方法。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i,done,wg)
}
close(done)
wg.Wait()
fmt.Println("all done!")
}
func doit(workerId int,done <-chan struct{},wg sync.WaitGroup) {
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()
<- done
fmt.Printf("[%v] is done\n",workerId)
}
如果你運作這個應用程式,你會看到:
[0] 正在運作
[0] 已完成
[1] 正在運作
[1] 已完成
看起來從業人員在主 goroutine 退出之前就完成了。但是! 您還會看到:
緻命錯誤:所有 goroutine 都處于休眠狀态 - 死鎖!
那不是很好:-) 發生了什麼事?為什麼會出現死鎖?勞工們離開了,他們被處決了wg.Done()。該應用程式應該可以工作。
發生死鎖是因為每個從業人員都獲得了原始“WaitGroup”變量的副本。當從業人員執行wg.Done()時,它對主 goroutine 中的“WaitGroup”變量沒有影響。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
wq := make(chan interface{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i,wq,done,&wg)
}
for i := 0; i < workerCount; i++ {
wq <- i
}
close(done)
wg.Wait()
fmt.Println("all done!")
}
func doit(workerId int, wq <-chan interface{},done <-chan struct{},wg *sync.WaitGroup) {
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()
for {
select {
case m := <- wq:
fmt.Printf("[%v] m => %v\n",workerId,m)
case <- done:
fmt.Printf("[%v] is done\n",workerId)
return
}
}
}
現在它按預期工作:-)
32.目标接收器準備好後立即傳回到無緩沖通道
級别:初學者
在收件人處理您的消息之前,不會阻止發件人。根據您運作代碼的機器,接收者 goroutine 可能有也可能沒有足夠的時間在發送者繼續執行之前處理消息。
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
for m := range ch {
fmt.Println("processed:",m)
}
}()
ch <- "cmd.1"
ch <- "cmd.2" //won't be processed
}
33.發送到關閉的頻道會導緻panic
級别:初學者
從封閉的管道接收是安全的。接收語句中的ok傳回值将被設定為false表示沒有接收到資料。如果您從緩沖通道接收,您将首先擷取緩沖資料,一旦它為空,ok傳回值将為false.
将資料發送到關閉的通道會導緻恐慌。這是一個記錄在案的行為,但對于可能期望發送行為類似于接收行為的新 Go 開發人員來說,這并不是很直覺。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- (idx + 1) * 2
}(i)
}
//get the first result
fmt.Println(<-ch)
close(ch) //not ok (you still have other senders)
//do other work
time.Sleep(2 * time.Second)
}
根據您的應用程式,修複會有所不同。這可能是一個小的代碼更改,或者可能需要更改您的應用程式設計。無論哪種方式,您都需要確定您的應用程式不會嘗試将資料發送到關閉的通道。
錯誤示例可以通過使用特殊的取消通道來向剩餘的從業人員發出不再需要他們的結果的信号來修複。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
done := make(chan struct{})
for i := 0; i < 3; i++ {
go func(idx int) {
select {
case ch <- (idx + 1) * 2: fmt.Println(idx,"sent result")
case <- done: fmt.Println(idx,"exiting")
}
}(i)
}
//get first result
fmt.Println("result:",<-ch)
close(done)
//do other work
time.Sleep(3 * time.Second)
}
34.使用“零”通道
級别:初學者
nil永遠在通道塊上發送和接收操作。這是一個有據可查的行為,但對于新的 Go 開發人員來說可能是一個驚喜。
package main
import (
"fmt"
"time"
)
func main() {
var ch chan int
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- (idx + 1) * 2
}(i)
}
//get first result
fmt.Println("result:",<-ch)
//do other work
time.Sleep(2 * time.Second)
}
如果您運作代碼,您将看到如下運作時錯誤:fatal error: all goroutines are asleep - deadlock!
此行為可用作在語句中動态啟用和禁用case塊的一種方式。select
package main
import "fmt"
import "time"
func main() {
inch := make(chan int)
outch := make(chan int)
go func() {
var in <- chan int = inch
var out chan <- int
var val int
for {
select {
case out <- val:
out = nil
in = inch
case val = <- in:
out = outch
in = nil
}
}
}()
go func() {
for r := range outch {
fmt.Println("result:",r)
}
}()
time.Sleep(0)
inch <- 1
inch <- 2
time.Sleep(3 * time.Second)
}
35.帶有值接收器的方法不能更改原始值
級别:初學者
方法接收器就像正常函數參數。如果它被聲明為一個值,那麼您的函數/方法将獲得您的接收器參數的副本。這意味着對接收器進行更改不會影響原始值,除非您的接收器是映射或切片變量,并且您正在更新集合中的項目或者您在接收器中更新的字段是指針。
package main
import "fmt"
type data struct {
num int
key *string
items map[string]bool
}
func (this *data) pmethod() {
this.num = 7
}
func (this data) vmethod() {
this.num = 8
*this.key = "v.key"
this.items["vmethod"] = true
}
func main() {
key := "key.1"
d := data{1,&key,make(map[string]bool)}
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
//prints num=1 key=key.1 items=map[]
d.pmethod()
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
//prints num=7 key=key.1 items=map[]
d.vmethod()
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
//prints num=7 key=v.key items=map[vmethod:true]
}
36.關閉 HTTP 響應正文
等級:中級
當您使用标準 http 庫送出請求時,您會得到一個 http 響應變量。如果您不閱讀響應正文,您仍然需要關閉它。請注意,您也必須為空響應執行此操作。這很容易忘記,尤其是對于新的 Go 開發人員。
一些新的 Go 開發人員确實嘗試關閉響應體,但他們做錯了地方。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close()//not ok
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
此代碼适用于成功的請求,但如果 http 請求失敗,resp變量可能是nil,這将導緻運作時 panic。
關閉響應正文的最常見原因是defer在 http 響應錯誤檢查之後使用調用。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close() //ok, most of the time :-)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
大多數情況下,當您的 http 請求失敗時,resp變量将為. 但是,當您遇到重定向失敗時,兩個變量都将是. 這意味着您仍然可能會出現洩漏。nilerrnon-nilnon-nil
non-nil您可以通過在 http 響應錯誤處理塊中添加關閉響應主體的調用來修複此洩漏。另一種選擇是使用一次defer調用來關閉所有失敗和成功請求的響應主體。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
原始 resp.Body.Close() 實作中,還讀取并丢棄剩餘的響應正文資料。這確定了如果啟用了 keepalive http 連接配接行為,則可以将 http 連接配接重新用于另一個請求。最新的 http 用戶端行為不同。現在,您有責任讀取并丢棄剩餘的響應資料。如果你不這樣做,http 連接配接可能會被關閉而不是被重用。
如果重用 http 連接配接對您的應用程式很重要,您可能需要在響應處理邏輯的末尾添加類似這樣的内容:
_, err = io.Copy(ioutil.Discard, resp.Body)
如果您不立即閱讀整個響應正文,則有必要這樣做,如果您使用如下代碼處理 json API 響應,則可能會發生這種情況:
json.NewDecoder(resp.Body).Decode(&data)
37.關閉 HTTP 連接配接
等級:中級
一些 HTTP 伺服器保持網絡連接配接打開一段時間(基于 HTTP 1.1 規範和伺服器“保持活動”配置)。預設情況下,标準 http 庫僅在目标 HTTP 伺服器請求時才會關閉網絡連接配接。這意味着您的應用程式可能會在某些情況下用完套接字/檔案描述符。
您可以通過将Close請求變量中的字段設定為 來要求 http 庫在請求完成後關閉連接配接true。
另一種選擇是添加Connection請求标頭并将其設定為close. 目标 HTTP 伺服器也應該使用Connection: close标頭響應。當 http 庫看到這個響應頭時,它也會關閉連接配接。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
req, err := http.NewRequest("GET","http://golang.org",nil)
if err != nil {
fmt.Println(err)
return
}
req.Close = true
//or do this:
//req.Header.Add("Connection", "close")
resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(string(body)))
}
您還可以全局禁用 http 連接配接重用。您需要為其建立自定義 http 傳輸配置。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
tr := &http.Transport{DisableKeepAlives: true}
client := &http.Client{Transport: tr}
resp, err := client.Get("http://golang.org")
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(string(body)))
}
如果您向同一個 HTTP 伺服器發送大量請求,則可以保持網絡連接配接打開。但是,如果您的應用程式在短時間内向許多不同的 HTTP 伺服器發送一個或兩個請求,最好在您的應用程式收到響應後立即關閉網絡連接配接。增加打開檔案的限制也可能是個好主意。但是,正确的解決方案取決于您的應用程式。
38.JSON 編碼器添加換行符
等級:中級
當您發現測試失敗是因為您沒有獲得預期值時,您正在為 JSON 編碼函數編寫測試。發生了什麼?如果您使用的是 JSON 編碼器對象,那麼您将在編碼的 JSON 對象的末尾獲得一個額外的換行符。
package main
import (
"fmt"
"encoding/json"
"bytes"
)
func main() {
data := map[string]int{"key": 1}
var b bytes.Buffer
json.NewEncoder(&b).Encode(data)
raw,_ := json.Marshal(data)
if b.String() == string(raw) {
fmt.Println("same encoded data")
} else {
fmt.Printf("'%s' != '%s'\n",raw,b.String())
//prints:
//'{"key":1}' != '{"key":1}\n'
}
}
JSON Encoder 對象專為流式傳輸而設計。使用 JSON 進行流式傳輸通常意味着以換行符分隔的 JSON 對象,這就是 Encode 方法添加換行符的原因。這是記錄在案的行為,但通常被忽視或遺忘。
39.JSON 包轉義鍵和字元串值中的特殊 HTML 字元
等級:中級
這是一個記錄在案的行為,但您必須仔細閱讀所有 JSON 封包檔才能了解它。SetEscapeHTML方法描述讨論了 and、小于和大于字元的預設編碼行為。
出于多種原因,這是 Go 團隊的一個非常不幸的設計決定。首先,您不能為json.Marshal調用禁用此行為。其次,這是一個實施得很糟糕的安全功能,因為它假定進行 HTML 編碼足以防止所有 Web 應用程式中的 XSS 漏洞。有很多不同的上下文可以使用資料,每個上下文都需要自己的編碼方法。最後,它很糟糕,因為它假定 JSON 的主要用例是網頁,預設情況下會破壞配置庫和 REST/HTTP API。
package main
import (
"fmt"
"encoding/json"
"bytes"
)
func main() {
data := "x < y"
raw,_ := json.Marshal(data)
fmt.Println(string(raw))
//prints: "x \u003c y" <- probably not what you expected
var b1 bytes.Buffer
json.NewEncoder(&b1).Encode(data)
fmt.Println(b1.String())
//prints: "x \u003c y" <- probably not what you expected
var b2 bytes.Buffer
enc := json.NewEncoder(&b2)
enc.SetEscapeHTML(false)
enc.Encode(data)
fmt.Println(b2.String())
//prints: "x < y" <- looks better
}
給 Go 團隊的建議……讓它成為一個選擇加入。
40.将 JSON 數字解組為接口值
等級:中級
預設情況下,float64當您将 JSON 資料解碼/解組到接口中時,Go 将 JSON 中的數值視為數字。這意味着以下代碼将因 panic 而失敗:
package main
import (
"encoding/json"
"fmt"
)
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
fmt.Println("error:", err)
return
}
var status = result["status"].(int) //error
fmt.Println("status value:",status)
}
運作時panic:
panic:接口轉換:接口是float64,而不是int
如果您嘗試解碼的 JSON 值是一個整數,那麼您有多個選項。
選項一:按原樣使用浮點值:-)
選項二:将浮點值轉換為您需要的整數類型。
package main
import (
"encoding/json"
"fmt"
)
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
fmt.Println("error:", err)
return
}
var status = uint64(result["status"].(float64)) //ok
fmt.Println("status value:",status)
}
選項三:使用一種類型來解組 JSON,并告訴它使用接口類型Decoder來表示 JSON 數字。Number
package main
import (
"encoding/json"
"bytes"
"fmt"
)
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
var decoder = json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&result); err != nil {
fmt.Println("error:", err)
return
}
var status,_ = result["status"].(json.Number).Int64() //ok
fmt.Println("status value:",status)
}
您可以使用Number值的字元串表示形式将其解組為不同的數字類型:
package main
import (
"encoding/json"
"bytes"
"fmt"
)
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
var decoder = json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&result); err != nil {
fmt.Println("error:", err)
return
}
var status uint64
if err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status); err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("status value:",status)
}
選項四:使用struct将您的數值映射到您需要的數值類型的類型。
package main
import (
"encoding/json"
"bytes"
"fmt"
)
func main() {
var data = []byte(`{"status": 200}`)
var result struct {
Status uint64 `json:"status"`
}
if err := json.NewDecoder(bytes.NewReader(data)).Decode(&result); err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("result => %+v",result)
//prints: result => {Status:200}
}
選項五:如果您需要延遲值解碼,請使用struct将您的數值映射到類型的 a。json.RawMessage
如果您必須在字段類型或結構可能發生變化的情況下執行條件 JSON 字段解碼,則此選項很有用。
package main
import (
"encoding/json"
"bytes"
"fmt"
)
func main() {
records := [][]byte{
[]byte(`{"status": 200, "tag":"one"}`),
[]byte(`{"status":"ok", "tag":"two"}`),
}
for idx, record := range records {
var result struct {
StatusCode uint64
StatusName string
Status json.RawMessage `json:"status"`
Tag string `json:"tag"`
}
if err := json.NewDecoder(bytes.NewReader(record)).Decode(&result); err != nil {
fmt.Println("error:", err)
return
}
var sstatus string
if err := json.Unmarshal(result.Status, &sstatus); err == nil {
result.StatusName = sstatus
}
var nstatus uint64
if err := json.Unmarshal(result.Status, &nstatus); err == nil {
result.StatusCode = nstatus
}
fmt.Printf("[%v] result => %+v\n",idx,result)
}
}
41.十六進制或其他非 UTF8 轉義序列無法使用 JSON 字元串值
等級:中級
Go 期望字元串值是 UTF8 編碼的。這意味着您的 JSON 字元串中不能有任意十六進制轉義的二進制資料(并且您還必須轉義反斜杠字元)。這确實是 Go 繼承的 JSON 陷阱,但它在 Go 應用程式中經常發生,是以無論如何都要提及它。
package main
import (
"fmt"
"encoding/json"
)
type config struct {
Data string `json:"data"`
}
func main() {
raw := []byte(`{"data":"\xc2"}`)
var decoded config
if err := json.Unmarshal(raw, &decoded); err != nil {
fmt.Println(err)
//prints: invalid character 'x' in string escape code
}
}
如果 Go 看到一個十六進制轉義序列,Unmarshal/Decode 調用将失敗。如果您确實需要在字元串中使用反斜杠,請確定使用另一個反斜杠對其進行轉義。如果您想使用十六進制編碼的二進制資料,您可以轉義反斜杠,然後使用 JSON 字元串中的解碼資料進行自己的十六進制轉義。
package main
import (
"fmt"
"encoding/json"
)
type config struct {
Data string `json:"data"`
}
func main() {
raw := []byte(`{"data":"\\xc2"}`)
var decoded config
json.Unmarshal(raw, &decoded)
fmt.Printf("%#v",decoded) //prints: main.config{Data:"\\xc2"}
//todo: do your own hex escape decoding for decoded.Data
}
另一種選擇是在 JSON 對象中使用位元組數組/切片資料類型,但二進制資料必須采用 base64 編碼。
package main
import (
"fmt"
"encoding/json"
)
type config struct {
Data []byte `json:"data"`
}
func main() {
raw := []byte(`{"data":"wg=="}`)
var decoded config
if err := json.Unmarshal(raw, &decoded); err != nil {
fmt.Println(err)
}
fmt.Printf("%#v",decoded) //prints: main.config{Data:[]uint8{0xc2}}
}
其他需要注意的是 Unicode 替換字元 (U+FFFD)。Go 将使用替換字元而不是無效的 UTF8,是以 Unmarshal/Decode 調用不會失敗,但你得到的字元串值可能不是你所期望的。
42.比較結構、數組、切片和映射
等級:中級
==如果每個結構字段都可以與相等運算符進行比較,則可以使用相等運算符 ,來比較結構變量。
package main
import "fmt"
type data struct {
num int
fp float32
complex complex64
str string
char rune
yes bool
events <-chan string
handler interface{}
ref *byte
raw [10]byte
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2) //prints: v1 == v2: true
}
如果任何結構字段不可比較,則使用相等運算符将導緻編譯時錯誤。請注意,隻有當它們的資料項具有可比性時,數組才具有可比性。
package main
import "fmt"
type data struct {
num int //ok
checks [10]func() bool //not comparable
doit func() bool //not comparable
m map[string] string //not comparable
bytes []byte //not comparable
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2)
}
Go 确實提供了許多輔助函數來比較無法使用比較運算符進行比較的變量。
最通用的解決方案是使用DeepEqual()反射包中的函數。
package main
import (
"fmt"
"reflect"
)
type data struct {
num int //ok
checks [10]func() bool //not comparable
doit func() bool //not comparable
m map[string] string //not comparable
bytes []byte //not comparable
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2)) //prints: v1 == v2: true
m1 := map[string]string{"one": "a","two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true
}
除了速度很慢(這可能會或可能不會對您的應用程式造成破壞)之外,DeepEqual()它也有自己的問題。
package main
import (
"fmt"
"reflect"
)
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false
}
DeepEqual()不認為空切片等于“nil”切片。此行為與您使用該bytes.Equal()函數獲得的行為不同。bytes.Equal()認為“nil”和空切片是相等的。
package main
import (
"fmt"
"bytes"
)
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) //prints: b1 == b2: true
}
DeepEqual()比較切片并不總是完美的。
package main
import (
"fmt"
"reflect"
"encoding/json"
)
func main() {
var str string = "one"
var in interface{} = "one"
fmt.Println("str == in:",str == in,reflect.DeepEqual(str, in))
//prints: str == in: true true
v1 := []string{"one","two"}
v2 := []interface{}{"one","two"}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2))
//prints: v1 == v2: false (not ok)
data := map[string]interface{}{
"code": 200,
"value": []string{"one","two"},
}
encoded, _ := json.Marshal(data)
var decoded map[string]interface{}
json.Unmarshal(encoded, &decoded)
fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded))
//prints: data == decoded: false (not ok)
}
如果您的位元組切片(或字元串)包含文本資料,當您需要以不區分大小寫的方式(在使用 、 或 之前)比較值時,您可能想使用或來自“位元組”和“字元串”ToUpper()包。它适用于英文文本,但不适用于許多其他語言的文本。并且應該被使用。ToLower()==bytes.Equal()bytes.Compare()strings.EqualFold()bytes.EqualFold()
如果您的位元組切片包含需要針對使用者提供的資料進行驗證的機密(例如,加密哈希、令牌等),請不要使用reflect.DeepEqual(), bytes.Equal(),或者bytes.Compare()因為這些函數會使您的應用程式容易受到計時攻擊。為避免洩漏計時資訊,請使用“crypto/subtle”包中的函數(例如,subtle.ConstantTimeCompare())。
43.從 panic 中恢複( recover )
等級:中級
該recover()函數可用于捕獲/攔截panic。調用 recover()隻有在 defer 延遲函數中完成時才會起作用。
不正确:
package main
import "fmt"
func main() {
recover() //doesn't do anything
panic("not good")
recover() //won't be executed :)
fmt.Println("ok")
}
正确方式:
package main
import "fmt"
func main() {
defer func() {
fmt.Println("recovered:",recover())
}()
panic("not good")
}
調用recover()僅在您的延遲函數中直接調用時才有效。
失敗:
package main
import "fmt"
func doRecover() {
fmt.Println("recovered =>",recover()) //prints: recovered => <nil>
}
func main() {
defer func() {
doRecover() //panic is not recovered
}()
panic("not good")
}
44.在切片、數組和映射“範圍”子句中更新和引用項目值
等級:中級
“範圍”子句中生成的資料值是實際集合元素的副本。它們不是對原始項目的引用。這意味着更新值不會更改原始資料。這也意味着擷取值的位址不會為您提供指向原始資料的指針。
package main
import "fmt"
func main() {
data := []int{1,2,3}
for _,v := range data {
v *= 10 //original item is not changed
}
fmt.Println("data:",data) //prints data: [1 2 3]
}
如果您需要更新原始集合記錄值,請使用索引運算符來通路資料。
package main
import "fmt"
func main() {
data := []int{1,2,3}
for i,_ := range data {
data[i] *= 10
}
fmt.Println("data:",data) //prints data: [10 20 30]
}
如果您的集合包含指針值,則規則略有不同。如果您希望原始記錄指向另一個值,您仍然需要使用索引運算符,但您可以使用“for range”子句中的第二個值更新存儲在目标位置的資料。
package main
import "fmt"
func main() {
data := []*struct{num int} {{1},{2},{3}}
for _,v := range data {
v.num *= 10
}
fmt.Println(data[0],data[1],data[2]) //prints &{10} &{20} &{30}
}
45.切片中的“隐藏”資料
等級:中級
重新切片切片時,新切片将引用原始切片的數組。如果您忘記了這種行為,如果您的應用程式配置設定大型臨時切片從它們建立新切片以引用原始資料的小部分,則可能會導緻意外的記憶體使用。
package main
import "fmt"
func get() []byte {
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
return raw[:3]
}
func main() {
data := get()
fmt.Println(len(data),cap(data),&data[0]) //prints: 3 10000 <byte_addr_x>
}
為避免此陷阱,請確定從臨時切片中複制所需的資料(而不是重新切片)。
package main
import "fmt"
func get() []byte {
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
res := make([]byte,3)
copy(res,raw[:3])
return res
}
func main() {
data := get()
fmt.Println(len(data),cap(data),&data[0]) //prints: 3 3 <byte_addr_y>
}
46.切片資料覆寫
等級:中級
假設您需要重寫路徑(存儲在切片中)。您重新切片路徑以引用修改第一個檔案夾名稱的每個目錄,然後組合名稱以建立新路徑。
package main
import (
"fmt"
"bytes"
)
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB (not ok)
fmt.Println("new path =>",string(path))
}
它沒有像你預期的那樣工作。而不是“AAAAsuffix/BBBBBBBBB”,你最終得到的是“AAAAsuffix/uffixBBBB”。發生這種情況是因為兩個目錄切片都引用了原始路徑切片中相同的底層數組資料。這意味着原始路徑也被修改了。根據您的應用程式,這也可能是一個問題。
這個問題可以通過配置設定新切片和複制你需要的資料來解決。另一種選擇是使用完整的切片表達式。
package main
import (
"fmt"
"bytes"
)
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex:sepIndex] //full slice expression
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB (ok now)
fmt.Println("new path =>",string(path))
}
完整切片表達式中的額外參數控制新切片的容量。現在追加到該切片将觸發新的緩沖區配置設定,而不是覆寫第二個切片中的資料。
47.切片中的“舊資料”
等級:中級
多個切片可以引用相同的資料。例如,當您從現有切片建立新切片時,可能會發生這種情況。如果您的應用程式依賴此行為來正常運作,那麼您需要擔心“陳舊”切片。
在某些時候,當原始數組無法容納更多新資料時,将資料添加到其中一個切片将導緻新的數組配置設定。現在其他切片将指向舊數組(帶有舊資料)。
import "fmt"
func main() {
s1 := []int{1,2,3}
fmt.Println(len(s1),cap(s1),s1) //prints 3 3 [1 2 3]
s2 := s1[1:]
fmt.Println(len(s2),cap(s2),s2) //prints 2 2 [2 3]
for i := range s2 { s2[i] += 20 }
//still referencing the same array
fmt.Println(s1) //prints [1 22 23]
fmt.Println(s2) //prints [22 23]
s2 = append(s2,4)
for i := range s2 { s2[i] += 10 }
//s1 is now "stale"
fmt.Println(s1) //prints [1 22 23]
fmt.Println(s2) //prints [32 33 14]
}
48.類型聲明和方法
等級:中級
當您通過從現有(非接口)類型定義新類型來建立類型聲明時,您不會繼承為該現有類型定義的方法。
失敗:
package main
import "sync"
type myMutex sync.Mutex
func main() {
var mtx myMutex
mtx.Lock() //error
mtx.Unlock() //error
}
編譯錯誤:
/tmp/sandbox106401185/main.go:9:mtx.Lock 未定義(myMutex 類型沒有字段或方法 Lock) /tmp/sandbox106401185/main.go:10:mtx.Unlock 未定義(myMutex 類型沒有字段或方法 Unlock)
如果您确實需要原始類型的方法,您可以定義一個新的結構類型,将原始類型嵌入為匿名字段。
正确方式:
package main
import "sync"
type myLocker struct {
sync.Mutex
}
func main() {
var lock myLocker
lock.Lock() //ok
lock.Unlock() //ok
}
接口類型聲明也保留了它們的方法集。
正确方式:
package main
import "sync"
type myLocker sync.Locker
func main() {
var lock myLocker = new(sync.Mutex)
lock.Lock() //ok
lock.Unlock() //ok
}
49. 從“for switch”和“for select”代碼塊中 break
等級:中級
沒有标簽的“break”語句隻會讓你脫離内部 switch/select 塊。如果使用“return”語句不是一個選項,那麼為外部循環定義一個标簽是下一個最好的事情。
package main
import "fmt"
func main() {
loop:
for {
switch {
case true:
fmt.Println("breaking out...")
break loop
}
}
fmt.Println("out!")
}
“goto”語句也可以解決問題......
50.“for”語句中的疊代變量和閉包
等級:中級
這是 Go 中最常見的問題。for語句中的疊代變量在每次疊代中被重用。這意味着在for循環中建立的每個閉包(又名函數字面量)都将引用相同的變量(并且它們将在這些 goroutine 開始執行時擷取該變量的值)。
不正确:
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func() {
fmt.Println(v)
}()
}
time.Sleep(3 * time.Second)
//goroutines print: three, three, three
}
最簡單的解決方案(不需要對 goroutine 進行任何更改)是将目前疊代變量值儲存在for循環塊内的局部變量中。
正确方式:
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
vcopy := v // 循環體内局部變量
go func() {
fmt.Println(vcopy)
}()
}
time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
另一種解決方案是将目前疊代變量作為參數傳遞給匿名 goroutine。
正确方式:
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func(in string) {
fmt.Println(in)
}(v)
}
time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}這是一個稍微複雜一點的陷阱版本。
不正确:
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []field{{"one"},{"two"},{"three"}}
for _,v := range data {
go v.print()
}
time.Sleep(3 * time.Second)
//goroutines print: three, three, three
}
正确方式:
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []field{{"one"},{"two"},{"three"}}
for _,v := range data {
v := v
go v.print()
}
time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
你認為當你運作這段代碼時你會看到什麼(為什麼)?
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []*field{{"one"},{"two"},{"three"}}
for _,v := range data {
go v.print()
}
time.Sleep(3 * time.Second)
}
51.延遲函數調用參數評估
等級:中級
延遲函數調用的參數是在評估defer語句時評估的(而不是在函數實際執行時)。當您推遲方法調用時,同樣的規則也适用。結構值也與顯式方法參數和封閉變量一起儲存。
package main
import "fmt"
func main() {
var i int = 1
defer fmt.Println("result =>",func() int { return i * 2 }())
i++
//prints: result => 2 (not ok if you expected 4)
}
如果您有指針參數,則可以更改它們指向的值,因為在defer評估語句時隻儲存指針。
package main
import (
"fmt"
)
func main() {
i := 1
defer func (in *int) { fmt.Println("result =>", *in) }(&i)
i = 2
//prints: result => 2
}
52.延遲函數調用執行
等級:中級
延遲調用在包含函數的末尾執行(并且以相反的順序),而不是在包含代碼塊的末尾。對于新的 Go 開發人員來說,将延遲代碼執行規則與變量作用域規則混淆是一個容易犯的錯誤。如果你有一個長時間運作的函數,它有一個for循環嘗試defer在每次疊代中清理資源調用,這可能會成為一個問題。
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
if len(os.Args) != 2 {
os.Exit(-1)
}
start, err := os.Stat(os.Args[1])
if err != nil || !start.IsDir(){
os.Exit(-1)
}
var targets []string
filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
targets = append(targets,fpath)
return nil
})
for _,target := range targets {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err) //prints error: too many open files
break
}
defer f.Close() //will not be closed at the end of this code block
//do something with the file...
}
}
解決問題的一種方法是将代碼塊包裝在函數中。
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
if len(os.Args) != 2 {
os.Exit(-1)
}
start, err := os.Stat(os.Args[1])
if err != nil || !start.IsDir(){
os.Exit(-1)
}
var targets []string
filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
targets = append(targets,fpath)
return nil
})
for _,target := range targets {
func() {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err)
return
}
defer f.Close() //ok
//do something with the file...
}()
}
}
另一種選擇是擺脫defer聲明:-)
53.失敗的類型斷言
等級:中級
失敗的類型斷言傳回斷言語句中使用的目标類型的“零值”。當它與可變陰影混合時,這可能會導緻意外行為。
不正确:
package main
import "fmt"
func main() {
var data interface{} = "great"
if data, ok := data.(int); ok {
fmt.Println("[is an int] value =>",data)
} else {
fmt.Println("[not an int] value =>",data)
//prints: [not an int] value => 0 (not "great")
}
}
正确方式:
package main
import "fmt"
func main() {
var data interface{} = "great"
if res, ok := data.(int); ok {
fmt.Println("[is an int] value =>",res)
} else {
fmt.Println("[not an int] value =>",data)
//prints: [not an int] value => great (as expected)
}
}
54.阻塞的 Goroutines 和資源洩漏
等級:中級
Rob Pike 在 2012 年 Google I/O 上的“Go Concurrency Patterns”演講中談到了一些基本的并發模式。從多個目标中擷取第一個結果就是其中之一。
func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
該函數為每個搜尋副本啟動一個 goroutine。每個 goroutine 将其搜尋結果發送到結果通道。傳回結果通道的第一個值。
其他 goroutine 的結果如何?goroutines 本身呢?
函數中的結果通道First()是無緩沖的。這意味着隻有第一個 goroutine 傳回。所有其他 goroutine 都被困在試圖發送它們的結果。這意味着如果您有多個副本,則每次調用都會洩漏資源。
為避免洩漏,您需要確定所有 goroutine 都退出。一種可能的解決方案是使用足夠大的緩沖結果通道來儲存所有結果。
func First(query string, replicas ...Search) Result {
c := make(chan Result,len(replicas))
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
另一種可能的解決方案是使用select帶有案例的語句default和可以儲存一個值的緩沖結果通道。該default案例確定即使結果通道無法接收消息,goroutines 也不會卡住。
func First(query string, replicas ...Search) Result {
c := make(chan Result,1)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
default:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
您還可以使用特殊的取消通道來中斷從業人員。
func First(query string, replicas ...Search) Result {
c := make(chan Result)
done := make(chan struct{})
defer close(done)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
case <- done:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
為什麼示範文稿包含這些錯誤?Rob Pike 隻是不想讓幻燈片複雜化。這是有道理的,但對于新的 Go 開發人員來說,這可能是一個問題,他們會按原樣使用代碼而不考慮它可能有問題。
55.不同零大小變量的相同位址
等級:中級
如果您有兩個不同的變量,它們不應該有不同的位址嗎?好吧,Go 不是這種情況 :-) 如果你有零大小的變量,它們可能在記憶體中共享完全相同的位址。
package main
import (
"fmt"
)
type data struct {
}
func main() {
a := &data{}
b := &data{}
if a == b {
fmt.Printf("same address - a=%p b=%p\n",a,b)
//prints: same address - a=0x1953e4 b=0x1953e4
}
}
56.iota 的首次使用并不總是從零開始
等級:中級
看起來辨別符iota就像一個增量運算符。你開始一個新的常量聲明,第一次使用iota你得到零,第二次使用它你得到一個,依此類推。但情況并非總是如此。
package main
import (
"fmt"
)
const (
azero = iota
aone = iota
)
const (
info = "processing"
bzero = iota
bone = iota
)
func main() {
fmt.Println(azero,aone) //prints: 0 1
fmt.Println(bzero,bone) //prints: 1 2
}
iota确實是常量聲明塊中目前行的索引運算符,是以如果第一次使用的不是iota常量聲明塊中的第一行,則初始值不會為零。
57.在值執行個體上使用指針接收器方法
等級:進階
隻要值是可尋址的,就可以對值調用指針接收器方法。換句話說,在某些情況下,您不需要該方法的值接收器版本。
不過,并非每個變量都是可尋址的。地圖元素不可尋址。通過接口引用的變量也是不可尋址的。
package main
import "fmt"
type data struct {
name string
}
func (p *data) print() {
fmt.Println("name:",p.name)
}
type printer interface {
print()
}
func main() {
d1 := data{"one"}
d1.print() //ok
var in printer = data{"two"} //error
in.print()
m := map[string]data {"x":data{"three"}}
m["x"].print() //error
}
編譯錯誤:
/tmp/sandbox017696142/main.go:21:不能在指派中使用資料文字(資料類型)作為類型列印機:資料沒有實作列印機(列印方法有指針接收器)
/tmp/sandbox017696142/main.go:25:不能調用m["x"] /tmp/sandbox017696142/main.go:25 上的指針方法:不能擷取 m["x"] 的位址
58.更新 map 值字段
等級:進階
如果您有結構值映射,則無法更新單個結構字段。
失敗:
package main
type data struct {
name string
}
func main() {
m := map[string]data {"x":{"one"}}
m["x"].name = "two" //error
}
編譯錯誤:
/tmp/sandbox380452744/main.go:9: 不能配置設定給 m["x"].name
它不起作用,因為地圖元素不可尋址。
對于新的 Go 開發者來說,更令人困惑的是 slice 元素是可尋址的。
package main
import "fmt"
type data struct {
name string
}
func main() {
s := []data {{"one"}}
s[0].name = "two" //ok
fmt.Println(s) //prints: [{two}]
}
請注意,不久前可以在其中一個 Go 編譯器 (gccgo) 中更新地圖元素字段,但該行為很快得到修複 :-) 它也被認為是 Go 1.3 的潛在功能。在那個時候支援它還不夠重要,是以它仍然在待辦事項清單上。
第一個解決方法是使用臨時變量。
package main
import "fmt"
type data struct {
name string
}
func main() {
m := map[string]data {"x":{"one"}}
r := m["x"]
r.name = "two"
m["x"] = r
fmt.Printf("%v",m) //prints: map[x:{two}]
}
另一種解決方法是使用指針映射。
package main
import "fmt"
type data struct {
name string
}
func main() {
m := map[string]*data {"x":{"one"}}
m["x"].name = "two" //ok
fmt.Println(m["x"]) //prints: &{two}
}
順便說一句,當你運作這段代碼時會發生什麼?
package main
type data struct {
name string
}
func main() {
m := map[string]*data {"x":{"one"}}
m["z"].name = "what?" //???
}
59.“nil”接口和“nil”接口值
等級:進階
這是 Go 中第二個最常見的問題,因為接口不是指針,即使它們看起來像指針。隻有當它們的類型和值字段為“nil”時,接口變量才會為“nil”。
接口類型和值字段是根據用于建立相應接口變量的變量的類型和值填充的。當您嘗試檢查接口變量是否等于“nil”時,這可能會導緻意外行為。
package main
import "fmt"
func main() {
var data *byte
var in interface{}
fmt.Println(data,data == nil) //prints: <nil> true
fmt.Println(in,in == nil) //prints: <nil> true
in = data
fmt.Println(in,in == nil) //prints: <nil> false
//'data' is 'nil', but 'in' is not 'nil'
}
當您有一個傳回接口的函數時,請注意這個陷阱。
不正确:
package main
import "fmt"
func main() {
doit := func(arg int) interface{} {
var result *struct{} = nil
if(arg > 0) {
result = &struct{}{}
}
return result
}
if res := doit(-1); res != nil {
fmt.Println("good result:",res) //prints: good result: <nil>
//'res' is not 'nil', but its value is 'nil'
}
}
正确方式:
package main
import "fmt"
func main() {
doit := func(arg int) interface{} {
var result *struct{} = nil
if(arg > 0) {
result = &struct{}{}
} else {
return nil // return an explicit 'nil'
}
return result
}
if res := doit(-1); res != nil {
fmt.Println("good result:",res)
} else {
fmt.Println("bad result (res is nil)") //here as expected
}
}
60.堆棧和堆變量
等級:進階
您并不總是知道您的變量是配置設定在堆棧還是堆上。在 C++ 中,使用new運算符建立變量始終意味着您有一個堆變量。在 Go 中,即使使用new() or make()函數,編譯器也會決定配置設定變量的位置。編譯器根據變量的大小和“轉義分析”的結果選擇存儲變量的位置。這也意味着可以傳回對局部變量的引用,這在 C 或 C++ 等其他語言中是不行的。
如果您需要知道變量的配置設定位置,請将“-m”gc 标志傳遞給“go build”或“go run”(例如,go run -gcflags -m app.go)。
61.GOMAXPROCS、并發和并行
等級:進階
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.GOMAXPROCS(-1)) //prints: X (1 on play.golang.org)
fmt.Println(runtime.NumCPU()) //prints: X (1 on play.golang.org)
runtime.GOMAXPROCS(20)
fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 20
runtime.GOMAXPROCS(300)
fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 256
}
62.讀寫操作重新排序
等級:進階
Go 可能會重新排序某些操作,但它可以確定發生它的 goroutine 中的整體行為不會改變。但是,它不能保證跨多個 goroutine 的執行順序。
package main
import (
"runtime"
"time"
)
var _ = runtime.GOMAXPROCS(3)
6
var a, b int
func u1() {
a = 1
b = 2
}
func u2() {
a = 3
b = 4
}
func p() {
println(a)
println(b)
}
func main() {
go u1()
go u2()
go p()
time.Sleep(1 * time.Second)
}
如果您多次運作此代碼,您可能會看到這些a和b變量組合:
1
2
3
4
0
2
0
0
1
4
a和最有趣的組合b是“02”。顯示b之前更新過a。
如果您需要跨多個 goroutine 保持讀寫操作的順序,則需要使用通道或“同步”包中的适當構造。
63.搶先排程
等級:進階
可能有一個流氓 goroutine 會阻止其他 goroutine 運作。如果您有一個for不允許排程程式運作的循環,則可能會發生這種情況。
package main
import "fmt"
func main() {
done := false
go func(){
done = true
}()
for !done {
}
fmt.Println("done!")
}
for循環不必為空。隻要它包含不觸發排程程式執行的代碼,它就會成為一個問題。
排程程式将在 GC、“go”語句、阻塞通道操作、阻塞系統調用和鎖定操作之後運作。它也可以在調用非内聯函數時運作。
package main
import "fmt"
func main() {
done := false
go func(){
done = true
}()
for !done {
fmt.Println("not done!") //not inlined
}
fmt.Println("done!")
}
要确定您在for循環中調用的函數是否内聯,請将“-m”gc 标志傳遞給“go build”或“go run”(例如,go build -gcflags -m)。
另一種選擇是顯式調用排程程式。您可以使用Gosched()“運作時”包中的功能來完成。
package main
import (
"fmt"
"runtime"
)
func main() {
done := false
go func(){
done = true
}()
for !done {
runtime.Gosched()
}
fmt.Println("done!")
}
請注意,上面的代碼包含競争條件。故意這樣做是為了顯示出問題的陷阱。
64.導入 C 和多行導入塊
等級:CGO
您需要導入“C”包才能使用 Cgo。你可以用一行import來做,也可以用一個import塊來做。
package main
/*
#include <stdlib.h>
*/
import (
"C"
)
import (
"unsafe"
)
func main() {
cs := C.CString("my go string")
C.free(unsafe.Pointer(cs))
}
如果您使用import塊格式,則不能在同一塊中導入其他包。
package main
/*
#include <stdlib.h>
*/
import (
"C"
"unsafe"
)
func main() {
cs := C.CString("my go string")
C.free(unsafe.Pointer(cs))
}
編譯錯誤:
./main.go:13:2: 無法确定 C.free 的名稱
65.Import C 和 Cgo 注釋之間沒有空行
等級:CGO
Cgo 的第一個陷阱之一是import "C"語句上方的 cgo 注釋的位置。
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"unsafe"
)
func main() {
cs := C.CString("my go string")
C.free(unsafe.Pointer(cs))
}
編譯錯誤:
./main.go:15:2: 無法确定 C.free 的名稱
確定聲明上方沒有任何空行import "C"。
66.不能使用可變參數調用 C 函數
等級:CGO
您不能直接調用帶有可變參數的 C 函數。
package main
/*
#include <stdio.h>
#include <stdlib.h>
*/
import "C"
import (
"unsafe"
)
func main() {
cstr := C.CString("go")
C.printf("%s\n",cstr) //not ok
C.free(unsafe.Pointer(cstr))
}
編譯錯誤:
./main.go:15:2:意外類型:...
您必須将可變參數 C 函數包裝在具有已知數量參數的函數中。
package main
/*
#include <stdio.h>
#include <stdlib.h>
void out(char* in) {
printf("%s\n", in);
}
*/
import "C"
import (
"unsafe"
)
func main() {
cstr := C.CString("go")
C.out(cstr) //ok
C.free(unsafe.Pointer(cstr))
}