1、開大括號不能放在單獨的一行
在大多數其他使用大括號的語言中,你需要選擇放置它們的位置。Go的方式不同。你可以為此感謝下自動分號的注入(沒有預讀)。是的,Go中也是有分號的:-)
錯誤代碼:
~~~
package main
import "fmt"
func main()
{
fmt.Println("hello world!")
}
~~~
編譯錯誤:
~~~
./main.go:5:6: missing function body for "main"
./main.go:6:1: syntax error: unexpected semicolon or newline before {
~~~
正確代碼:
~~~
package main
import "fmt"
func main() {
fmt.Println("hello world!")
}
~~~
2、未使用的變量
如果你有未使用的變量,代碼將編譯失敗。當然也有例外。在函數內一定要使用聲明的變量,但未使用的全局變量是沒問題的。
如果你給未使用的變量分配了一個新的值,代碼還是會編譯失敗。你需要在某個地方使用這個變量,才能讓編譯器愉快的編譯。
錯誤代碼:
~~~
package main
var gvar int
func main() {
var one int
two := 2
var three int
three = 3
}
~~~
編譯錯誤:
~~~
./main.go:6:6: one declared and not used
./main.go:7:9: two declared and not used
./main.go:8:6: three declared and not used
~~~
正確代碼:
~~~
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、未使用的Imports
如果你引入一個包,而沒有使用其中的任何函數、接口、結構體或者變量的話,代碼將會編譯失敗。
你可以使用goimports來增加引入或者移除未使用的引用:
~~~
$ go get golang.org/x/tools/cmd/goimports
~~~
如果你真的需要引入的包,你可以添加一個下劃線標記符,_,來作為這個包的名字,從而避免編譯失敗。下滑線標記符用于引入,但不使用。
錯誤代碼:
~~~
package main
import (
"fmt"
"log"
"time"
)
func main() {
}
~~~
編譯錯誤:
~~~
./main.go:4:2: imported and not used: "fmt"
./main.go:5:2: imported and not used: "log"
./main.go:6:2: imported and not used: "time"
~~~
正確代碼:
~~~
package main
import (
_ "fmt"
"log"
"time"
)
var _ = log.Println
func main() {
_ = time.Now
}
// 另一個選擇是移除或者注釋掉未使用的imports
~~~
4、簡式的變量聲明僅可以在函數內部使用
錯誤代碼:
~~~
package main
myvar := 1
func main() {
}
~~~
編譯錯誤:
~~~
./main.go:3:1: syntax error: non-declaration statement outside function body
~~~
正確代碼:
~~~
package main
var myvar = 1
func main() {
}
~~~
5、使用簡式聲明重復聲明變量
你不能在一個單獨的聲明中重復聲明一個變量,但在多變量聲明中這是允許的,其中至少要有一個新的聲明變量。
重復變量需要在相同的代碼塊內,否則你將得到一個隱藏變量。
錯誤代碼:
~~~
package main
func main() {
one := 0
one := 1
}
~~~
編譯錯誤:
~~~
./main.go:5:6: no new variables on left side of :=
~~~
正確代碼:
~~~
package main
func main() {
one := 0
one, two := 1, 2
one, two = two, one
}
~~~
6、偶然的變量隱藏 Accidental 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)
}
~~~
運行結果:
~~~
1
1
2
1
~~~
即使對于經驗豐富的Go開發者而言,這也是一個非常常見的陷阱。這個坑很容易挖,但又很難發現。
你可以使用 vet命令來發現一些這樣的問題。 默認情況下, vet不會執行這樣的檢查,你需要設置-shadow參數:
命令:go tool vet -shadow your_file.go
~~~
go tool vet -shadow main.go
main.go:10: declaration of "x" shadows declaration at main.go:6
~~~
7、不使用顯式類型,無法使用“nil”來初始化變量
nil標志符用于表示interface、函數、maps、slices和channels的“零值”。如果你不指定變量的類型,編譯器將無法編譯你的代碼,因為它猜不出具體的類型。
錯誤代碼:
~~~
package main
func main() {
var x = nil
_ = x
}
~~~
編譯錯誤:
~~~
./main.go:4:6: use of untyped nil
~~~
正確代碼:
~~~
package main
func main() {
var x interface{} = nil
_ = x
}
~~~
8、使用“nil” Slices and Maps
在一個nil的slice中添加元素是沒問題的,但對一個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
}
~~~
運行錯誤:
~~~
panic: assignment to entry in nil map
~~~
9、Map的容量
你可以在map創建時指定它的容量,但你無法在map上使用cap()函數。
錯誤代碼:
~~~
package main
func main() {
m := make(map[string]int, 99)
cap(m)
}
~~~
編譯錯誤:
~~~
./main.go:5:5: invalid argument m (type map[string]int) for cap
~~~
10、字符串不會為nil
這對于經常使用nil分配字符串變量的開發者而言是個需要注意的地方。
~~~
package main
func main() {
var x string = nil
if x == nil {
x = "default"
}
}
~~~
編譯錯誤:
~~~
./main.go:4:6: cannot use nil as type string in assignment
./main.go:5:7: invalid operation: x == nil (mismatched types string and nil)
~~~
正確代碼:
~~~
package main
func main() {
var x string
if x == "" {
x = "default"
}
}
~~~
11、Array函數的參數
如果你是一個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])
}
~~~
運行結果:
~~~
[7 2 3]
[1 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]
}
~~~
運行結果:
~~~
&[7 2 3]
[7 2 3]
~~~
另一個選擇是使用slice。即使你的函數得到了slice變量的一份拷貝,它依舊會參照原始的數據。
~~~
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]
}
~~~
運行結果:
~~~
[7 2 3]
[7 2 3]
~~~
12、在Slice和Array使用“range”語句時的出現的不希望得到的值
如果你在其他的語言中使用“for-in”或者“foreach”語句時會發生這種情況。Go中的“range”語法不太一樣。它會得到兩個值:第一個值是元素的索引,而另一個值是元素的數據。
~~~
package main
import "fmt"
func main() {
x := []string{"a", "b", "c"}
for v := range x {
fmt.Println(v) //prints 0, 1, 2
}
}
~~~
運行結果:
~~~
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
}
}
~~~
運行結果:
~~~
a
b
c
~~~
13、Slices和Arrays是一維的
看起來Go好像支持多維的Array和Slice,但不是這樣的。盡管可以創建數組的數組或者切片的切片。對于依賴于動態多維數組的數值計算應用而言,Go在性能和復雜度上還相距甚遠。
你可以使用純一維數組、“獨立”切片的切片,“共享數據”切片的切片來構建動態的多維數組。
如果你使用純一維的數組,你需要處理索引、邊界檢查、當數組需要變大時的內存重新分配。
使用“獨立”slice來創建一個動態的多維數組需要兩步。首先,你需要創建一個外部的slice。然后,你需要分配每個內部的slice。內部的slice相互之間獨立。你可以增加減少它們,而不會影響其他內部的slice。
~~~
package main
func main() {
x := 2
y := 4
table := make([][]int, x)
for i := range table {
table[i] = make([]int, y)
}
}
~~~
使用“共享數據”slice的slice來創建一個動態的多維數組需要三步。首先,你需要創建一個用于存放原始數據的數據“容器”。然后,你再創建外部的slice。最后,通過重新切片原始數據slice來初始化各個內部的slice。
~~~
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>
}
~~~
運行結果:
~~~
[0 1 2 3 4 5 6 7] 0xc4200140a0
[[0 1 2 3] [4 5 6 7]] 0xc4200140a0
~~~
關于多維array和slice已經有了專門申請,但現在看起來這是個低優先級的特性。
14、訪問不存在的 Map Keys
這對于那些希望得到“nil”標示符的開發者而言是個技巧(和其他語言中做的一樣)。如果對應的數據類型的“零值”是“nil”,那返回的值將會是“nil”,但對于其他的數據類型是不一樣的。檢測對應的“零值”可以用于確定map中的記錄是否存在,但這并不總是可信(比如,如果在二值的map中“零值”是false,這時你要怎么做)。檢測給定map中的記錄是否存在的最可信的方法是,通過map的訪問操作,檢查第二個返回的值。
~~~
package main
import "fmt"
func main() {
x := map[string]string{"one": "a", "two": "", "three": "c"}
if v := x["two"]; v == "" {
fmt.Println("no entry")
}
}
~~~
運行結果:
~~~
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")
} else {
fmt.Println("exist")
}
}
~~~
運行結果:
~~~
exist
~~~
15、Strings無法修改
嘗試使用索引操作來更新字符串變量中的單個字符將會失敗。string是只讀的byte slice(和一些額外的屬性)。如果你確實需要更新一個字符串,那么使用byte slice,并在需要時把它轉換為string類型。
錯誤代碼:
~~~
package main
import "fmt"
func main() {
x := "text"
x[0] = 'T'
fmt.Println(x)
}
~~~
編譯錯誤:
~~~
./main.go:7:7: cannot assign to x[0]
~~~
正確代碼:
~~~
package main
import "fmt"
func main() {
x := "text"
xbytes := []byte(x)
xbytes[0] = 'T'
fmt.Println(string(xbytes)) //prints Text
}
~~~
需要注意的是:這并不是在文字string中更新字符的正確方式,因為給定的字符可能會存儲在多個byte中。如果你確實需要更新一個文字string,先把它轉換為一個rune slice。即使使用rune slice,單個字符也可能會占據多個rune,比如當你的字符有特定的重音符號時就是這種情況。這種復雜又模糊的“字符”本質是Go字符串使用byte序列表示的原因。
16、String和Byte Slice之間的轉換
當你把一個字符串轉換為一個byte slice(或者反之)時,你就得到了一個原始數據的完整拷貝。這和其他語言中cast操作不同,也和新的slice變量指向原始byte slice使用的相同數組時的重新slice操作不同。
Go在[]byte到string和string到[]byte的轉換中確實使用了一些優化來避免額外的分配(在todo列表中有更多的優化)。
第一個優化避免了當[]byte keys用于在map[string]集合中查詢時的額外分配:m[string(key)]。
第二個優化避免了字符串轉換為[]byte后在for range語句中的額外分配:for i,v := range []byte(str) {...}。
17、String和索引操作
字符串上的索引操作返回一個byte值,而不是一個字符(和其他語言中的做法一樣)。
~~~
package main
import "fmt"
func main() {
x := "text"
fmt.Println(x[0]) //print 116
fmt.Printf("%T\n", x[0]) //prints uint8
}
~~~
運行結果:
~~~
116
uint8
~~~
如果你需要訪問特定的字符串“字符”(unicode編碼的points/runes),使用for range。官方的“unicode/utf8”包和實驗中的utf8string包(golang.org/x/exp/utf8string)也可以用。utf8string包中包含了一個很方便的At()方法。把字符串轉換為rune的切片也是一個選項。
18、字符串不總是UTF8文本
字符串的值不需要是UTF8的文本。它們可以包含任意的字節。只有在string literal使用時,字符串才會是UTF8。即使之后它們可以使用轉義序列來包含其他的數據。
為了知道字符串是否是UTF8,你可以使用“unicode/utf8”包中的ValidString()函數。
~~~
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
}
~~~
運行結果:
~~~
true
false
~~~
19、字符串的長度
讓我們假設你是Python開發者,你有下面這段代碼:
~~~
data = u'?'
print(len(data)) #prints: 1
~~~
當把它轉換為Go代碼時,你可能會大吃一驚。
~~~
package main
import "fmt"
func main() {
data := "?"
fmt.Println(len(data)) //prints: 3
}
~~~
內建的 len()函數返回byte的數量,而不是像Python中計算好的unicode字符串中字符的數量。
要在Go中得到相同的結果,可以使用“unicode/utf8”包中的 RuneCountInString()函數。
~~~
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
data := "?"
fmt.Println(utf8.RuneCountInString(data)) //prints: 1
}
~~~
運行結果:
~~~
1
~~~
理論上說 RuneCountInString()函數并不返回字符的數量,因為單個字符可能占用多個rune。
~~~
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
data := "e?"
fmt.Println(len(data)) //prints: 3
fmt.Println(utf8.RuneCountInString(data)) //prints: 2
}
~~~
運行結果:
~~~
3
2
~~~
20、在多行的Slice、Array和Map語句中遺漏逗號
~~~
package main
func main() {
x := []int{
1,
2
}
_ = x
}
~~~
編譯錯誤:
~~~
./main.go:6:4: syntax error: unexpected newline, expecting comma or }
~~~
正確代碼:
~~~
package main
func main() {
x := []int{
1,
2,
}
x = x
y := []int{3, 4}
y = y
}
~~~
當你把聲明折疊到單行時,如果你沒加末尾的逗號,你將不會得到編譯錯誤。
21、log.Fatal和log.Panic不僅僅是Log
Logging庫一般提供不同的log等級。與這些logging庫不同,Go中log包在你調用它的Fatal*()和Panic*()函數時,可以做的不僅僅是log。當你的應用調用這些函數時,Go也將會終止應用
~~~
package main
import "log"
func main() {
log.Fatalln("Fatal Level: log entry") //app exits here
log.Println("Normal Level: log entry")
}
~~~
運行結果:
~~~
2018/05/29 22:13:00 Fatal Level: log entry
exit status 1
~~~
22、內建的數據結構操作不是同步的
即使Go本身有很多特性來支持并發,并發安全的數據集合并不是其中之一,確保數據集合以原子的方式更新是你的職責。Goroutines和channels是實現這些原子操作的推薦方式,但你也可以使用“sync”包,如果它對你的應用有意義的話。
23、String在“range”語句中的迭代值
索引值(“range”操作返回的第一個值)是返回的第二個值的當前“字符”(unicode編碼的point/rune)的第一個byte的索引。它不是當前“字符”的索引,這與其他語言不同。注意真實的字符可能會由多個rune表示。如果你需要處理字符,確保你使用了“norm”包(golang.org/x/text/unicode/norm)。
string變量的for range語句將會嘗試把數據翻譯為UTF8文本。對于它無法理解的任何byte序列,它將返回0xfffd runes(即unicode替換字符),而不是真實的數據。如果你任意(非UTF8文本)的數據保存在string變量中,確保把它們轉換為byte slice,以得到所有保存的數據。
~~~
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)
}
~~~
運行結果:
~~~
0x41 0xfffd 0x2 0xfffd 0x4
0x41 0xfe 0x2 0xff 0x4
~~~
24、對Map使用“for range”語句迭代
如果你希望以某個順序(比如,按key值排序)的方式得到元素,就需要這個技巧。每次的map迭代將會生成不同的結果。Go的runtime有心嘗試隨機化迭代順序,但并不總會成功,這樣你可能得到一些相同的map迭代結果。所以如果連續看到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的游樂場(https://play.golang.org/),你將總會得到同樣的結果,因為除非你修改代碼,否則它不會重新編譯代碼。
25、"switch"聲明中的失效行為
在“switch”聲明語句中的“case”語句塊在默認情況下會break。這和其他語言中的進入下一個“next”代碼塊的默認行為不同。
~~~
package main
import "fmt"
func main() {
isSpace := func(ch byte) bool {
switch ch {
case ' ':
case '\t':
return true
}
return false
}
fmt.Println(isSpace('\t'))
fmt.Println(isSpace(' '))
}
~~~
運行結果:
~~~
true
false
~~~
你可以通過在每個“case”塊的結尾使用“fallthrough”,來強制“case”代碼塊進入。你也可以重寫switch語句,來使用“case”塊中的表達式列表。
~~~
package main
import "fmt"
func main() {
isSpace := func(ch byte) bool {
switch ch {
case ' ':
fallthrough
case '\t':
return true
}
return false
}
fmt.Println(isSpace('\t'))
fmt.Println(isSpace(' '))
}
~~~
~~~
package main
import "fmt"
func main() {
isSpace := func(ch byte) bool {
switch ch {
case ' ', '\t':
return true
}
return false
}
fmt.Println(isSpace('\t'))
fmt.Println(isSpace(' '))
}
~~~
運行結果:
~~~
true
true
~~~
26、自增和自減
許多語言都有自增和自減操作。不像其他語言,Go不支持前置版本的操作。你也無法在表達式中使用這兩個操作符。
錯誤代碼:
~~~
package main
import (
"fmt"
)
func main() {
data := []int{1, 2, 3}
i := 0
++i
fmt.Println(data[i++])
}
~~~
編譯錯誤:
~~~
./main.go:10:2: syntax error: unexpected ++, expecting }
~~~
正確代碼:
~~~
package main
import "fmt"
func main() {
data := []int{1, 2, 3}
i := 0
i++
fmt.Println(data[i])
}
~~~
27、按位NOT操作
許多語言使用 ~作為一元的NOT操作符(即按位補足),但Go為了這個重用了XOR操作符(^)。
錯誤代碼:
~~~
package main
import "fmt"
func main() {
fmt.Println(~2)
}
~~~
編譯錯誤:
~~~
./main.go:6:14: bitwise complement operator is ^
~~~
正確代碼:
~~~
package main
import "fmt"
func main() {
var d uint8 = 2
fmt.Printf("%08b\n", ^d)
}
~~~
Go依舊使用^作為XOR的操作符,這可能會讓一些人迷惑。
如果你愿意,你可以使用一個二元的XOR操作(如, 0x02 XOR 0xff)來表示一個一元的NOT操作(如,NOT 0x02)。這可以解釋為什么^被重用來表示一元的NOT操作。
Go也有特殊的‘AND NOT’按位操作(&^),這也讓NOT操作更加的讓人迷惑。這看起來需要特殊的特性/hack來支持 A AND (NOT B),而無需括號。
~~~
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))
}
~~~
28、操作優先級的差異
除了”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
}
~~~
運行結果:
~~~
0x2 & 0x2 + 0x4 -> 0x6
0x2 + 0x2 << 0x1 -> 0x6
0xf | 0x2 ^ 0x2 -> 0xd
~~~
29、未導出的結構體不會被編碼
以小寫字母開頭的結構體將不會被(json、xml、gob等)編碼,因此當你編碼這些未導出的結構體時,你將會得到零值。
~~~
package main
import (
"encoding/json"
"fmt"
)
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:""}
}
~~~
運行結果:
~~~
main.MyData{One:1, two:"two"}
{"One":1}
main.MyData{One:1, two:""}
~~~
30、有活動的 Goroutines 下的應用退出
應用將不會等待所有的goroutines完成。這對于初學者而言是個很常見的錯誤。
~~~
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] is running
[1] is running
all done!
~~~
一個最常見的解決方法是使用“WaitGroup”變量。它將會讓主goroutine等待所有的worker goroutine完成。如果你的應用有長時運行的消息處理循環的worker,你也將需要一個方法向這些goroutine發送信號,讓它們退出。你可以給各個worker發送一個“kill”消息。另一個選項是關閉一個所有worker都接收的channel。這是一次向所有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)
}
~~~
如果你運行這個應用,你將會看到:
~~~
[1] is running
[1] is done
[0] is running
[0] is done
fatal error: all goroutines are asleep - deadlock!
~~~
看起來所有的worker在主goroutine退出前都完成了。為什么會出現死鎖?worker退出了,它們也執行了wg.Done()。應用應該沒問題啊。
死鎖發生是因為各個worker都得到了原始的“WaitGroup”變量的一個拷貝。當worker執行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
}
}
}
~~~
運行結果:
~~~
[0] is running
[0] m => 0
[1] is running
[1] is done
[0] m => 1
[0] is done
all done!
~~~
現在它會如預期般工作
31、向無緩存的Channel發送消息,只要目標接收者準備好就會立即返回
發送者將不會被阻塞,除非消息正在被接收者處理。根據你運行代碼的機器的不同,接收者的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
}
~~~
運行結果:
~~~
processed: cmd.1
processed: cmd.2
~~~
32、向已關閉的Channel發送會引起Panic
從一個關閉的channel接收是安全的。在接收狀態下的ok的返回值將被設置為false,這意味著沒有數據被接收。如果你從一個有緩存的channel接收,你將會首先得到緩存的數據,一旦它為空,返回的ok值將變為false。
向關閉的channel中發送數據會引起panic。這個行為有文檔說明,但對于新的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)
}
~~~
運行錯誤:
~~~
6
panic: send on closed channel
goroutine 6 [running]:
main.main.func1(0xc420070060, 0x1)
~~~
根據不同的應用,修復方法也將不同。可能是很小的代碼修改,也可能需要修改應用的設計。無論是哪種方法,你都需要確保你的應用不會向關閉的channel中發送數據。
上面那個有bug的例子可以通過使用一個特殊的廢棄的channel來向剩余的worker發送不再需要它們的結果的信號來修復。
~~~
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)
}
~~~
運行結果:
~~~
2 sent result
result: 6
1 exiting
0 exiting
~~~
33、使用"nil" Channels
在一個nil的channel上發送和接收操作會被永久阻塞。這個行為有詳細的文檔解釋,但它對于新的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!
~~~
這個行為可以在select聲明中用于動態開啟和關閉case代碼塊的方法。
~~~
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)
}
~~~
運行結果:
~~~
result: 1
result: 2
~~~
34、傳值方法的接收者無法修改原有的值
方法的接收者就像常規的函數參數。如果聲明為值,那么你的函數/方法得到的是接收者參數的拷貝。這意味著對接收者所做的修改將不會影響原有的值,除非接收者是一個map或者slice變量,而你更新了集合中的元素,或者你更新的域的接收者是指針。
~~~
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]
}
~~~
運行結果:
~~~
num=1 key=key.1 items=map[]
num=7 key=key.1 items=map[]
num=7 key=v.key items=map[vmethod:true]
~~~
- 序言
- 目錄
- 環境搭建
- Linux搭建golang環境
- Windows搭建golang環境
- Mac搭建golang環境
- Go 環境變量
- 編輯器
- vs code
- Mac 安裝vs code
- Windows 安裝vs code
- vim編輯器
- 介紹
- 1.Go語言的主要特征
- 2.golang內置類型和函數
- 3.init函數和main函數
- 4.包
- 1.工作空間
- 2.源文件
- 3.包結構
- 4.文檔
- 5.編寫 Hello World
- 6.Go語言 “ _ ”(下劃線)
- 7.運算符
- 8.命令
- 類型
- 1.變量
- 2.常量
- 3.基本類型
- 1.基本類型介紹
- 2.字符串String
- 3.數組Array
- 4.類型轉換
- 4.引用類型
- 1.引用類型介紹
- 2.切片Slice
- 3.容器Map
- 4.管道Channel
- 5.指針
- 6.自定義類型Struct
- 流程控制
- 1.條件語句(if)
- 2.條件語句 (switch)
- 3.條件語句 (select)
- 4.循環語句 (for)
- 5.循環語句 (range)
- 6.循環控制Goto、Break、Continue
- 函數
- 1.函數定義
- 2.參數
- 3.返回值
- 4.匿名函數
- 5.閉包、遞歸
- 6.延遲調用 (defer)
- 7.異常處理
- 8.單元測試
- 壓力測試
- 方法
- 1.方法定義
- 2.匿名字段
- 3.方法集
- 4.表達式
- 5.自定義error
- 接口
- 1.接口定義
- 2.執行機制
- 3.接口轉換
- 4.接口技巧
- 面向對象特性
- 并發
- 1.并發介紹
- 2.Goroutine
- 3.Chan
- 4.WaitGroup
- 5.Context
- 應用
- 反射reflection
- 1.獲取基本類型
- 2.獲取結構體
- 3.Elem反射操作基本類型
- 4.反射調用結構體方法
- 5.Elem反射操作結構體
- 6.Elem反射獲取tag
- 7.應用
- json協議
- 1.結構體轉json
- 2.map轉json
- 3.int轉json
- 4.slice轉json
- 5.json反序列化為結構體
- 6.json反序列化為map
- 終端讀取
- 1.鍵盤(控制臺)輸入fmt
- 2.命令行參數os.Args
- 3.命令行參數flag
- 文件操作
- 1.文件創建
- 2.文件寫入
- 3.文件讀取
- 4.文件刪除
- 5.壓縮文件讀寫
- 6.判斷文件或文件夾是否存在
- 7.從一個文件拷貝到另一個文件
- 8.寫入內容到Excel
- 9.日志(log)文件
- server服務
- 1.服務端
- 2.客戶端
- 3.tcp獲取網頁數據
- 4.http初識-瀏覽器訪問服務器
- 5.客戶端訪問服務器
- 6.訪問延遲處理
- 7.form表單提交
- web模板
- 1.渲染終端
- 2.渲染瀏覽器
- 3.渲染存儲文件
- 4.自定義io.Writer渲染
- 5.模板語法
- 時間處理
- 1.格式化
- 2.運行時間
- 3.定時器
- 鎖機制
- 互斥鎖
- 讀寫鎖
- 性能比較
- sync.Map
- 原子操作
- 1.原子增(減)值
- 2.比較并交換
- 3.導入、導出、交換
- 加密解密
- 1.md5
- 2.base64
- 3.sha
- 4.hmac
- 常用算法
- 1.冒泡排序
- 2.選擇排序
- 3.快速排序
- 4.插入排序
- 5.睡眠排序
- 限流器
- 日志包
- 日志框架logrus
- 隨機數驗證碼
- 生成指定位數的隨機數
- 生成圖形驗證碼
- 編碼格式轉換
- UTF-8與GBK
- 解決中文亂碼
- 設計模式
- 創建型模式
- 單例模式
- singleton.go
- singleton_test.go
- 抽象工廠模式
- abstractfactory.go
- abstractfactory_test.go
- 工廠方法模式
- factorymethod.go
- factorymethod_test.go
- 原型模式
- prototype.go
- prototype_test.go
- 生成器模式
- builder.go
- builder_test.go
- 結構型模式
- 適配器模式
- adapter.go
- adapter_test.go
- 橋接模式
- bridge.go
- bridge_test.go
- 合成/組合模式
- composite.go
- composite_test.go
- 裝飾模式
- decoretor.go
- decorator_test.go
- 外觀模式
- facade.go
- facade_test.go
- 享元模式
- flyweight.go
- flyweight_test.go
- 代理模式
- proxy.go
- proxy_test.go
- 行為型模式
- 職責鏈模式
- chainofresponsibility.go
- chainofresponsibility_test.go
- 命令模式
- command.go
- command_test.go
- 解釋器模式
- interpreter.go
- interperter_test.go
- 迭代器模式
- iterator.go
- iterator_test.go
- 中介者模式
- mediator.go
- mediator_test.go
- 備忘錄模式
- memento.go
- memento_test.go
- 觀察者模式
- observer.go
- observer_test.go
- 狀態模式
- state.go
- state_test.go
- 策略模式
- strategy.go
- strategy_test.go
- 模板模式
- templatemethod.go
- templatemethod_test.go
- 訪問者模式
- visitor.go
- visitor_test.go
- 數據庫操作
- golang操作MySQL
- 1.mysql使用
- 2.insert操作
- 3.select 操作
- 4.update 操作
- 5.delete 操作
- 6.MySQL事務
- golang操作Redis
- 1.redis介紹
- 2.golang鏈接redis
- 3.String類型 Set、Get操作
- 4.String 批量操作
- 5.設置過期時間
- 6.list隊列操作
- 7.Hash表
- 8.Redis連接池
- 其它Redis包
- go-redis/redis包
- 安裝介紹
- String 操作
- List操作
- Set操作
- Hash操作
- golang操作ETCD
- 1.etcd介紹
- 2.鏈接etcd
- 3.etcd存取
- 4.etcd監聽Watch
- golang操作kafka
- 1.kafka介紹
- 2.寫入kafka
- 3.kafka消費
- golang操作ElasticSearch
- 1.ElasticSearch介紹
- 2.kibana介紹
- 3.寫入ElasticSearch
- NSQ
- 安裝
- 生產者
- 消費者
- zookeeper
- 基本操作測試
- 簡單的分布式server
- Zookeeper命令行使用
- GORM
- gorm介紹
- gorm查詢
- gorm更新
- gorm刪除
- gorm錯誤處理
- gorm事務
- sql構建
- gorm 用法介紹
- Go操作memcached
- beego框架
- 1.beego框架環境搭建
- 2.參數配置
- 1.默認參數
- 2.自定義配置
- 3.config包使用
- 3.路由設置
- 1.自動匹配
- 2.固定路由
- 3.正則路由
- 4.注解路由
- 5.namespace
- 4.多種數據格式輸出
- 1.直接輸出字符串
- 2.模板數據輸出
- 3.json格式數據輸出
- 4.xml格式數據輸出
- 5.jsonp調用
- 5.模板處理
- 1.模板語法
- 2.基本函數
- 3.模板函數
- 6.請求處理
- 1.GET請求
- 2.POST請求
- 3.文件上傳
- 7.表單驗證
- 1.表單驗證
- 2.定制錯誤信息
- 3.struct tag 驗證
- 4.XSRF過濾
- 8.靜態文件處理
- 1.layout設計
- 9.日志處理
- 1.日志處理
- 2.logs 模塊
- 10.會話控制
- 1.會話控制
- 2.session 包使用
- 11.ORM 使用
- 1.鏈接數據庫
- 2. CRUD 操作
- 3.原生 SQL 操作
- 4.構造查詢
- 5.事務處理
- 6.自動建表
- 12.beego 驗證碼
- 1.驗證碼插件
- 2.驗證碼使用
- beego admin
- 1.admin安裝
- 2.admin開發
- beego 熱升級
- beego實現https
- gin框架
- 安裝使用
- 路由設置
- 模板處理
- 文件上傳
- gin框架中文文檔
- gin錯誤總結
- 項目
- 秒殺項目
- 日志收集
- 面試題
- 面試題一
- 面試題二
- 錯題集
- Go語言陷阱和常見錯誤
- 常見語法錯誤
- 初級
- 中級
- 高級
- Go高級應用
- goim
- goim 啟動流程
- goim 工作流程
- goim 結構體
- gopush
- gopush工作流程
- gopush啟動流程
- gopush業務流程
- gopush應用
- gopush新添功能
- gopush壓力測試
- 壓測注意事項
- rpc
- HTTP RPC
- TCP RPC
- JSON RPC
- 常見RPC開源框架
- pprof
- pprof介紹
- pprof應用
- 使用pprof及Go 程序的性能優化
- 封裝 websocket
- cgo
- Golang GC
- 查看程序運行過程中的GC信息
- 定位gc問題所在
- Go語言 demo
- 用Go語言計算一個人的年齡,生肖,星座
- 超簡易Go語言實現的留言板代碼
- 信號處理模塊,可用于在線加載配置,配置動態加載的信號為SIGHUP
- 陽歷和陰歷相互轉化的工具類 golang版本
- 錯誤總結
- 網絡編程
- 網絡編程http
- 網絡編程tcp
- Http請求
- Go語言必知的90個知識點
- 第三方庫應用
- cli應用
- Cobra
- 圖表庫
- go-echarts
- 開源IM
- im_service
- 機器學習庫
- Tensorflow
- 生成二維碼
- skip2/go-qrcode生成二維碼
- boombuler/barcode生成二維碼
- tuotoo/qrcode識別二維碼
- 日志庫
- 定時任務
- robfig/cron
- jasonlvhit/gocron
- 拼多多開放平臺 SDK
- Go編譯
- 跨平臺交叉編譯
- 一問一答
- 一問一答(一)
- 為什么 Go 標準庫中有些函數只有簽名,沒有函數體?
- Go開發的應用
- etcd
- k8s
- Caddy
- nsq
- Docker
- web框架