go 经典译文:50 个 go 新手易犯的错误(2020版) | go 技术论坛-江南app体育官方入口
简介
go是一个非常简单且有意思的编程语言,但是,它并不是完美的,它也像其他编程语言一样有瑕疵。这些瑕疵并不完全是go本身的问题。其中一部分是因为它在一些方面的设计上与其他语言出入很大,也有一部分是因为设计思路上或细节上的缺失。
如果你愿意花时间学习这门语言,去读官方规范、查wiki、参与论坛讨论、研究rob pike的帖子和演讲、甚至精读源码,你会很容易发现go的这些瑕疵。不是每个人都是这么开始学习go的,但是没关系,如果你是go新手,这里的资料将会帮你节省大量debug的时间。
圈套、陷阱和共同错误
花括号不能放在单独的一行
- 等级: 新手
大多数使用花括号的语言中,您可以选择放置花括号的位置。 但 go 不一样。 go 在编译时会自动注入分号,花括号单独一行会导致分号注入错误(无需自己书写分号)。 所以 go 其实是有分号的 :-)
错误的范例:
package main
import "fmt"
func main()
{ // 错误,不能将左大括号放在单独的行上
fmt.println("hello there!")
}
编译错误:
/tmp/sandbox826898458/main.go:6: 语法错误: {前出现意外的分号或者新的一行
正确的写法:
package main
import "fmt"
func main() {
fmt.println("works!")
}
未使用的变量
- 等级: 新手
如果存在未使用的变量会导致编译失败。但是有一个例外, 只有在函数内部声明的变量未使用才会导致报错,如果你有未使用的全局变量是没问题的,也可以存在未使用的函数参数。
如果给变量赋值但是没有使用该变量值,您的代码仍将无法编译。您需要以某种方式使用变量值以使编译器通过。
错误的范例:
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: one declared and not used /tmp/sandbox473116179/main.go:7: two declared and not used /tmp/sandbox473116179/main.go:8: 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
}
另一种选择是注释掉或删除未使用的变量:-)
未使用的导入
- 等级:新手
如果你导入一个包却没有使用它的任何导出函数,接口,结构体或变量,你的代码将会编译失败。
如果确实需要导入包,你可以使用空白标识符_
作为其包名,以避免此编译失败。对于这些副作用,使用空标识符来导入包。
错误的范例:
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:导入但未使用:“time”
正确的写法:
package main
import (
_ "fmt"
"log"
"time"
)
var _ = log.println
func main() {
_ = time.now
}
另一个选择是删除或注释掉未使用的导入 :-) 工具可以为您提供帮助。
短变量声明只能在函数内部使用
- 等级: 新手
错误的范例:
package main
myvar := 1 //error
func main() {
}
编译错误:
/tmp/sandbox265716165/main.go:3: non-declaration statement outside function body
正确的写法:
package main
var myvar = 1
func main() {
}
使用短变量声明重新声明变量
- 等级: 新手
你不能在独立的语句中重新声明变量,但在至少声明一个新变量的多变量声明中允许这样做。
重新声明的变量必须位于同一块中,否则最终将得到隐藏变量。
错误的范例:
package main
func main() {
one := 0
one := 1 //error
}
编译错误:
/tmp/sandbox706333626/main.go:5: no new variables on left side of :=
正确的写法:
package main
func main() {
one := 0
one, two := 1,2
one,two = two,one
}
不能使用短变量声明来设置字段值
- 级别: 新手
错误的范例:
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 :=
尽管有解决这个问题的办法,但它不太可能改变,因为 rob pike 喜欢它「按原样」:-)
使用临时变量或预先声明所有变量并使用标准赋值运算符。
正确的写法:
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}
}
偶然的变量隐藏
- 等级: 新手
简短的变量声明语法非常方便(特别是对于那些来自动态语言的变量),以至于可以像对待常规赋值操作一样轻松地对待它。如果你在新的代码块中犯了此错误,将不会有编译器错误,但你的应用程序将无法达到你的期望。
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
将不执行任何隐藏变量的检查。确保使用 -shadow
标志:go tool vet -shadow your_file.go
注意,vet
命令不会报告所有的隐藏变量。使用 进行更全面的隐藏变量检查。
不能使用 「nil」来初始化没有显式类型的变量
- 等级: 新手
「nil」标识符可以用作接口,函数,指针,映射,切片和通道的「零值」。如果不指定变量类型,则编译器将无法编译代码,因为它无法猜测类型。
错误的范例:
package main
func main() {
var x = nil //error
_ = x
}
编译错误:
/tmp/sandbox188239583/main.go:4: use of untyped nil
正确的写法:
package main
func main() {
var x interface{} = nil
_ = x
}
使用 「nil」 切片和映射
- 等级: 新手
可以将数据添加到「nil」切片中,但是对映射执行相同操作会产生运行时崩溃(runtime 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
}
映射容量
- 等级: 新手
你可以在创建映射时指定映射的容量,但不能在映射中使用 cap()
函数。
错误的范例:
package main
func main() {
m := make(map[string]int,99)
cap(m) //error
}
编译错误:
/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap
字符串不能为「nil」
- 等级: 新手
对于习惯于为字符串变量分配「nil」标识符的开发人员来说,这是一个陷阱。
错误的范例:
package main
func main() {
var x string = nil //error
if x == nil { //error
x = "default"
}
}
编译错误:
/tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment /tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types string and nil)
正确的写法:
package main
func main() {
var x string //defaults to "" (zero value)
if x == "" {
x = "default"
}
}
数组函数参数
- 等级: 新手
如果你是 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]
}
切片和数组「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
}
}
正确的写法:
package main
import "fmt"
func main() {
x := []string{"a","b","c"}
for _, v := range x {
fmt.println(v) //prints a, b, c
}
}
切片和数组是一维的
- 等级: 新手
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]
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]]
}
对于多维数组和切片有一个规范/建议,但目前看来这是低优先级的功能。
访问不存在的映射键
- 等级: 新手
对于希望获得「nil」标识符的开发人员来说这是一个陷阱(就像其他语言一样)。如果相应数据类型的「零值」为「 nil」,则返回值将为「 nil」,但对于其他数据类型,返回值将不同。检查适当的「零值」可用于确定映射记录是否存在,但是并不总是可靠的(例如,如果您的布尔值映射中「零值」为 false,您会怎么做)。知道给定映射记录是否存在的最可靠方法是检查由映射访问操作返回的第二个值。
错误的范例:
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")
}
}
字符串是不可变的
- 等级: 新手
尝试使用索引运算符更新字符串变量中的单个字符将导致失败。字符串是只读字节片(具有一些其他属性)。如果确实需要更新字符串,则在必要时使用字节片而不是将其转换为字符串类型。
错误的范例:
package main
import "fmt"
func main() {
x := "text"
x[0] = 't'
fmt.println(x)
}
编译错误:
/tmp/sandbox305565531/main.go: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
}
请注意,这不是真正更新文本字符串中字符的正确方法,因为给定字符可以存储在多个字节中。如果确实需要更新文本字符串,请先将其转换为符文切片。即使使用符文切片,单个字符也可能跨越多个符文。例如,如果你的字符带有重音符号,则可能会发生这种情况。「字符」的这种复杂和模凌两可的性质是将 go 字符串表示为字节序列的原因。
字符串和字节片之间的转换
- 等级: 新手
当你将字符串转换为字节片(反之亦然)时,你将获得原始数据的完整副本。这不像其他语言中的强制转换操作,也不像在新切片变量指向原始字节片所使用的相同基础数组的切片一样。
go 对于 []byte
转string
,和 string
转 []byte
确实做了一些优化,以免转换额外分配(在待办事项列表中还对此进行了更多的优化)
第一个优化避免了在map[string]
获取 m[string(key)]
中使用 []byte
的keys查找条目时的额外分配。
第二个优化避免了在 for range
字符串被转换的语句 []byte
: for i,v := range []byte(str) {...}
.
索引运算符和字符串
-级别:初学者
字符串上的index方法(运算符)返回一个字节值,而不是一个字符类型(就像在其他语言中一样)。
package main
import "fmt"
func main() {
x := "text"
fmt.println(x[0]) //print 116
fmt.printf("%t",x[0]) //prints uint8
}
如果需要访问特定字符串“characters”(unicode代码点/运行符),请使用 for range
语句。官方的“unicode/utf8”包和基础的utf8string包(golang.org/x/exp/utf8string)也很有用。utf8string包有一个方便的 at()
方法,将字符串转换为切片也是一种选择。
字符串并不总是 utf8 文本
- 等级: 新手
字符串的值不一定是 utf8 文本。它们可以包含任意字节。只有在使用字符串字面值时,字符串才是 utf8。即使这样,它们也可以使用转译序列包括其他数据。若要了解您是否具有 utf8 文本字符串,请使用 「unicode/uft8」包中的函数 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
}
字符串长度
- 等级: 新手
假设你是 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 中获得相同的结果,请使用 「unicode/utf8」包中的 runecountinstring()
函数。
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
}
在多行切片,数组和映射字面值中缺少逗号
- 等级: 新手
错误的范例:
package main
func main() {
x := []int{
1,
2 //error
}
_ = x
}
编译错误:
/tmp/sandbox367520156/main.go:6: syntax error: need trailing comma before newline in composite literal /tmp/sandbox367520156/main.go:8: non-declaration statement outside function body /tmp/sandbox367520156/main.go:9: syntax error: unexpected }
正确的写法:
package main
func main() {
x := []int{
1,
2,
}
x = x
y := []int{3,4,} //no error
y = y
}
如果在声明折叠为一行时留下逗号,则不会出现编译错误。
log.fatal 与 log.panic 比 log 要做的更多
- 级别: 新手
日志库通常提供不同的日志级别。与那些日志记录库不同,go 中的日志包的作用远不止于日志记录。如果在您的应用中调用 go 的 fatal *()
和 panic *()
函数,go 将会终止您的应用 :-)
package main
import "log"
func main() {
log.fatalln("fatal level: log entry") //app exits here
log.println("normal level: log entry")
}
内置数据结构操作不同步
- 等级: 新手
尽管 go 有很多支持并发的原生特性,但是并发安全的数据集合不在这些特性中。开发者需要保证对这些数据集合的并发更新操作是原子性的,比如对 map 的并发更新。go 推荐使用 channels 来实现对集合数据的原子性操作。当然如果「sync」包更适合你的应用也可以利用「sync」包来实现。
「range」语句对于字符串的操作
- 等级:新手
「range」语句的第一个返回值是当前「字符」(该字符可能是 unicode 码点/rune)的第一个字节在字符串中按字节的索引值(unicode 是多字节编码),「range」语句的第二个返回值是当前的「字符」。这是 go 其他语言不同的地方,其他语言的迭代操作大多是返回当前字符的位置,但 go「range」返回的并不是当前字符的位置。在实际的使用中一个字符可能是由多个 rune 表示的,所以当我们需要处理字符时强烈推荐使用「norm」包(golang.org/x/text/unicode/norm)。
带有字符串变量的 for range
子句将尝试把数据解释为 utf8 文本。对于任何它无法理解的字节序列,它将返回 0xfffd
runes(即 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)
}
使用 「for range」子句遍历 map
- level:初学者
如果你希望map每项数据按照顺序排列(例如,按键值顺序),这是不可能的,每次map迭代会输出不一样的结果。go运行时可能会随机分配迭代顺序,因此你可能会得到几次相同的map迭代结果也不用惊讶。
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 ()运行这段代码,将始终得到相同的迭代结果,因为除非进行更改代码,否则它不会重新编译代码。
switch语句中的fallthrough行为
- 级别: 新手
在"switch"语句中的"case"块,其缺省行为是break出"switch"。这一行为与其它语言不同,其它语言的缺省行为是,继续执行下一个"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)
}
增量和减量
- 级别:初学者
许多语言都有递增和递减运算符。与其他语言不同,go不支持操作的前缀版本。您也不能在表达式中使用这两个运算符。
失败:
package main
import "fmt"
func main(){
data := []int{1,2,3}
i := 0
i //错误
fmt.println(data [i ])//错误
}
编译错误:
/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])
}
按位not运算符
- 级别:初学者
许多语言都使用〜
作为一元not运算符(也称为按位补码),但是go为此重用了xor运算符(^
)。
失败:
package main
import "fmt"
func main(){
fmt.println(〜2)//错误
}
编译错误:
/tmp/sandbox965529189/main.go:6:按位补码运算符是^
作品:
package main
import "fmt"
func main(){
var d uint8 = 2
fmt.printf(“%08b \ n”,^ d)
}
go 仍然使用 ^
作为 xor 运算符,这可能会使某些人感到困惑。
如果你愿意,你可以用二进制的 xor 操作(例如,' not 0x02 ') 来表示一个单目的 not 操作(例如,' 0x02 xor 0xff ')。这可以解释为什么 ^
被重用于表示一元 not 操作。
go 还具有一个特殊的 'and not' 按位运算符(&^
),这增加了 not 运算符的困惑。看起来像一个特性/黑客,不需要括号就可以支持 a and (not b)
。
package main
import "fmt"
func main() {
var a uint8 = 0x82
var b uint8 = 0x02
fmt.printf("b [a]\n",a)
fmt.printf("b [b]\n",b)
fmt.printf("b (not b)\n",^b)
fmt.printf("b ^ b = b [b xor 0xff]\n",b,0xff,b ^ 0xff)
fmt.printf("b ^ b = b [a xor b]\n",a,b,a ^ b)
fmt.printf("b & b = b [a and b]\n",a,b,a & b)
fmt.printf("b &^b = b [a 'and not' b]\n",a,b,a &^ b)
fmt.printf("b&(^b)= b [a and (not b)]\n",a,b,a & (^b))
}
运算符优先级差异
- 级别:初学者
除了「位清除」运算符(&^
)之外,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
}
未导出的结构字段不进行编码
- 级别:初学者
以小写字母开头的 struct 字段将不被编码 (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:""}
}
应用退出与活动的 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]正在运行
全部完成!
最常见的江南app体育官方入口的解决方案之一是使用“ 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] is running
[0] is done
[1] is running
[1] is done
看起来 worker 在主 goroutine 退出之前已经完成。这太棒了!但是1,你还会看到这样的情况:
fatal error: all goroutines are asleep - deadlock!
这不太好 :-) 发生了什么?为什么会出现死锁?当 worker 离开时,它们执行了 wg.done()
。应用程序应该是可以工作的。
发生死锁是因为每个 worker 都会获得原始「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
}
}
}
现在它可以按预期工作了 :-)
发送到无缓冲通道的消息在目标接收器准备就绪后立即返回
- 等级: 新手
直到接收方处理完您的消息后,发送才会被阻止。根据运行代码的机器,接收方 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
}
发送到关闭通道会引起崩溃
- 等级: 新手
从关闭的通道接收是安全的。接收语句中的 ok
返回值将设置为 false
表示未接收到任何数据。如果你是从缓冲通道接收到的数据,则将首先获取缓冲数据,一旦缓冲数据为空,返回的 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)
}
//获取第一个结果
fmt.println(<-ch)
close(ch) //这样做很不好 (因为在协程中还有动作在向 channel 发送数据)
//做些其他的事情
time.sleep(2 * time.second)
}
根据你的应用程序,修复这样的程序将会有所不同。修改细微的代码不让 panic
中断程序是次要的,因为可能你更加需要修改程序的逻辑设计。无论哪种方式,您都需要确保你的应用程序不会在 channel
已经关闭的情况下发送数据给它。
可以通过使用特殊的取消渠道来通知剩余的工作人员不再需要他们的结果,从而解决该示例问题。
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)
}
"nil" 使用“nil”通道
- level: beginner
send and receive operations on a nil
channel block forver. it's a well documented behavior, but it can be a surprise for new go developers.
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)
}
方法中的接受者不能修改原始值
- 级别: 初学者
方法接收者就像常规函数参数一样。如果声明为值,那么您的函数/方法将获得接收器参数的副本。这意味着对接收者进行更改不会影响原始值,除非您的接收者是映射或切片变量,并且您要更新集合中的项,或者您要在接收者中更新的字段是指针。
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]
}
关闭http响应body
- 级别: 中级
当使用net/http
库发送http请求时,会返回一个*http.respose
变量。 如果你不读取响应body,依然需要关闭这个body。 注意对于空body也必须关闭。 对于go程序员新手很容易忘记这点。
一些go程序员新手尝试关闭响应body,但他们在错误的位置进行了关闭body。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.get("https://api.ipify.org?format=json")
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))
}
这种方法适合请求成功的情况,但是如果http请求失败,则resp
变量可能为nil
,这将导致运行触发panic
。
关闭http响应body的最常见方法,应该是在http响应检查错误之后使用defer
调用close
方法。
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
变量将为nil
,而err
变量将为非空。 但是当重定向失败时,两个变量都将为非空。 这意味着body仍然可能会未关闭而导致泄漏。
你可以通过在http响应错误处理时,添加一段关闭非空响应body的代码这解决这个问题(重定向时响应和err都是非空,检查了err返回错误而没有关闭body), 使用一个defer
关闭所有失败和成功请求的响应body。
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()
方法的底层实现是读取并丢弃响应body的剩余数据。 这样可以保证使用了keepalive http
长连接机制,可以将http连接复用,用来发送另外一个请求。 在最新的http客户端处理方法是不同的。 但是现在你需要读取并丢弃其余的响应数据。 如果你不读取并丢弃剩余数据,那么http连接可能会关闭而不是被长连接复用。 这个小陷阱应该记录在go 1.5中。
如果复用http长连接对于你的程序很重要,那么可能需要在响应处理逻辑的末尾添加以下内容:
_, err = io.copy(ioutil.discard, resp.body)
如果你没有读取全部响应body,则需要这样丢弃数据,如果使用以下代码处理json api响应,json库只读取了部分body就完成了json对象解析,未读取完毕body,则可能会发生这种情况:
json.newdecoder(resp.body).decode(&data)
关闭http连接
- 级别: 中级
某些http服务器会打开长连接(基于http/1.1规范和服务器的keepalive
机制)。 在默认情况下,net/http库客户端在收到http服务端要求关闭时,才会关闭长连接。 这意味着程序在某些情况下没有关闭长连接,可能会泄露系统fd,用完操作系统的套接字/文件描述符。
你可以在请求发送前将*http.requsst
对象的close
字段设置为true
,用于关闭net/http库客户端连接。
另一种方法是添加connection
header并设置值为close
。目标http服务器响应也应该返回header connection:close
。当net/http库客户端看到这个header时,它也会关闭连接。
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
// 或者使用下面的这行方法:
//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长连接(keepalives),创建一个自定义使用的*http.transport
对象,用于发送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服务器发送大量请求,则可以打开keepalives选项使用长连接。但是如果你在应用是短时间内,向不同的http服务器发送一两个请求(少量请求),那么则最好在收到http响应后立刻关闭网络连,设置更大的操作系统打开文件句柄数量是一个好方法(ulimit -n)。正确的解决方法取决于你的应用程序。
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 编码器对象旨在用于流传输。使用 json 进行流传输通常意味着用换行符分隔的 json 对象,这就是为什么 encode 方法添加换行符的原因。这是正常的行为,但是通常被忽略或遗忘。
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团队的建议...选择加入。
将json数字解组为接口值
-级别:中级
默认情况下,当您将json数据解码/解组到接口中时,go会将json中的数字值视为float64
数字。这意味着以下代码将因失败而失败:
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值为整数,则可以使用服务器选项。
选项一:按原样使用float值:-)
选项二:将浮点值转换为所需的整数类型。
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)
}
选项三:使用decoder
类型解组json,并使用number
接口类型告诉它表示json数字。
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
将您的数值映射到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)
}
}
十六进制或其他非utf8json字符串转义的值不正确
- 级别: 中等
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)
// 输出:字符串转义中的无效字符'x'
}
}
如果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: 对已解码的数据进行十六进制转义解码
}
另一种方法是在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调用不会失败,但是您获得的字符串可能不是你需要的结果。
比较结构体/数组/切片/map
- 级别: 中级
如果结构体的每个字段都具有可比性,那么则可以使用等号运算符==
比较结构体变量。
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 //无法比较
doit func() bool //无法比较
m map[string] string //无法比较
bytes []byte //无法比较
}
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 //无法比较
doit func() bool //无法比较
m map[string] string //无法比较
bytes []byte //无法比较
}
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)
}
如果你的[]byte
(或字符串)包含文本数据,当你需要使用不区分大小写比较值时,你可能倾向于使用使用"bytes"和"string"库的toupper()/tolower()
函数(在使用==
,bytes.equal()
或bytes.compare()
比较之前)。
这种方法适合英文,但是却不适合许多其他语言的文本。正确的方法应该使用strings.equalfold()
和bytes.equalfold()
方法进行比较。
如果你的[]byte
中包含了验证用户信息的机密信息(例如,加密哈希,令牌等),请不要使用 reflect.deepequal()
或bytes.equal()
或bytes.compare()
函数。因为这些函数可能是你受到,为了比较泄露时间信息,请使用'crypto/subtle'库(例如:subtle.constanttimecompare()
)。
从panic中恢复
- 级别: 中等
recover()
函数可用于捕获/拦截panic。 但是只有在defer函数中,调用recover()
才能达到目的。
不正确:
package main
import "fmt"
func main() {
recover() // 什么也没执行
panic("not good")
recover() // 不会执行到 :)
fmt.println("ok")
}
生效:
package main
import "fmt"
func main() {
defer func() {
fmt.println("recovered:",recover())
}()
panic("not good")
}
仅在你的defer函数中直接调用recover()
时才有效。
失败:
package main
import "fmt"
func dorecover() {
fmt.println("recovered =>",recover()) //prints: recovered =>
}
func main() {
defer func() {
dorecover() //panic is not recovered
}()
panic("not good")
}
使用或更新切片/数组/map rnage遍历的数据
- 级别: 中等
在"range"范围的产生是数据是集合的元素副本,这些值不是原始数据的引用,这意味修改range的值不会改变原始数据。这也意味获得的值地址也不会提供执行原始数据的指针。
package main
import "fmt"
func main() {
data := []int{1,2,3}
for _,v := range data {
v *= 10 //原始项目不变
}
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}
}
切片的隐藏数据
- 级别: 中级
重新分割切片时,新切片将引用旧切片的底层数组。如果你忘记这个行为,并且分配相对较大切片,则从中创建了新建的切片引用了部分原始数据,则可能导致意外的底层数据使用。
package main
import "fmt"
func get() []byte {
raw := make([]byte,10000)
fmt.println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000
return raw[:3]
}
func main() {
data := get()
fmt.println(len(data),cap(data),&data[0]) //prints: 3 10000
}
为避免此陷阱,请确保从临时切片中复制所需的数据(而不是切割切片)。
package main
import "fmt"
func get() []byte {
raw := make([]byte,10000)
fmt.println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000
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
}
切片数据污染
- 等级: 中级
假如需要修改路径(存储在切片中)。你可以重新设置路径用来引用每个目录,从而修改第一个目录的名称,然后将这些名称合并创建新路径。
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] //完整切片表达式
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))
}
完整切片表达式中的额外参数控制新切片的容量。 现在追加到该切片的数据将触发切片扩容,而不是覆盖第二个片中的数据。
旧的切片
- 级别: 中等
多个切片可以引用相同的数据。 例如当你使用现有切片创建新切片时,可能会发生这种情况。 如果程序依靠此行为来正常运行,那么将需要担心的旧的切片。
在某些时候,当原始数组无法容纳更多新数据时,将数据添加到切片将导致新的数组扩容。现在其他切片将指向旧数组(包含旧数据)。
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 }
//仍然引用相同的数组
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]
}
类型声明和方法
-级别:中级
通过从现有(非接口)类型定义新类型来创建类型声明时,您不会继承为该现有类型定义的方法。
失败:
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 undefined (type mymutex has no field or method lock) /tmp/sandbox106401185/main.go:10: mtx.unlock undefined (type mymutex has no field or method 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
}
突破“ for switch”和“ for select”代码块
- 级别:中级
没有标签的“ 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”语句也可以解决问题。
句中的迭代变量和闭包
- 级别:中级
这是go中最常见的陷阱。for
语句中的迭代变量在每次迭代中都会重复使用。这意味着在for
循环中创建的每个闭包(aka函数文字)都将引用相同的变量(它们将在这些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
}
最简单的江南app体育官方入口的解决方案(不需要对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
}
另一种江南app体育官方入口的解决方案是将当前迭代变量作为参数传递给匿名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)
}
延迟函数调用参数评估
- 级别:中级
在评估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
}
延迟函数调用执行
- 级别:中级
延迟的调用在包含函数的末尾(以相反的顺序)而不是在包含代码块的末尾执行。对于新的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
语句 :-)
失败类型断言
- 级别: 中级
失败的类型断言将为断言语句中使用的目标类型返回「零值」。当它与影子变量混合在一起时,可能导致意外行为。
错误的范例:
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)
}
}
阻塞的 goroutines 和资源泄漏
- 级别: 中级
rob pike 在 google i/o 大会上的演讲 谈到了许多基本的并发模式。从多个目标中获取第一个结果就是其中之一。
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
}
该函数为每个搜索副本启动 goroutines。每个 goroutine 将其搜索结果发送到结果通道。返回结果通道的第一个值。
其他 goroutines 的结果如何?那 goroutines 本身呢?
first()
函数中的结果通道未缓冲。这意味着仅第一个 goroutine 返回。所有其他 goroutine 都被困在尝试发送结果。这意味着,如果您有多个副本,则每个调用都会泄漏资源。
为了避免泄漏,您需要确保所有 goroutine 都退出。一种潜在的江南app体育官方入口的解决方案是使用足够大的缓冲结果通道来保存所有结果。
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
}
另一种可能的江南app体育官方入口的解决方案是使用 select
语句和 default
大小写以及可保存一个值的缓冲结果通道。default
情况确保即使结果通道无法接收消息,goroutine 也不会卡住。
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 开发人员来说可能是个问题,他们会按原样使用该代码,而不认为它可能会出现问题。
相同地址的不同零大小变量
- 级别: 中级
如果您有两个不同的变量,它们不应该有不同的地址吗?好吧,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
}
}
iota 的第一次使用并不总是从零开始
- 级别: 中级
它可能看起来像是一个 iota
标识符就像一个增量运算符。开始一个新的常量声明,第一次使用 iota 时得到 0,第二次使用时得到 1,依此类推。但情况并非总是如此。
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
不是常量声明块中的第一行,则初始值将不为零。
在值实例上使用指针接收器方法
- 级别: 高级
只要该值是可寻址的,就可以在该值上调用指针接收器方法。换句话说,在某些情况下,您不需要该方法的值接收器版本。
但是,并非每个变量都是可寻址的。map 元素不可寻址。通过接口引用的变量也是不可寻址的。
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: cannot use data literal (type data) as type printer in assignment: data does not implement printer (print method has pointer receiver)
/tmp/sandbox017696142/main.go:25: cannot call pointer method on m["x"] /tmp/sandbox017696142/main.go:25: cannot take the address of m["x"]
更新 map 值字段
- 级别: 高级
如果您具有结构值 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: cannot assign to m["x"].name
它不会工作,因为 map 元素不可寻址。
对于 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)中更新 map 元素字段,但是该行为很快得到解决:-)它也被认为是 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?" //???
}
「nil」接口和「nil」接口值
- 级别: 高级
这是 go 语言中第二常见的陷阱,因为即使接口看起来像指针,它们也不是指针。接口变量仅在其类型和值字段为「nil」时才为「nil」。
接口类型和值字段基于用于创建相应接口变量的变量的类型和值进行填充。当您尝试检查接口变量是否等于「nil」时,这可能导致意外的行为。
package main
import "fmt"
func main() {
var data *byte
var in interface{}
fmt.println(data,data == nil) //prints: true
fmt.println(in,in == nil) //prints: true
in = data
fmt.println(in,in == nil) //prints: 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:
//'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
}
}
堆栈和堆变量
- 级别: 高级
您并不总是知道您的变量是分配在堆栈还是堆上。在 c 中,使用 new
运算符创建变量始终意味着您具有堆变量。在 go 语言中,即使使用 new()
或 make()
函数,编译器仍会决定将变量分配到何处。编译器根据变量的大小和「转义分析」的结果来选择存储变量的位置。这也意味着可以返回对局部变量的引用,而在其他语言(如 c 或 c )中则不可以。
如果您需要知道变量的分配位置,请将「-m」gc 标志传递给「go build」或「go run」(例如,go run -gcflags -m app.go
)。
gomaxprocs,并发和并行
- 级别: 高级
go 1.4 以下版本仅使用一个执行上下文/os 线程。这意味着在任何给定时间只能执行一个 goroutine。从 go 1.5 开始,将执行上下文的数量设置为 runtime.numcpu()
返回的逻辑 cpu 内核的数量。该数字可能与系统上逻辑 cpu 内核的总数不匹配,具体取决于进程的 cpu 亲和力设置。您可以通过更改 gomaxprocs
环境变量或调用 runtime.gomaxprocs()
函数来调整此数字。
常见的误解是 gomaxprocs
代表 go 将用于运行 goroutine 的 cpu 数量。runtime.gomaxprocs()
函数文档使这个问题更加混乱。gomaxprocs
变量描述()在讨论 os 线程方面做得更好。
您可以将 gomaxprocs
设置为大于 cpu 的数量。从 1.10 版开始,gomaxprocs 不再受限制。gomaxprocs
的最大值以前是 256,后来在 1.9 中增加到 1024。
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
}
读写操作重新排序
- 级别: 高级
go 可以对某些操作进行重新排序,但可以确保 goroutine 中发生该行为的整体行为不会改变。但是,它不能保证跨多个 goroutine 的执行顺序。
package main
import (
"runtime"
"time"
)
var _ = runtime.gomaxprocs(3)
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
23
40
20
01
4
a
和 b
最有趣的组合是「02」。它显示 b
已在 a
之前更新。
如果您需要跨多个 goroutine 保留读取和写入操作的顺序,则需要使用通道或「sync」包中的适当的方法。
抢占式调度
-级别:高级
可能有一个流氓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!")
}
请注意,上面的代码包含一个竞争条件。这样做是故意显示出隐藏的陷阱。
导入c和多行导入块
-级别:cgo
您需要导入“ c”包才能使用cgo。您可以单行import
进行此操作,也可以使用import
块进行此操作。
package main
/*
#include
*/
import (
"c"
)
import (
"unsafe"
)
func main() {
cs := c.cstring("my go string")
c.free(unsafe.pointer(cs))
}
如果以 import
块的方式引入此包 ,则无法在同一个块中引入其他包。
package main
/*
#include
*/
import (
"c"
"unsafe"
)
func main() {
cs := c.cstring("my go string")
c.free(unsafe.pointer(cs))
}
编译错误:
./main.go:13:2: could not determine kind of name for c.free
在 c 和 cgo 注释之间不要有空白行
- 级别: cgo
cgo 的第一个陷阱是:cgo 注释需位于 import c
声明的上方。
package main
/*
#include
*/
import "c"
import (
"unsafe"
)
func main() {
cs := c.cstring("my go string")
c.free(unsafe.pointer(cs))
}
编译错误:
./main.go:15:2: could not determine kind of name for c.free
确保在 import c
声明前没有任何空白行。
不能调用带有可变参数的c函数
- level: cgo
你不能直接调用带有可变参数的c函数
package main
/*
#include
#include
*/
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: unexpected type: ...
你需要用已知数量参数的函数封装c可变数量参数的函数
package main
/*
#include
#include
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))
}