6

新手可能会犯的50个Golang错误

 3 years ago
source link: http://www.hi-roy.com/2020/12/16/%E6%96%B0%E6%89%8B%E5%8F%AF%E8%83%BD%E4%BC%9A%E7%8A%AF%E7%9A%8450%E4%B8%AAGolang%E9%94%99%E8%AF%AF/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

新手可能会犯的50个Golang错误

12月 16, 2020 发布在 Golang, 菜鸟翻译屋

原文,向原作者表示感谢。这里省略了一些无关的介绍说明以及过于简单的说明,直接看代码就懂了。

左花括号不能单独一行

在大多数语言中你可以随便放置花括号的位置,但go语言不一样,你可以理解为为go会自动注入分号(automatic semicolon injection):

错误代码:

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: syntax error: unexpected semicolon or newline before {

正确代码:

package main

import "fmt"

func main() {
fmt.Println("works!")
}

未使用的变量

在go中如果有未使用的变量,即便对其进行了赋值,编译时也会报错。只有全局变量和函数参数可以声明而不使用。

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?")
}

未使用的包

如果引用了某个包而又不使用其中的任何函数、结构体、接口或者变量,则会编译报错。

如果真的需要引入这些包(Roy注:比如仅想执行这些包的init函数时),可以使用_作为包的别名:

package main

import (
_ "fmt"
"log"
"time"
)

var _ = log.Println

func main() {
_ = time.Now
}

简短声明的变量只能在函数内部使用

package main

myvar := 1 //error

func main() {
}


package main

var myvar = 1

func main() {
}

使用简短声明来重复声明变量

package main

func main() {
one := 0
one := 1 //error
}


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 :=

即使有方法可以解决这个问题,但go语言之父挺喜欢目前这样的。
可以使用临时变量或预先声明所有变量并使用标准赋值操作。

正确代码:

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}
}

意外的变量修改

短声明语法对于从动态语言转go的人来说太方便了,但很容易被误认为是赋值操作,如果你在新的代码块中犯了这个错误,编译不会报错,但也不会如预期那样工作:

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 tool vet -shadow your_file.go命令来进行检测,但注意vet命令并不能检测出所有被覆盖的变量,可以使用go-nyet来进一步检测。(Roy注:这个已经集成到vet中了,这里仅仅为了保持原文)

显式类型的变量无法使用nil来初始化

nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。但声明时不指定类型,编译器也无法推断出变量的具体类型。

package main

func main() {
var x = nil //error

_ = x
}

package main

func main() {
var x interface{} = nil

_ = x
}

使用值为 nil 的 slice、map

允许对值为nil的slice添加元素,但对值为nil的map添加元素则会造成运行时错误:

package main

func main() {
var s []int
s = append(s,1)
}

package main

func main() {
var m map[string]int
m["one"] = 1 //error

}

map容量

在创建map类型的变量时可以指定容量,但不能像slice一样使用 cap() 来检测分配空间的大小:

package main

func main() {
m := make(map[string]int,99)
cap(m) //error
}

string类型的变量值不能为nil

string类型的初始值为””而不是nil

package main

func main() {
var x string = nil //error

if x == nil { //error
x = "default"
}
}

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]
}

或者使用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]
}

(Roy注:这里需要了解一下slice的构造,由指向底层数组的指针、len、cap构成,但这个例子里如果超出cap引起底层数组变化,还是有问题。这里原意是修改里面的数据值,而非添加数据。)

混淆了range遍历slice和array时的返回值

与其他编程语言中的for-inforeach语句不同,go中的range在遍历时会返回2个值,第一个是元素索引,第二个是元素的值:

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
}
}

slice和array是一维数据

看上去go中支持多维数组或切片,但实际上并不是。尽管可以创建多维数组或切片,但对于依赖动态多维数组的数值计算型程序,go是远远不够的。

可以使用原始的一维数组、“独立的”切片、“共享数据”的切片来创建动态的多维数组。

如果使用原始的一维数组,则需要在数组需要增长时负责索引、边界检查和内存重新分配。

使用独立的切片来创建动态多维数组分2步:首先创建一个外部切片,然后再分配每个内部切片。内部切片相互独立,所以可以独立的增大或减小他们而不互相影响。

package main

func main() {
x := 2
y := 4

table := make([][]int,x)
for i:= range table {
table[i] = make([]int,y)
}
}

使用共享数据的切片来创建动态多维数组分3步:首先创建一个数据容器切片来存储所有数据,然后创建外部切片,最后切割原始的容器切片来初始化内部切片:

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>
}

目前有增加多维数组和切片的提议,不过优先级比较低。

访问字典中不存在的key

这对于习惯使用返回值是否是默认值来判断key是否存在的开发人员来说是个陷阱,go中可以使用第二个返回值来确定key是否存在:

//错误代码
package main

import "fmt"

func main() {
x := map[string]string{"one":"a","two":"","three":"c"}

if v := x["two"]; v == "" { //incorrect two是存在的,值为''
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")
}
}

字符串是不可变的

尝试使用下标改变字符串中的某个字符会报错,字符串是只读的byte slice,如果确实需要更新一个字符串,那么在必要时使用byte slice而不是字符串类型。

//错误代码
package main

import "fmt"

func main() {
x := "text"
x[0] = 'T'

fmt.Println(x)
}


//正确代码

package main

import "fmt"

func main() {
x := "text"
xbytes := []byte(x)
xbytes[0] = 'T'

fmt.Println(string(xbytes)) //prints Text
}

注意,上面的代码并不是修改字符串中字符的正确方法, 因为某些字符可能占用多个字节。如果需要的话先将字符串转换为rune切片。即使使用了rune切片,单个字符也可能会占用多个rune,比如字符有grave accent,这种“字符”的复杂和模棱两可的性质是Go字符串被表示为字节序列的原因。

string和[]byte相互转换

当进行string和[]byte的相互转换时得到的是原始数据的完整拷贝,而不是其他语言中的强制转换或分配一个新的切片变量指针指向原始数据同一个底层数组。

go中做了2点优化来避免转换时分配额外的内存分配:

  1. map[string]字典m[string(key)]上使用[]byte来获取内容时。
  2. for range操作中将string转换为[]byte时:for i,v := range []byte(str) {...}

字符串和索引操作

在字符串上使用索引下标操作反回的是byte类型,而不是字符。

package main

import "fmt"

func main() {
x := "text"
fmt.Println(x[0]) //print 116
fmt.Printf("%T",x[0]) //prints uint8
}

如果你需要访问字符串中的“字符”(unicode code points/runes),使用for range操作。内置的unicode/utf8包和实验性的golang.org/x/exp/utf8string包都可以用,utf8string包中有个At()方法。当然把字符串转换成[]runes也可以达到目的。

字符串并不总是utf8格式

string的值不必是UTF8文本,可以包含任意的值。只有使用字符串文字字面值时才一定是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
}

字符串长度

假设你是一个python开发者:

data = u'♥'  
print(len(data)) #prints: 1

但在go中:

package main

import "fmt"

func main() {
data := "♥"
fmt.Println(len(data)) //prints: 3
}

内置的len()函数返回字节长度替代了字符长度。

想得到同样的结果使用unicode/utf8包的RuneCountInString()

package main

import (
"fmt"
"unicode/utf8"
)

func main() {
data := "♥"
fmt.Println(utf8.RuneCountInString(data)) //prints: 1

注意这个函数反悔的并不是字符长度,因为单个字符可能占用多个rune:

package main

import (
"fmt"
"unicode/utf8"
)

func main() {
data := "é"
fmt.Println(len(data)) //prints: 3
fmt.Println(utf8.RuneCountInString(data)) //prints: 2
}

在多行array、slice、map语句中缺少逗号

package main

func main() {
x := []int{
1,
2 //error
}
_ = x
}


package main

func main() {
x := []int{
1,
2,
}
x = x

y := []int{3,4,} //no error
y = y
}

将声明写为一行时最后的逗号,不会报编译错误。

log.Fatal和log.Painc

logging库通常提供不同的日志等级,而go中的Fatal*()Panic*()函数不仅仅输出日志还会停止程序:

package main

import "log"

func main() {
log.Fatalln("Fatal Level: log entry") //app exits here
log.Println("Normal Level: log entry")
}

对内置数据结构的操作并不是同步的

尽管go天生支持并发编程,但并不保证并发操作数据安全。开发人员需要自己负责确保数据操作的原子性。goroutine和channel是实现原子操做的推荐方法,当然需要的话也可以使用sync包。

对string使用range操作得到的值

使用range对string进行迭代时,得到的索引(返回的第一个值)时当前“字符”(返回的第二个值)的第一个字节位置,而非当前字符的位置。而一个字符可以由多个rune组成。如果需要对字符进行操作,确保使用norm(golang.org/x/text/unicode/norm)包来进行检查。

for range操作会尝试将字符串当utf8类型,对于任何它不理解的字节序列都会返回0xfffd来替代真实数值。如果你有特殊值存在字符串中,确保转换成byte切片来获取全部数据:

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)
}

对map进行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来执行上面的代码,你总会得到同样的顺序,因为这个网站除非你改了代码,否则并不会重新编译代码。

switch中的fallthrough语句

switch语句中case分支默认自带break,这和默认执行下一个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)
}

可以使用fallthrough关键字来强制当前case块执行完成后执行下一个case块,当然也可以改写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 //error
fmt.Println(data[i++]) //error
}


package main

import "fmt"

func main() {
data := []int{1,2,3}
i := 0
i++
fmt.Println(data[i])
}

很多语言中使用~来进行取反,但go中复用了XOR操作符^

package main

import "fmt"

func main() {
fmt.Println(~2) //error
}

package main

import "fmt"

func main() {
var d uint8 = 2
fmt.Printf("%08b\n",^d)
}

这可能让人困惑,如果你愿意,可以使用XOR操作来代替一元的取反操作,比如使用0x02 XOR 0xFF 替代 NOT 0x02,这也解释了为什么^可以重用来进行取反操作了。

go中也提供了一个特殊的操作符&^来执行AND NOT操作,但这增加了NOT操作的混乱性。这看上去就像是没有括号的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))
}

运算符优先级

除了&^,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字段无法被encode

小写字母开头的字段成员是无法被外部直接访问的,所以struct在进行json、xml、gob等格式的encode操作时,这些私有字段会被忽略,导出时得到零值:

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:""}
}

程序结束时还有goroutine在运行

程序并不等待所有的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] is running 
[1] is running
all done!

一个常见的解决方案是使用WaitGroup,他将阻塞主goroutine等待其他goroutine执行完成。如果goroutine需要长时间运行的来处理消息,可以直接发送kill命令给每个goroutine,或者关闭所有goroutin都需要接收的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)
}
[0] is running 
[0] is done
[1] is running
[1] is done

看起来好像是所有的goroutine都已经执行完成了,然而你会看到报错:

fatal error: all goroutines are asleep - deadlock!

WTF??这个问题的主要原因在于每个goroutine得到的都是原始WaitGroup的一个副本,当goroutine内部执行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
}
}
}

上面的代码才是正确的姿势。(Roy注:总之一句话,当把WaitGroup传递给goroutine时请传递指针)

向无缓冲的channel发送数据时,只要recevier准备好了就会继续向下执行

发送方不会阻塞直到接收方处理这个消息。取决于运行下列代码的环境,接收方可能有时间处理消息也可能来不及处理消息。

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
}

(Roy注:上面的代码可能执行多次结果不同,有时候可以看到”processed cmd.2”,有时候主goroutine把cmd.2丢掉channel里就结束退出了。但如果goroutine中一直没有处理channel中的消息,那么程序就会一直阻塞等待channel中cmd.1这个数据被消费而报错)

向close的channel发送数据会panic

从一个已经关闭的channel读取数据时安全的,如果ok字段接收到的值为false则代表没有数据可以接受了(Roy注:这里应该是指channel关闭了),对于有缓冲的channel,会将缓存中的数据全部取出后ok返回false

向一个关闭的channel发送数据会引发panic,虽然文档中记录很清楚,但很多新人还是会掉进这个坑:

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)
}

根据程序不同,修改方法也是不同的,可以大改也可以小改,但不论如何都要保证程序不会向关闭的channel发送数据。

上面的例子可以使用一个专门用于通知取消的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值的channel

对一个nil值的channel读取或发送数据会永远阻塞,文档里也写的挺清楚,不过还是有新手会跳进这个坑:

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块的方法:

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)
}

使用值传递的方法不能改变参数原有值

结构体方法的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。

例外是map或slice类型的变量,并且是以指针方式更新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]
}

关闭HTTP响应体

当使用标准http库获取响应时,即便不需要获取响应体的内容也需要手动关闭它,即便是一个空响应也是如此。这点很容易忘记,或者方法不对:

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))
}

上面的代码在响应成功时没问题,但如果响应失败,resp为nil,就会引发运行时错误。

最常见的方法是在检查响应后使用defer关闭响应体:

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是非空值。然而如果得到的是重定向失败这2者都是非空值,也就是说程序还会泄露。

可以通过在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()实现是读取响应体的数据之后丢弃,保证了keep-alive的HTTP连接能重用处理不止一个请求。但Go的最新版本将读取并丢弃数据的任务交给了用户,如果你不处理,HTTP连接可能会直接关闭而非重用,参考在Go1.5版本文档。

如果重用http链接对你的程序是否重要,你也许需要添加类似下面的代码在响应处理逻辑中:

_, err = io.Copy(ioutil.Discard, resp.Body)

如果使用正确的方法读取全部的响应体,这是非常重要的。比如你处理一个json api的响应时:

json.NewDecoder(resp.Body).Decode(&data)

关闭HTTP连接

有些HTTP服务器会通过使用keep-alive来保持连接,而默认情况下标准http库只有当服务器要求时才会关闭连接。也就是说,你的程序可能会在某种场景下耗尽socket描述符。

可以通过设置Close字段的值为true,来要求标准http库在请求完成后关闭连接。

另一个选项是添加Connection请求头并设置为close。目标服务器收到后会返回响应并在header中携带Connection: close,当标准库收到这个响应头就会关闭连接。

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 transport:

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服务器发送请求,那么收到响应后立即关闭网络连接是一个好主意。

或者提高文件描述符数量限制也是个好主意,取决于实际情况来选择解决方案。

JSON解码时会添加换行符

当使用JSON Encoder 对象时,那么将在编码后的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方法添加换行符的原因。文档中有写,不过通常被忽略。

JSON包转义键和字符串值中的特殊HTML字符

这也是文档里有的,不过需要特别仔细的查看所有JSON相关的文档才能发现这个。 SetEscapeHTML方法描述了关于一些特殊字符的encoding默认行为。

这是一个很糟糕的设计,首先你不能禁用这个行为在json.Marshal时,其次这引起很糟糕的安全问题因为它假设所有的web程序都能够做出充足的xss漏洞检测。最后它假设所有的用例都是为了web页面的,打破了默认的REST/HTTP APIs配置库。

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
}

将JSON中的数字解码为interface类型

默认情况下,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)
}

有几种方法处理:

使用float类型声明字典值的类型。

将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来解码并指定使用数字类型:

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)
}

也可以使用字符串来表示数值类型:

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)
}

使用结构体来接收转码后的数据并定义需要的类型:

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}
}

如果需要延时解码或某个字段类型是可变的,可以使用结构体结合json.RawMessage

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)
}
}

JSON字符串值不适合十六进制或其他非utf8转义序列

go期望所有的字符串都是utf8编码,这意味着不能在JSON字符串中使用任意十六进制转义的二进制数据(还必须转义反斜杠字符),这是一个非常常见的坑。

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
}

}

如果你确实需要在字符串中使用反斜杠,确保用另一个反斜杠来转义它。如果想使用十六进制编码的二进制数据,可以转义反斜杠,然后将十六进制转义与解码数据存在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中使用byte数组或切片,但这需要将数据进行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调用不会失败,但获得的字符串值可能不是预期的。

比较结构体、数组、切片和字典

可以使用相等运算符==来比较结构体变量,前提是两个结构体的成员都是可比较的类型:

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中也提供了很多函数来进行比较操作,最常用的就是reflect包中的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
}

除了速度慢一点外,这个函数也有个小陷阱:

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()不同:

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或string中的英文文本,可以使用bytes或strings包的ToUpper()ToLower()函数将大小写统一后再使用==bytes.Equal()bytes.Compare()比较。比较非英语的byte或string,应使用bytes.EqualFold()strings.EqualFold()

如果byte切片中含有验证用户身份的数据(密文哈希、token 等),不应再使用reflect.DeepEqual()bytes.Equal()bytes.Compare()。这三个函数容易对程序造成timing attacks,此时应使用crypto/subtle包中的subtle.ConstantTimeCompare()等函数。

从panic中恢复

在defer函数中 直接调用 recover()函数可以捕获、中断panic:

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")
}

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")
}

Slice,Array,Map在range语句中更新引用元素的值

在range操作中得到的值是原始元素的拷贝,也就是说对其修改并不会影响原始数据,换言之得到的指针也不会指向原始数据。

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}
}

slice中的隐藏数据

当你重新划分一个slice时,新的slice将引用原有slice的数组。如果你忘了这个行为的话,在你的应用分配大量临时的slice用于创建新的slice来引用原有数据的一小部分时,会导致难以预期的内存使用:

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>
}

为了避免这样要确保从临时的slice中拷贝数据(而不是重新划分slice):

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>
}

slice数据“损坏”

假设需要重新一个路径(在slice中保存)。你通过修改第一个文件夹的名字,然后把名字合并来创建新的路劲,来重新划分指向各个文件夹的路径:

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”。这个情况的发生是因为两个文件夹的slice都潜在的引用了同一个原始的路径slice。这意味着原始路径也被修改了。根据你的应用,这也许会是个问题。

通过分配新的slice并拷贝需要的数据,你可以修复这个问题。另一个选择是使用完整的slice表达式:

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))
}

完整的slice表达式中的额外参数可以控制新的slice的容量。现在在那个slice后添加元素将会触发一个新的buffer分配,而不是覆盖第二个slice中的数据。

陈旧的slices

多个slice可以引用同一个数据。比如,当从一个已有的slice创建一个新的slice时,这就会发生。如果你的应用功能需要这种行为,那么你将需要关注下“陈旧的”slice。

在某些情况下,在一个slice中添加新的数据,在原有数组无法保持更多新的数据时,将导致分配一个新的数组。而现在其他的slice还指向老的数组(和老的数据)。

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]
}

(Roy注:凡是用到slice操作的时候一定要注意底层指向的数组是不是同一个。)

类型声明和方法

当使用旧类型(非interface)创建新的类型时,新类型并不会继承旧类型的方法:

package main

import "sync"

type myMutex sync.Mutex

func main() {
var mtx myMutex
mtx.Lock() //error
mtx.Unlock() //error
}

如果你确实需要原有类型的方法,你可以定义一个新的struct类型,用匿名方式把原有类型嵌入其中。

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语句也是同理。

for循环中的迭代和闭包

这是一个很常见的陷阱,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)
}

(Roy注:会输出 one tow three 顺序随机,因为传递的是地址)

Defer函数调用参数的求值

对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)
}

但对于指针类型又有所不同:

package main

import (
"fmt"
)

func main() {
i := 1
defer func (in *int) { fmt.Println("result =>", *in) }(&i)

i = 2
//prints: result => 2
}

defer函数执行时机

对defer延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。

比如在一个长时间执行的函数里,内部 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,直接调用f.Close()

失败的类型断言

在类型断言语句中,断言失败则会返回目标类型的“零值”,断言变量与原来变量混用可能出现异常情况:

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)
}
}

阻塞的Goroutine和资源泄露

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来进行数据处理,将第一个值写入到channel中。

那么其他的goroutine的搜索结果怎么处理?关于这些goroutine本身又会如何?

Frist函数中result这个channel是无缓冲的,也就是说只有第一个goroutine会返回并结束,而其他的goroutine将阻塞到向channel中写数据的地方。也就是说造成了资源泄露。

避免这个问题需要保证所有的goroutine都退出,一种解决方案就是使用有缓冲的channel确保每个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语句来解决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
}

也可以使用一个取消channel来通知goroutine退出:

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
}

为何在演讲中会包含这些bug?Rob Pike仅仅是不想把演示复杂化。这么作是合理的,但对于Go新手而言,可能会直接使用代码,而不去思考它可能有问题。

0大小的变量初始地址相同

不同的变量不应该有不同的内存地址么?go中有个小陷阱,0大小(zero-size)的变量可能指向了同一个内存地址:

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不一定总是从0开始

看起来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实际上是定义常量语句块内部的行数,所以如果不在第一行使用的话,他的值就不会是0。

使用指针作为方法的receiver

只要值是可取址的,那在这个值上调用指针接收方法是没问题的。换句话说,在某些情况下,你不需要在有一个接收值的方法版本。

然而并不是所有的变量是可取址的。Map的元素就不是。通过interface引用的变量也不是。

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
}

更新Map的值

如果你有一个struct值的map,你无法更新单个的struct值,因为 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

更让人困惑得是,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}]
}

注意:不久前gccgo编译器可更新map struct元素的字段值,不过很快便修复了,官方认为是Go1.3的潜在特性,无需及时实现,依旧在todo list中。

更新map中struct元素的字段值,第一个方法是使用临时变量:

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}]
}

另一个是使用指向map的指针:

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?" //???
}

(Roy注:会报错panic: runtime error: invalid memory address or nil pointer dereference)

“nil” Interfaces和”nil” Interfaces的值

这在Go中是第二最常见的陷阱,因为interface虽然看起来像指针,但并不是指针。interface变量仅在类型和值为“nil”时才为“nil”。

interface的类型和值会根据用于创建对应interface变量的类型和值的变化而变化。当你检查一个interface变量是否等于“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'
}

一定要小心这个陷阱当你的函数返回interface时:

// 错误代码
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
}
}

栈和堆变量

你并不总是知道变量是分配到栈还是堆上。在C++中,使用new创建的变量总是在堆上。在Go中,即使是使用new()或者make()函数来分配,变量的位置还是由编译器决定。编译器根据变量的大小和“泄露分析”的结果来决定其位置。这也意味着在局部变量上返回引用是没问题的,而这在C或者C++这样的语言中是不行的。

如果你想知道变量分配的位置,在“go build”或“go run”上传入“-m” gc标志(即,go run -gcflags -m app.go)。

GOMAXPROCS、并发和并行

Go1.4及以下版本,程序只会使用1个执行上下文/OS线程,即任何时间都最多只有1个goroutine在执行。

Go1.5版本将可执行上下文的数量设置为 runtime.NumCPU() 返回的逻辑CPU核心数,这个数与系统实际总的CPU逻辑核心数是否一致,取决于你的CPU分配给程序的核心数,可以使用GOMAXPROCS环境变量或者动态的使用runtime.GOMAXPROCS()来调整。

一个常见的误解是,GOMAXPROCS表示用来运行goroutineCPU的数量,runtime.GOMAXPROCS()函数文档更让人困惑。

GOMAXPROCS变量描述更好的解释了关于OS线程问题。

你可以设置GOMAXPROCS的值大于CPU数量,在1.10版本已经不再对这个进行限制。最大值从256提升到了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变量有多个不同的组合:

最有趣的组合式是”02”。这表明b在a之前更新了。

如果你需要在多goroutine内放置读写顺序的变化,你将需要使用channel,或者使用”sync”包构建合适的结构体。

有可能会出现这种情况,一个无耻的goroutine阻止其他goroutine运行。当你有一个不让调度器运行的for循环时,这就会发生。

package main

import "fmt"

func main() {
done := false

go func(){
done = true
}()

for !done {
}
fmt.Println("done!")
}

for循环并不需要是空的。只要它包含了不会触发调度执行的代码,就会发生这种问题。

调度器会在GC、“go”声明、阻塞channel操作、阻塞系统调用和lock操作后运行。它也会在非内联函数调用后执行。

package main

import "fmt"

func main() {
done := false

go func(){
done = true
}()

for !done {
fmt.Println("not done!") //not inlined
}
fmt.Println("done!")
}

要想知道你在for循环中调用的函数是否是内联的,你可以在“go build”或“go run”时传入“-m” gc标志(如,go build -gcflags -m)。

也可以使用 runtime 包中的 Gosched() 来 手动启动调度器:

package main

import (
"fmt"
"runtime"
)

func main() {
done := false

go func(){
done = true
}()

for !done {
runtime.Gosched()
}
fmt.Println("done!")
}

注意上面的代码存在竞态条件(race condition),这里仅仅为了演示这个坑。

引入C模块和多行导入

你需要引入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))
}

引入C包不能和其他包一起:

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: could not determine kind of name for C.free

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: could not determine kind of name for C.free,确保不会有任何空行。

使用可变参数不能调用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: unexpected type: ...

您必须将可变参数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))
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK