50 Shades of Go:新 Golang 开发人员的陷阱、陷阱和常见错误

50 Shades of Go 在其他语言里

概述

围棋是一种简单而有趣的语言,但是,像任何其他语言一样,它有一些陷阱......其中许多问题并不完全是 Go 的错。如果您来自另一种语言,其中一些错误是自然陷阱。其他原因是由于错误的假设和缺少细节。

如果您花时间阅读官方规范、wiki、邮件列表讨论、Rob Pike 的许多精彩帖子和演示以及源代码来学习语言,那么很多这些陷阱可能看起来很明显。不过,不是每个人都以同样的方式开始,这没关系。如果您是 Go 新手,此处的信息将为您节省调试代码的时间。

总初学者

中级初级

高级初学者

Cgo(又名 Brave Beginner)



陷阱、陷阱和常见错误

开大括号不能放在单独的行上
  • 级别:初级

在大多数使用大括号的其他语言中,您可以选择放置大括号的位置。围棋则不同。您可以感谢自动分号注入(无前瞻)的这种行为。是的,Go 确实有分号 :-)

失败:

package main

import "fmt"

func main()  
{ //error, can't have the opening brace on a separate line
    fmt.Println("hello there!")
}

编译错误:

/tmp/sandbox826898458/main.go:6:语法错误:{

工程:

package main

import "fmt"

func main() {  
    fmt.Println("works!")
}
未使用的变量
  • 级别:初级

如果您有一个未使用的变量,您的代码将无法编译。不过有一个例外。您必须使用在函数中声明的变量,但如果有未使用的全局变量,则没关系。使用未使用的函数参数也是可以的。

如果为未使用的变量分配新值,则代码仍将无法编译。您需要以某种方式使用变量值来使编译器满意。

失败:

package main

var gvar int //not an error

func main() {  
    var one int   //error, unused variable
    two := 2      //error, unused variable
    var three int //error, even though it's assigned 3 on the next line
    three = 3

    func(unused string) {
        fmt.Println("Unused arg. No compile error")
    }("what?")
}

编译错误:

/tmp/sandbox473116179/main.go:6:一个已声明但未使用 /tmp/sandbox473116179/main.go:7:两个已声明但未使用 /tmp/sandbox473116179/main.go:8:三个已声明但未使用

工程:

package main

import "fmt"

func main() {  
    var one int
    _ = one

    two := 2 
    fmt.Println(two)

    var three int 
    three = 3
    one = three

    var four int
    four = four
}

另一种选择是注释掉或删除未使用的变量:-)

未使用的进口
  • 级别:初级

如果导入包时未使用其任何导出的函数、接口、结构或变量,则代码将无法编译。

如果确实需要导入的包,可以使用空白标识符 作为其包名,以避免此编译失败。空白标识符用于导入包的副作用。_

失败:

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
}

另一种选择是删除或注释掉未使用的导入:-)goimports 工具可以帮助您。

短变量声明只能在函数内部使用
  • 级别:初级

失败:

package main

myvar := 1 //error

func main() {  
}

编译错误:

/tmp/sandbox265716165/main.go:3:函数体外部的非声明语句

工程:

package main

var myvar = 1

func main() {  
}
使用简短的变量声明重新声明变量
  • 级别:初级

不能在独立语句中重新声明变量,但在至少声明一个新变量的多变量声明中是允许的。

重新声明的变量必须位于同一块中,否则最终会得到一个阴影变量。

失败:

package main

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

编译错误:

/tmp/sandbox706333626/main.go:5:左侧没有新变量 :=

工程:

package main

func main() {  
    one := 0
    one, two := 1,2

    one,two = two,one
}
不能使用短变量声明来设置字段值
  • 级别:初级

失败:

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::=
左侧的非名称 data.result

即使有一张票可以解决这个问题,它也不太可能改变,因为 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 命令来查找其中的一些问题。默认情况下,不会执行任何阴影变量检查。确保使用以下标志:vet-shadowgo tool vet -shadow your_file.go

请注意,该命令不会报告所有阴影变量。使用 go-nyet 进行更积极的阴影变量检测。vet

不能使用“nil”初始化没有显式类型的变量
  • 级别:初级

“nil”标识符可用作接口、函数、指针、映射、切片和通道的“零值”。如果未指定变量类型,编译器将无法编译代码,因为它无法猜测类型。

失败:

package main

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

    _ = x
}

编译错误:

/tmp/sandbox188239583/main.go:4:使用非类型化 nil

工程:

package main

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

    _ = x
}
使用“nil”切片和地图
  • 级别:初级

将项目添加到“无”切片是可以的,但对地图执行相同的操作会产生运行时恐慌。

工程:

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:上限的参数 m(类型 map[string]int)无效

字符串不能为“nil”
  • 级别:初级

对于习惯于为字符串变量分配“nil”标识符的开发人员来说,这是一个难题。

失败:

package main

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

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

编译错误:

/tmp/sandbox630560459/main.go:4:不能在赋值中使用 nil 作为类型字符串 /tmp/sandbox630560459/main.go:6:无效操作:x == nil(字符串和 nil 类型不匹配)

工程:

package main

func main() {  
    var x string //defaults to "" (zero value)

    if x == "" {
        x = "default"
    }
}
数组函数参数
  • 级别:初级

如果您是 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]
}
Slice 和 Array “range” 子句中的意外值
  • 级别:初级

如果您习惯了其他语言中的“for-in”或“foreach”语句,则可能会发生这种情况。Go 中的“range”子句是不同的。它生成两个值:第一个值是项目索引,第二个值是项目数据。

坏:

package main

import "fmt"

func main() {  
    x := []string{"a","b","c"}

    for v := range x {
        fmt.Println(v) //prints 0, 1, 2
    }
}

好:

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

有一个关于多维数组和切片的规范/提案,但目前看起来它是一个低优先级的功能。

访问不存在的映射键
  • 级别:初级

对于希望获得“零”标识符的开发人员来说,这是一个陷阱(就像在其他语言中所做的那样)。如果相应数据类型的“零值”为“nil”,则返回值将为“nil”,但对于其他数据类型,返回值将有所不同。检查适当的“零值”可用于确定映射记录是否存在,但它并不总是可靠的(例如,如果您有一个布尔值映射,其中“零值”为假,您会怎么做)。了解给定地图记录是否存在的最可靠方法是检查地图访问操作返回的第二个值。

坏:

package main

import "fmt"

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

    if v := x["two"]; v == "" { //incorrect
        fmt.Println("no entry")
    }
}

好:

package main

import "fmt"

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

    if _,ok := x["two"]; !ok {
        fmt.Println("no entry")
    }
}
字符串是不可变的
  • 级别:初级

尝试使用索引运算符更新字符串变量中的单个字符将导致失败。字符串是只读字节切片(具有一些额外的属性)。如果确实需要更新字符串,则使用字节片,并在必要时将其转换为字符串类型。

失败:

package main

import "fmt"

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

    fmt.Println(x)
}

编译错误:

/tmp/sandbox305565531/main.go:7:无法分配给 x[0]

工程:

package main

import "fmt"

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

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

请注意,这并不是更新文本字符串中字符的正确方法,因为给定字符可以存储在多个字节中。如果您确实需要对文本字符串进行更新,请先将其转换为符文切片。即使使用符文切片,单个字符也可能跨越多个符文,例如,如果您的角色带有严重的口音,则可能会发生这种情况。“字符”的这种复杂性和模糊性是 Go 字符串表示为字节序列的原因。

字符串和字节片之间的转换
  • 级别:初级

当您将字符串转换为字节片时(反之亦然),您将获得原始数据的完整副本。它不像其他语言中的强制转换操作,也不像重新切片,其中新的切片变量指向原始字节切片使用的相同底层数组。

Go 确实对 to 和 to 转换进行了一些优化,以避免额外的分配(在待办事项列表中有更多优化)。[]bytestringstring[]byte

第一个优化避免了在使用键查找集合中的条目时的额外分配: 。[]bytemap[string]m[string(key)]

第二个优化避免了在字符串转换为 : 的子句中的额外分配。for range[]bytefor i,v := range []byte(str) {...}

字符串和索引运算符
  • 级别:初级

字符串上的索引运算符返回一个字节值,而不是一个字符(就像在其他语言中所做的那样)。

package main

import "fmt"

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

如果需要访问特定的字符串“字符”(unicode 代码点/符文),请使用该子句。官方的“unicode/utf8”包和实验性的 utf8string 包 (golang.org/x/exp/utf8string) 也很有用。utf8string 包包含一个方便的方法。将字符串转换为符文片段也是一种选择。for rangeAt()

字符串并不总是 UTF8 文本
  • 级别:初级

字符串值不需要是 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
}

内置函数返回字节数,而不是像 Python 中的 unicode 字符串那样返回字符数。len()

要在 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:语法错误:复合文本中换行符前需要尾随逗号 /tmp/sandbox367520156/main.go:8:函数体外部的非声明语句 /tmp/sandbox367520156/main.go:9:语法错误:意外 }

工程:

package main

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

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

如果在将声明折叠为单行时保留尾随逗号,则不会收到编译器错误。

日志。致命和日志。恐慌不仅仅是记录
  • 级别:初级

日志记录库通常提供不同的日志级别。与这些日志库不同,Go 中的日志包在调用其 and 函数时所做的不仅仅是日志。当您的应用程序调用这些函数时,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 具有许多原生支持并发的功能,但并发安全数据收集并不是其中之一:-)你有责任确保数据收集更新是原子的。Goroutines 和 channels 是实现这些原子操作的推荐方法,但如果“sync”包对你的应用程序有意义,你也可以利用它。

“range”子句中字符串的迭代值
  • 级别:初级

索引值(“range”操作返回的第一个值)是第二个值中返回的当前“字符”(unicode 码位/符文)的第一个字节的索引。它不像在其他语言中那样是当前“字符”的索引。请注意,一个实际角色可能由多个符文表示。如果您需要使用角色,请务必查看“规范”包 (golang.org/x/text/unicode/norm)。

带有字符串变量的子句将尝试将数据解释为 UTF8 文本。对于它不理解的任何字节序列,它将返回0xfffd符文(又名 unicode 替换字符)而不是实际数据。如果字符串变量中存储了任意(非 UTF8 文本)数据,请确保将它们转换为字节切片,以便按原样获取所有存储的数据。for range

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”子句遍历映射
  • 级别:初级

如果您希望项目按特定顺序排列(例如,按键值排序),则这是一个陷阱。每次地图迭代将产生不同的结果。Go 运行时尝试在迭代顺序上加倍努力,但并不总是成功,因此您可能会获得多个相同的地图迭代。看到连续 5 次相同的迭代,不要感到惊讶。

package main

import "fmt"

func main() {  
    m := map[string]int{"one":1,"two":2,"three":3,"four":4}
    for k,v := range m {
        fmt.Println(k,v)
    }
}

如果你使用 Go Playground (https://play.golang.org/),你总是会得到相同的结果,因为除非你进行更改,否则它不会重新编译代码。

“switch”语句中的失败行为
  • 级别:初级

默认情况下,“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 //error
    fmt.Println(data[i++]) //error
}

编译错误:

/tmp/sandbox101231828/main.go:8:语法错误:意外 ++ /tmp/sandbox101231828/main.go:9:语法错误:意外 ++,期望:

工程:

package main

import "fmt"

func main() {  
    data := []int{1,2,3}
    i := 0
    i++
    fmt.Println(data[i])
}
按位 NOT 运算符
  • 级别:初级

许多语言使用一元 NOT 运算符(又名按位补码),但 Go 为此重用了 XOR 运算符 ()。~^

失败:

package main

import "fmt"

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

编译错误:

/tmp/sandbox965529189/main.go:6:按位补码运算符为 ^

工程:

package main

import "fmt"

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

Go 仍然用作 XOR 运算符,这可能会让某些人感到困惑。^

如果需要,可以用二进制 XOR 运算(例如 )表示一元 NOT 运算(例如 )。这可以解释为什么重用于表示一元 NOT 操作。NOT 0x020x02 XOR 0xff^

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
}
未导出的结构字段不编码
  • 级别:初级

以小写字母开头的结构字段不会进行编码(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 的应用退出
  • 级别:初级

该应用程序不会等待所有 goroutines 完成。对于初学者来说,这是一个常见的错误。每个人都从某个地方开始,所以犯菜鸟错误并不可耻:-)

package main

import (  
    "fmt"
    "time"
)

func main() {  
    workerCount := 2

    for i := 0; i < workerCount; i++ {
        go doit(i)
    }
    time.Sleep(1 * time.Second)
    fmt.Println("all done!")
}

func doit(workerId int) {  
    fmt.Printf("[%v] is running\n",workerId)
    time.Sleep(3 * time.Second)
    fmt.Printf("[%v] is done\n",workerId)
}

你会知道的:

[0] 正在运行
[1] 正在运行
全部完成!

最常见的解决方案之一是使用“WaitGroup”变量。它将允许主 goroutine 等待,直到所有工作线程 goroutine 完成。如果你的应用有长时间运行的带有消息处理循环的工作线程,你还需要一种方法来向这些 goroutine 发出信号,表明是时候退出了。您可以向每个工作人员发送“杀死”消息。另一种选择是关闭所有工作人员接收的通道。这是一种一次向所有 goroutine 发出信号的简单方法。

package main

import (  
    "fmt"
    "sync"
)

func main() {  
    var wg sync.WaitGroup
    done := make(chan struct{})
    workerCount := 2

    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doit(i,done,wg)
    }

    close(done)
    wg.Wait()
    fmt.Println("all done!")
}

func doit(workerId int,done <-chan struct{},wg sync.WaitGroup) {  
    fmt.Printf("[%v] is running\n",workerId)
    defer wg.Done()
    <- done
    fmt.Printf("[%v] is done\n",workerId)
}

如果运行此应用,则会看到:

[0] 正在运行
[0] 完成
[1] 正在运行
[1] 完成

看起来工人在主 goroutine 退出之前就完成了。伟大!但是,您还会看到以下内容:

致命错误:所有 goroutines 都处于睡眠状态 - 死锁!

那不是很好:-)这是怎么回事?为什么会出现僵局?工人们离开了,他们被处决了.该应用程序应该可以工作。wg.Done()

之所以发生死锁,是因为每个工作线程都会获得原始“WaitGroup”变量的副本。当 worker 执行时,它对主 goroutine 中的“WaitGroup”变量没有影响。wg.Done()

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
}
发送到关闭的通道会导致恐慌
  • 级别:初级

从封闭的通道接收是安全的。receive 语句中的返回值将设置为指示未收到任何数据。如果您从缓冲通道接收,您将首先获得缓冲数据,一旦它为空,返回值将为 。okfalseokfalse

将数据发送到封闭通道会导致恐慌。这是一个记录在案的行为,但对于可能希望发送行为与接收行为相似的新 Go 开发人员来说,它不是很直观。

package main

import (  
    "fmt"
    "time"
)

func main() {  
    ch := make(chan int)
    for i := 0; i < 3; i++ {
        go func(idx int) {
            ch <- (idx + 1) * 2
        }(i)
    }

    //get the first result
    fmt.Println(<-ch)
    close(ch) //not ok (you still have other senders)
    //do other work
    time.Sleep(2 * time.Second)
}

根据您的应用程序,修复程序会有所不同。这可能是一个微小的代码更改,或者可能需要更改应用程序设计。无论采用哪种方式,都需要确保应用程序不会尝试将数据发送到封闭通道。

可以通过使用特殊的取消渠道来修复错误示例,以向剩余的工人发出信号,表明他们的结果不再被取消。

package main

import (  
    "fmt"
    "time"
)

func main() {  
    ch := make(chan int)
    done := make(chan struct{})
    for i := 0; i < 3; i++ {
        go func(idx int) {
            select {
            case ch <- (idx + 1) * 2: fmt.Println(idx,"sent result")
            case <- done: fmt.Println(idx,"exiting")
            }
        }(i)
    }

    //get first result
    fmt.Println("result:",<-ch)
    close(done)
    //do other work
    time.Sleep(3 * time.Second)
}
使用“nil”通道
  • 级别:初级

在通道块上发送和接收操作。这是一个有据可查的行为,但对于新的 Go 开发人员来说可能是一个惊喜。nil

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!

此行为可用作动态启用和禁用语句中的块的方法。caseselect

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 响应正文
  • 级别:中级

当您使用标准 http 库发出请求时,您会得到一个 http 响应变量。如果您不阅读响应正文,您仍然需要关闭它。请注意,您也必须对空响应执行此操作。这很容易被遗忘,尤其是对于新的 Go 开发人员。

一些新的 Go 开发人员确实尝试关闭响应正文,但他们在错误的地方这样做。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    resp, err := http.Get("https://api.ipify.org?format=json")
    defer resp.Body.Close()//not ok
    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

此代码适用于成功的请求,但如果 http 请求失败,则变量可能是 ,这将导致运行时崩溃。respnil

关闭响应正文的最常见原因是在 http 响应错误检查后使用调用。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 请求失败时,变量将是 ,变量将是 。但是,当您遇到重定向失败时,两个变量都将是 。这意味着您仍然可能最终导致泄漏。respnilerrnon-nilnon-nil

您可以通过在 http 响应错误处理块中添加对关闭响应正文的调用来修复此泄漏。另一种选择是使用一个调用来关闭所有失败和成功请求的响应正文。non-nildefer

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

的原始实现还读取和丢弃剩余的响应正文数据。这确保了在启用 keepalive http 连接行为时,http 连接可以重新用于另一个请求。最新的 http 客户端行为是不同的。现在,您有责任读取和丢弃剩余的响应数据。如果不这样做,http 连接可能会被关闭,而不是被重用。这个小问题应该记录在 Go 1.5 中。resp.Body.Close()

如果重用 http 连接对应用程序很重要,则可能需要在响应处理逻辑的末尾添加如下内容:

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

如果您不立即读取整个响应正文,这将是必要的,如果您使用如下代码处理 json API 响应,则可能会发生这种情况:

json.NewDecoder(resp.Body).Decode(&data)  
关闭 HTTP 连接
  • 级别:中级

某些 HTTP 服务器会在一段时间内保持网络连接打开状态(基于 HTTP 1.1 规范和服务器“保持活动”配置)。默认情况下,标准 http 库仅在目标 HTTP 服务器请求时才会关闭网络连接。这意味着在某些情况下,您的应用可能会用完套接字/文件描述符。

您可以通过将请求变量中的字段设置为 来要求 http 库在请求完成后关闭连接。Closetrue

另一种选择是添加请求标头并将其设置为 。目标 HTTP 服务器也应使用标头进行响应。当 http 库看到此响应标头时,它也会关闭连接。ConnectioncloseConnection: 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 传输配置。

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 编码函数编写测试,因为 你没有得到预期的值。发生了什么事?如果您使用的是 JSON 编码器对象,则您将在编码的 JSON 对象的末尾获得一个额外的换行符。

package main

import (
  "fmt"
  "encoding/json"
  "bytes"
)

func main() {
  data := map[string]int{"key": 1}
  
  var b bytes.Buffer
  json.NewEncoder(&b).Encode(data)

  raw,_ := json.Marshal(data)
  
  if b.String() == string(raw) {
    fmt.Println("same encoded data")
  } else {
    fmt.Printf("'%s' != '%s'\n",raw,b.String())
    //prints:
    //'{"key":1}' != '{"key":1}\n'
  }
}

JSON Encoder 对象专为流式处理而设计。使用 JSON 进行流式处理通常意味着换行符分隔的 JSON 对象,这就是 Encode 方法添加换行符的原因。这是一种记录在案的行为,但通常被忽视或遗忘。

JSON 包转义键和字符串值中的特殊 HTML 字符
  • 级别:中级

这是一个记录在案的行为,但你必须仔细阅读所有 JSON 包文档才能了解它。方法说明介绍了 and、小于和大于字符的默认编码行为。SetEscapeHTML

出于多种原因,这是 Go 团队非常不幸的设计决定。首先,您无法为调用禁用此行为。其次,这是一个实现不佳的安全功能,因为它假设执行 HTML 编码足以防止所有 Web 应用程序中的 XSS 漏洞。可以使用许多不同的上下文来使用数据,并且每个上下文都需要自己的编码方法。最后,这很糟糕,因为它假设 JSON 的主要用例是网页,默认情况下会破坏配置库和 REST/HTTP API。json.Marshal

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:接口转换:接口是 float64,而不是 int

如果您尝试解码的 JSON 值是整数,则可以使用服务器选项。

选项一:按原样使用浮点值 :-)

选项二:将浮点值转换为所需的整数类型。

package main

import (  
  "encoding/json"
  "fmt"
)

func main() {  
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  if err := json.Unmarshal(data, &result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status = uint64(result["status"].(float64)) //ok
  fmt.Println("status value:",status)
}

选项三:使用类型对 JSON 进行解组,并告诉它使用接口类型表示 JSON 编号。DecoderNumber

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

选项五:如果需要延迟值解码,请使用将数值映射到类型的 a。structjson.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)
  }
}
JSON 字符串值不适用于十六进制或其他非 UTF8 转义序列
  • 级别:中级

Go 要求字符串值采用 UTF8 编码。这意味着您不能在 JSON 字符串中使用任意十六进制转义二进制数据(并且还必须转义反斜杠字符)。这确实是 Go 继承的 JSON 陷阱,但它在 Go 应用程序中经常发生,因此无论如何都要提及它。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data string `json:"data"`
}

func main() {
  raw := []byte(`{"data":"\xc2"}`)
  var decoded config

  if err := json.Unmarshal(raw, &decoded); err != nil {
        fmt.Println(err)
    //prints: invalid character 'x' in string escape code
    }
  
}

如果 Go 看到十六进制转义序列,则 Unmarshal/Decode 调用将失败。如果你确实需要在字符串中有一个反斜杠,请确保用另一个反斜杠来转义它。如果要使用十六进制编码的二进制数据,可以转义反斜杠,然后使用 JSON 字符串中的解码数据执行自己的十六进制转义。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data string `json:"data"`
}

func main() {
  raw := []byte(`{"data":"\\xc2"}`)
  
  var decoded config
  
  json.Unmarshal(raw, &decoded)
  
  fmt.Printf("%#v",decoded) //prints: main.config{Data:"\\xc2"}
  //todo: do your own hex escape decoding for decoded.Data  
}

另一种选择是在 JSON 对象中使用字节数组/切片数据类型,但二进制数据必须进行 base64 编码。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data []byte `json:"data"`
}

func main() {
  raw := []byte(`{"data":"wg=="}`)
  var decoded config
  
  if err := json.Unmarshal(raw, &decoded); err != nil {
          fmt.Println(err)
      }
  
  fmt.Printf("%#v",decoded) //prints: main.config{Data:[]uint8{0xc2}}
}

其他需要注意的是 Unicode 替换字符 (U+FFFD)。Go 将使用替换字符而不是无效的 UTF8,因此 Unmarshal/Decode 调用不会失败,但你得到的字符串值可能不是你所期望的。

比较结构体、数组、切片和映射
  • 级别:中级

如果每个结构字段都可以与相等运算符进行比较,则可以使用相等运算符 来比较结构变量。==

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
}

除了速度慢(可能会也可能不会破坏您的应用程序)之外,还有自己的陷阱。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()

package main

import (  
    "fmt"
    "bytes"
)

func main() {  
    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) //prints: b1 == b2: true
}

DeepEqual()比较切片并不总是完美的。

package main

import (  
    "fmt"
    "reflect"
    "encoding/json"
)

func main() {  
    var str string = "one"
    var in interface{} = "one"
    fmt.Println("str == in:",str == in,reflect.DeepEqual(str, in)) 
    //prints: str == in: true true

    v1 := []string{"one","two"}
    v2 := []interface{}{"one","two"}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2)) 
    //prints: v1 == v2: false (not ok)

    data := map[string]interface{}{
        "code": 200,
        "value": []string{"one","two"},
    }
    encoded, _ := json.Marshal(data)
    var decoded map[string]interface{}
    json.Unmarshal(encoded, &decoded)
    fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded)) 
    //prints: data == decoded: false (not ok)
}

如果字节切片(或字符串)包含文本数据,则当您需要以不区分大小写的方式比较值时(在使用 、 或 之前),您可能会想使用“字节”和“字符串”包或来自“字节”和“字符串”包。它适用于英语文本,但不适用于许多其他语言的文本。 并应改用。ToUpper()ToLower()==bytes.Equal()bytes.Compare()strings.EqualFold()bytes.EqualFold()

如果字节片包含需要根据用户提供的数据进行验证的机密(例如,加密哈希、令牌等),请不要使用 、 或因为这些函数会使应用程序容易受到计时攻击。为避免泄露时序信息,请使用“crypto/subtle”包中的函数(例如 )。reflect.DeepEqual()bytes.Equal()bytes.Compare()subtle.ConstantTimeCompare()

从恐慌中恢复过来
  • 级别:中级

该函数可用于捕获/拦截恐慌。只有在延迟函数中完成调用时,调用才会起作用。recover()recover()

不對:

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

仅当 to 的调用在延迟函数中直接调用时才有效。recover()

失败:

package main

import "fmt"

func doRecover() {  
    fmt.Println("recovered =>",recover()) //prints: recovered => <nil>
}

func main() {  
    defer func() {
        doRecover() //panic is not recovered
    }()

    panic("not good")
}
更新和引用 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}
}
切片中的“隐藏”数据
  • 级别:中级

对切片进行切片时,新切片将引用原始切片的数组。如果忘记此行为,则如果应用程序分配大型临时切片,从中创建新切片以引用原始数据的小部分,则可能会导致意外的内存使用。

package main

import "fmt"

func get() []byte {  
    raw := make([]byte,10000)
    fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
    return raw[:3]
}

func main() {  
    data := get()
    fmt.Println(len(data),cap(data),&data[0]) //prints: 3 10000 <byte_addr_x>
}

为避免此陷阱,请确保从临时切片中复制所需的数据(而不是对其进行重新切片)。

package main

import "fmt"

func get() []byte {  
    raw := make([]byte,10000)
    fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
    res := make([]byte,3)
    copy(res,raw[:3])
    return res
}

func main() {  
    data := get()
    fmt.Println(len(data),cap(data),&data[0]) //prints: 3 3 <byte_addr_y>
}
切片数据“损坏”
  • 级别:中级

假设您需要重写路径(存储在切片中)。对路径进行切片以引用每个目录,修改第一个文件夹名称,然后组合这些名称以创建新路径。

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/BBBBBBBB”,你最终得到了“AAAAsuffix/uffixBBBB”。发生这种情况是因为两个目录切片都引用了原始路径切片中的相同底层数组数据。这意味着原始路径也被修改了。根据您的应用程序,这也可能是一个问题。

此问题可以通过分配新切片并复制所需的数据来解决。另一种选择是使用完整切片表达式。

package main

import (  
    "fmt"
    "bytes"
)

func main() {  
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')
    dir1 := path[:sepIndex:sepIndex] //full slice expression
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)
    path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB (ok now)

    fmt.Println("new path =>",string(path))
}

完整切片表达式中的额外参数控制新切片的容量。现在,追加到该切片将触发新的缓冲区分配,而不是覆盖第二个切片中的数据。

“陈旧”切片
  • 级别:中级

多个切片可以引用相同的数据。例如,当您从现有切片创建新切片时,可能会发生这种情况。如果应用程序依赖于此行为才能正常运行,则需要担心“过时”切片。

在某些时候,当原始数组无法容纳更多新数据时,将数据添加到其中一个切片将导致新的数组分配。现在,其他切片将指向旧数组(具有旧数据)。

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]
}
类型声明和方法
  • 级别:中级

通过从现有(非接口)类型定义新类型来创建类型声明时,不会继承为该现有类型定义的方法。

失败:

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(类型 myMutex 没有字段或方法 Lock) /tmp/sandbox106401185/main.go:10:mtx。Unlock undefined(类型 myMutex 没有字段或方法 Unlock)

如果确实需要原始类型的方法,可以定义一个新的结构类型,将原始类型嵌入为匿名字段。

工程:

package main

import "sync"

type myLocker struct {  
    sync.Mutex
}

func main() {  
    var lock myLocker
    lock.Lock() //ok
    lock.Unlock() //ok
}

接口类型声明还保留其方法集。

工程:

package main

import "sync"

type myLocker sync.Locker

func main() {  
    var lock myLocker = new(sync.Mutex)
    lock.Lock() //ok
    lock.Unlock() //ok
}
打破“for switch”和“for select”代码块
  • 级别:中级

没有标签的“break”语句只能让您退出内部开关/选择块。如果使用“return”语句不是一个选项,那么为外部循环定义标签是下一个最好的选择。

package main

import "fmt"

func main() {  
    loop:
        for {
            switch {
            case true:
                fmt.Println("breaking out...")
                break loop
            }
        }

    fmt.Println("out!")
}

“goto”语句也可以解决问题......

“for”语句中的迭代变量和闭包
  • 级别:中级

这是围棋中最常见的问题。语句中的迭代变量在每次迭代中都会重用。这意味着在循环中创建的每个闭包(又名函数文字)都将引用相同的变量(并且它们将在这些 goroutine 开始执行时获得该变量的值)。forfor

不對:

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)
}
延迟函数调用参数评估
  • 级别:中级

延迟函数调用的参数在计算语句时计算(而不是在函数实际执行时)。延迟方法调用时,相同的规则也适用。结构值也会与显式方法参数和闭合变量一起保存。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 开发人员来说,将延迟的代码执行规则与变量范围规则混淆是一个容易犯的错误。如果您有一个长时间运行的函数,该函数具有尝试在每次迭代中资源清理调用的循环,则可能会成为一个问题。fordefer

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 在 2012 年 Google I/O 大会上的“Go 并发模式”演讲中谈到了许多基本的并发模式。从多个目标中获取第一个结果就是其中之一。

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 将其搜索结果发送到结果通道。返回结果通道中的第一个值。

其他 goroutine 的结果呢?goroutines 本身呢?

函数中的结果通道是无缓冲的。这意味着只有第一个 goroutine 返回。所有其他 goroutine 都卡在尝试发送其结果时。这意味着,如果有多个副本,则每个调用都会泄漏资源。First()

为了避免泄漏,您需要确保所有 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
}

另一个可能的解决方案是使用带有大小写和缓冲结果通道的语句,该通道可以保存一个值。该案例可确保即使结果通道无法接收消息,goroutines 也不会卡住。selectdefaultdefault

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 的第一次使用并不总是从零开始
  • 级别:中级

标识符似乎就像一个增量运算符。你开始一个新的常量声明,第一次使用时你得到零,第二次使用它时你得到一个,依此类推。但情况并非总是如此。iotaiota

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
}

这实际上是常量声明块中当前行的索引运算符,因此,如果第一次使用的不是常量声明块中的第一行,则初始值将不为零。iotaiota

在值实例上使用指针接收器方法
  • 级别:高级

只要值是可寻址的,就可以对值调用指针接收器方法。换言之,在某些情况下,不需要具有该方法的值接收器版本。

但是,并非每个变量都是可寻址的。地图元素不可寻址。通过接口引用的变量也是不可寻址的。

package main

import "fmt"

type data struct {  
    name string
}

func (p *data) print() {  
    fmt.Println("name:",p.name)
}

type printer interface {  
    print()
}

func main() {  
    d1 := data{"one"}
    d1.print() //ok

    var in printer = data{"two"} //error
    in.print()

    m := map[string]data {"x":data{"three"}}
    m["x"].print() //error
}

编译错误:

/tmp/sandbox017696142/main.go:21:不能在分配中使用数据文本(类型数据)作为类型打印机: 数据未实现打印机(打印方法具有指针接收器)
/tmp/sandbox017696142/main.go:25: 无法在 m[“x”] 上调用指针方法 /tmp/sandbox017696142/main.go:25:无法获取 m[“x”] 的地址

更新地图值字段
  • 级别:高级

如果您有结构值的映射,则无法更新单个结构字段。

失败:

package main

type data struct {  
    name string
}

func main() {  
    m := map[string]data {"x":{"one"}}
    m["x"].name = "two" //error
}

编译错误:

/tmp/sandbox380452744/main.go:9:无法分配给 m[“x”].name

它不起作用,因为地图元素不可寻址。

对于新的 Go 开发人员来说,可能更令人困惑的是切片元素是可寻址的。

package main

import "fmt"

type data struct {  
    name string
}

func main() {  
    s := []data {{"one"}}
    s[0].name = "two" //ok
    fmt.Println(s)    //prints: [{two}]
}

请注意,不久前可以更新其中一个 Go 编译器 (gccgo) 中的地图元素字段,但该行为很快得到修复:-)它也被认为是 Go 1.3 的一个潜在功能。在那个时间点,它不够重要,无法支持,所以它仍然在待办事项列表中。

第一个解决方法是使用临时变量。

package main

import "fmt"

type data struct {  
    name string
}

func main() {  
    m := map[string]data {"x":{"one"}}
    r := m["x"]
    r.name = "two"
    m["x"] = r
    fmt.Printf("%v",m) //prints: map[x:{two}]
}

另一种解决方法是使用指针映射。

package main

import "fmt"

type data struct {  
    name string
}

func main() {  
    m := map[string]*data {"x":{"one"}}
    m["x"].name = "two" //ok
    fmt.Println(m["x"]) //prints: &{two}
}

顺便问一下,当你运行这段代码时会发生什么?

package main

type data struct {  
    name string
}

func main() {  
    m := map[string]*data {"x":{"one"}}
    m["z"].name = "what?" //???
}
“nil” 接口和“nil” 接口值
  • 级别:高级

这是 Go 中第二个最常见的问题,因为接口不是指针,即使它们看起来像指针。仅当接口变量的类型和值字段为“nil”时,接口变量才会为“nil”。

接口类型和值字段根据用于创建相应接口变量的变量的类型和值进行填充。当您尝试检查接口变量是否等于“nil”时,这可能会导致意外行为。

package main

import "fmt"

func main() {  
    var data *byte
    var in interface{}

    fmt.Println(data,data == nil) //prints: <nil> true
    fmt.Println(in,in == nil)     //prints: <nil> true

    in = data
    fmt.Println(in,in == nil)     //prints: <nil> false
    //'data' is 'nil', but 'in' is not 'nil'
}

当您有一个返回接口的函数时,请注意此陷阱。

不對:

package main

import "fmt"

func main() {  
    doit := func(arg int) interface{} {
        var result *struct{} = nil

        if(arg > 0) {
            result = &struct{}{}
        }

        return result
    }

    if res := doit(-1); res != nil {
        fmt.Println("good result:",res) //prints: good result: <nil>
        //'res' is not 'nil', but its value is 'nil'
    }
}

工程:

package main

import "fmt"

func main() {  
    doit := func(arg int) interface{} {
        var result *struct{} = nil

        if(arg > 0) {
            result = &struct{}{}
        } else {
            return nil //return an explicit 'nil'
        }

        return result
    }

    if res := doit(-1); res != nil {
        fmt.Println("good result:",res)
    } else {
        fmt.Println("bad result (res is nil)") //here as expected
    }
}
堆栈和堆变量
  • 级别:高级

你并不总是知道你的变量是在堆栈上分配的还是在堆上分配的。在 C++ 中,使用运算符创建变量始终意味着您有一个堆变量。在 Go 中,编译器决定变量的分配位置,即使使用了 or 函数也是如此。编译器根据变量的大小和“转义分析”的结果选择存储变量的位置。这也意味着可以返回对局部变量的引用,这在 C 或 C++ 等其他语言中是不行的。newnew()make()

如果你需要知道你的变量被分配到哪里,请将 “-m” gc 标志传递给 “go build” 或 “go run”(例如,)。go run -gcflags -m app.go

GOMAXPROCS、并发和并行性
  • 级别:高级

Go 1.4 及更低版本仅使用一个执行上下文/操作系统线程。这意味着在任何给定时间只能执行一个 goroutine。从 1.5 开始,Go 将执行上下文数设置为 返回的逻辑 CPU 内核数。该数字可能与系统上的逻辑 CPU 内核总数匹配,也可能不匹配,具体取决于进程的 CPU 关联性设置。您可以通过更改环境变量或调用函数来调整此数字。runtime.NumCPU()GOMAXPROCSruntime.GOMAXPROCS()

有一个常见的误解,它表示 Go 将用于运行 goroutine 的 CPU 数量。函数文档增加了更多的混乱。变量描述 (https://golang.org/pkg/runtime/) 在谈论操作系统线程方面做得更好。GOMAXPROCSruntime.GOMAXPROCS()GOMAXPROCS

您可以设置为多个 CPU 数量。从 1.10 开始,GOMAXPROCS 不再有限制。过去的最大值是 256,后来在 1.9 中增加到 1024。GOMAXPROCSGOMAXPROCS

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 可能会对某些操作进行重新排序,但它可以确保发生 go 例程中的整体行为不会改变。但是,它不能保证跨多个 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)
}

如果多次运行此代码,可能会看到以下和变量组合:ab

1
2

3
4

0
2

0
0

1
4

和 最有趣的组合是“02”。它表明之前已更新。abba

如果需要在多个 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!")
}

要确定您在循环中调用的函数是否是内联的,请将“-m”gc 标志传递给 “go build” 或 “go run”(例如,)。forgo 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!")
}

请注意,上面的代码包含争用条件。这是故意这样做的,以显示 sheduling 陷阱。

导入 C 和多行导入块
  • 级别:Cgo

您需要导入“C”包才能使用 Cgo。你可以用一行来做到这一点,也可以用一个块来做到这一点。importimport

package main

/*
#include <stdlib.h>
*/
import (
  "C"
)

import (
  "unsafe"
)

func main() {
  cs := C.CString("my go string")
  C.free(unsafe.Pointer(cs))
}

如果使用块格式,则无法在同一块中导入其他包。import

package main

/*
#include <stdlib.h>
*/
import (
  "C"
  "unsafe"
)

func main() {
  cs := C.CString("my go string")
  C.free(unsafe.Pointer(cs))
}

编译错误:

./main.go:13:2:无法确定 C.free 的名称类型

导入 C 和 Cgo 注释之间没有空行 注释
  • 级别:Cgo

Cgo 的第一个问题之一是语句上方 cgo 注释的位置。import "C"

package main

/*
#include <stdlib.h>
*/

import "C"

import (
  "unsafe"
)

func main() {
  cs := C.CString("my go string")
  C.free(unsafe.Pointer(cs))
}

编译错误:

./main.go:15:2:无法确定 C.free 的名称类型

确保语句上方没有任何空行。import "C"

无法调用带有变量参数的 C 函数
  • 级别:Cgo

不能直接使用变量参数调用 C 函数。

package main

/*
#include <stdio.h>
#include <stdlib.h>
*/
import "C"

import (
  "unsafe"
)

func main() {
  cstr := C.CString("go")
  C.printf("%s\n",cstr) //not ok
  C.free(unsafe.Pointer(cstr))
}

编译错误:

./main.go:15:2:意外类型:...

您必须将可变参数 C 函数包装在具有已知参数数的函数中。

package main

/*
#include <stdio.h>
#include <stdlib.h>

void out(char* in) {
  printf("%s\n", in);
}
*/
import "C"

import (
  "unsafe"
)

func main() {
  cstr := C.CString("go")
  C.out(cstr) //ok
  C.free(unsafe.Pointer(cstr))
}


评论与讨论

Reddit讨论。

黑客新闻讨论。

感谢您的反馈和建议!