进阶常犯的错误

进阶常犯的错误 #

引用: Go 经典译文:50 个 Go 新手易犯的错误(2020版)

关闭 HTTP 响应 Body #

  • 级别:中级

当使用 net/http 库发送 http 请求时,会返回一个 *http.Respose 变量。 如果你不读取响应 Body,依然需要关闭这个 Body。 注意对于空 Body 也必须关闭。 对于 GO 程序员新手很容易忘记这点。

一些 GO 程序员新手尝试关闭响应 Body,但他们在错误的位置进行了关闭 Body。

package main

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

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

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

    fmt.Println(string(body))
}

这种方法适合请求成功的情况,但是如果 http 请求失败,则 resp 变量可能为 nil,这将导致运行触发 panic

关闭 http 响应 Body 的最常见方法,应该是在 http 响应检查错误之后使用 defer 调用 Close 方法。

package main

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

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

    defer resp.Body.Close()//ok, most of the time :-)
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

在大多数情况下,当 http 请求失败时,resp 变量将为 nil,而 err 变量将为非空。 但是当重定向失败时,两个变量都将为非空。 这意味着 Body 仍然可能会未关闭而导致泄漏。

你可以通过在 http 响应错误处理时,添加一段关闭非空响应 Body 的代码这解决这个问题 (重定向时响应和 err 都是非空,检查了 err 返回错误而没有关闭 Body), 使用一个 defer 关闭所有失败和成功请求的响应 Body。

package main

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

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

    if err != nil {
        fmt.Println(err)
        return
    }

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

    fmt.Println(string(body))
}

resp.Body.Close() 方法的底层实现是读取并丢弃响应 Body 的剩余数据。 这样可以保证使用了 keepalive http 长连接机制,可以将 http 连接复用,用来发送另外一个请求。 在最新的 http 客户端处理方法是不同的。 但是现在你需要读取并丢弃其余的响应数据。 如果你不读取并丢弃剩余数据,那么 http 连接可能会关闭而不是被长连接复用。 这个小陷阱应该记录在 Go 1.5 中。

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

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

如果你没有读取全部响应 Body,则需要这样丢弃数据,如果使用以下代码处理 json API 响应,json 库只读取了部分 Body 就完成了 json 对象解析,未读取完毕 Body,则可能会发生这种情况:

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

关闭 HTTP 连接 #

  • 级别:中级

某些 HTTP 服务器会打开长连接(基于 HTTP/1.1 规范和服务器的 Keepalive 机制)。 在默认情况下,net/http 库客户端在收到 HTTP 服务端要求关闭时,才会关闭长连接。 这意味着程序在某些情况下没有关闭长连接,可能会泄露系统 fd,用完操作系统的套接字 / 文件描述符。

你可以在请求发送前将 *http.Requsst 对象的 Close 字段设置为 true, 用于关闭 net/http 库客户端连接。

另一种方法是添加 Connection Header 并设置值为 close。目标 HTTP 服务器响应也应该返回 Header Connection:close。当 net/http 库客户端看到这个 Header 时,它也会关闭连接。

package main

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

func main() {  
    req, err := http.NewRequest("GET","http://golang.org",nil)
    if err != nil {
        fmt.Println(err)
        return
    }

    req.Close = true
    // 或者使用下面的这行方法:
    //req.Header.Add("Connection", "close")

    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

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

    fmt.Println(len(string(body)))
}

你还可以在全局范围内禁用使用 HTTP 长连接 (KeepAlives),创建一个自定义使用的 *http.Transport 对象,用于发送 http 客户端的请求。

package main

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

func main() {  
    tr := &http.Transport{DisableKeepAlives: true}
    client := &http.Client{Transport: tr}

    resp, err := client.Get("http://golang.org")
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(resp.StatusCode)

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

    fmt.Println(len(string(body)))
}

如果你同时向一个 http 服务器发送大量请求,则可以打开 KeepAlives 选项使用长连接。但是如果你在应用是短时间内,向不同的 HTTP 服务器发送一两个请求 (少量请求),那么则最好在收到 http 响应后立刻关闭网络连,设置更大的操作系统打开文件句柄数量是一个好方法 (ulimit -n)。正确的解决方法取决于你的应用程序。

JSON 编码器添加换行符 #

  • 级别:中级

你发现你为 JSON 编码功能编写的测试由于未获得期望值而导致测试失败,为什么会这样?如果你是用的是 JSON 编码器对象,则在编码的 JSON 对象的末尾将获得一个额外的换行符。

package main

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

func main() {
  data := map[string]int{"key": 1}

  var b bytes.Buffer
  json.NewEncoder(&b).Encode(data)

  raw,_ := json.Marshal(data)

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

JSON 编码器对象旨在用于流传输。使用 JSON 进行流传输通常意味着用换行符分隔的 JSON 对象,这就是为什么 Encode 方法添加换行符的原因。这是正常的行为,但是通常被忽略或遗忘。

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

  • 级别:中级

这是已记录的行为,但是你必须仔细阅读所有 JSON 包文档以了解有关情况。SetEscapeHTML 方法描述讨论了 and 字符 (小于和大于) 的默认编码行为。

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

package main

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

func main() {
  data := "x < y"

  raw,_ := json.Marshal(data)
  fmt.Println(string(raw))
  //prints: "x \u003c y" <- probably not what you expected

  var b1 bytes.Buffer
  json.NewEncoder(&b1).Encode(data)
  fmt.Println(b1.String())
  //prints: "x \u003c y" <- probably not what you expected

  var b2 bytes.Buffer
  enc := json.NewEncoder(&b2)
  enc.SetEscapeHTML(false)
  enc.Encode(data)
  fmt.Println(b2.String())
  //prints: "x < y" <- looks better
}

给 Go 团队的建议… 选择加入。

比较结构体 / 数组 / 切片 / Map #

  • 级别:中级

如果结构体的每个字段都具有可比性 , 那么则可以使用等号运算符 == 比较结构体变量。

package main

import "fmt"

type data struct {  
    num int
    fp float32
    complex complex64
    str string
    char rune
    yes bool
    events <-chan string
    handler interface{}
    ref *byte
    raw [10]byte
}

func main() {  
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",v1 == v2) //prints: v1 == v2: true
}

如果结构体的任意一个属性不具有可比性,那么使用等号运算符在编译时就会显示报错。注意,数组的数据类型具有可比性时,数组才能比较。

package main

import "fmt"

type data struct {  
    num int                //ok
    checks [10]func() bool //无法比较
    doit func() bool       //无法比较
    m map[string] string   //无法比较
    bytes []byte           //无法比较
}

func main() {  
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",v1 == v2)
}

GO 提供了一些辅助函数用来比较无法比较的变量。

最常见的方法就是使用反射库的 DeepEqual() 函数。

package main

import (  
    "fmt"
    "reflect"
)

type data struct {  
    num int                //ok
    checks [10]func() bool //无法比较
    doit func() bool       //无法比较
    m map[string] string   //无法比较
    bytes []byte           //无法比较
}

func main() {  
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2)) //prints: v1 == v2: true

    m1 := map[string]string{"one": "a","two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true
}

除了运行缓慢 (可能对你的应用程序造成破坏或可能不会破坏交易) 之外,DeepEqual() 也有自己的陷阱。

package main

import (  
    "fmt"
    "reflect"
)

func main() {  
    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false
}

DeepEqual() 认为空切片不等于 “nil” 切片。此行为与你使用 bytes.Equal() 函数获得的行为不同。bytes.Equal() 认为 “nil” 和空片相等。

package main

import (  
    "fmt"
    "bytes"
)

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

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

package main

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

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

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

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

如果你的 []byte(或字符串) 包含文本数据,当你需要使用不区分大小写比较值时,你可能倾向于使用使用 “bytes” 和 “string” 库的 ToUpper()/ToLower() 函数 (在使用 ==,bytes.Equal()bytes.Compare() 比较之前)。
这种方法适合英文,但是却不适合许多其他语言的文本。正确的方法应该使用 strings.EqualFold()bytes.EqualFold() 方法进行比较。

如果你的 []byte 中包含了验证用户信息的机密信息(例如,加密哈希,令牌等), 请不要使用 reflect.DeepEqual()bytes.Equal()bytes.Compare() 函数。因为这些函数可能是你受到 定时攻击,为了比较泄露时间信息,请使用 ‘crypto/subtle’ 库 (例如:subtle.ConstantTimeCompare())。

从 Panic 中恢复 #

  • 级别:中等

recover() 函数可用于捕获 / 拦截 panic。 但是只有在 defer 函数中,调用 recover() 才能达到目的。

不正确:

package main

import "fmt"

func main() {  
    recover() // 什么也没执行
    panic("not good")
    recover() // 不会执行到 :)
    fmt.Println("ok")
}

生效:

package main

import "fmt"

func main() {  
    defer func() {
        fmt.Println("recovered:",recover())
    }()

    panic("not good")
}

仅在你的 defer 函数中直接调用 recover() 时才有效。

失败:

package main

import "fmt"

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

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

    panic("not good")
}

使用或更新切片 / 数组 / Map Rnage 遍历的数据 #

  • 级别:中等

在 “Range” 范围的产生是数据是集合的元素副本,这些值不是原始数据的引用,这意味修改 Range 的值不会改变原始数据。这也意味获得的值地址也不会提供执行原始数据的指针。

package main

import "fmt"

func main() {  
    data := []int{1,2,3}
    for _,v := range data {
        v *= 10 //原始项目不变
    }

    fmt.Println("data:",data) //prints data: [1 2 3]
}

如果需要修改原始数据,需要使用索引访问数据。

package main

import "fmt"

func main() {  
    data := []int{1,2,3}
    for i,_ := range data {
        data[i] *= 10
    }

    fmt.Println("data:",data) //prints data: [10 20 30]
}

如果你的集合包含指针类型,那么规则有些不同。如果希望原始数据指向另外一个值,则仍然需要使用索引操作,但是也可以使用 “for range” 语法中第二个值来更新存储在目标的数据。

package main

import "fmt"

func main() {  
    data := []*struct{num int} {{1},{2},{3}}

    for _,v := range data {
        v.num *= 10
    }

    fmt.Println(data[0],data[1],data[2]) //prints &{10} &{20} &{30}
}

切片的隐藏数据 #

  • 级别:中级

重新分割切片时,新切片将引用旧切片的底层数组。如果你忘记这个行为,并且分配相对较大切片,则从中创建了新建的切片引用了部分原始数据,则可能导致意外的底层数据使用。

package main

import "fmt"

func get() []byte {  
    raw := make([]byte,10000)
    fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <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/BBBBBBBBB” 这样,而是 “AAAAsuffix/uffixBBBB”。发送这种请求是因为两个路径切片的引用了相同的原始底层数据。这意味修改原始路径也会被修改。根据你的程序情况,这也可能会是一个问题。

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

package main

import (  
    "fmt"
    "bytes"
)

func main() {  
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')
    dir1 := path[:sepIndex:sepIndex] //完整切片表达式
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

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

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

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

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

旧的切片 #

  • 级别:中等

多个切片可以引用相同的数据。 例如当你使用现有切片创建新切片时,可能会发生这种情况。 如果程序依靠此行为来正常运行,那么将需要担心的旧的切片。

在某些时候,当原始数组无法容纳更多新数据时,将数据添加到切片将导致新的数组扩容。现在其他切片将指向旧数组(包含旧数据)。

import "fmt"

func main() {  
    s1 := []int{1,2,3}
    fmt.Println(len(s1),cap(s1),s1) //prints 3 3 [1 2 3]

    s2 := s1[1:]
    fmt.Println(len(s2),cap(s2),s2) //prints 2 2 [2 3]

    for i := range s2 { s2[i] += 20 }

    //仍然引用相同的数组
    fmt.Println(s1) //prints [1 22 23]
    fmt.Println(s2) //prints [22 23]

    s2 = append(s2,4)

    for i := range s2 { s2[i] += 10 }

    //s1 is now "stale"
    fmt.Println(s1) //prints [1 22 23]
    fmt.Println(s2) //prints [32 33 14]
}

类型声明和方法 #

  • 级别:中级

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

失败:

package main

import "sync"

type myMutex sync.Mutex

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

编译错误:

/tmp/sandbox106401185/main.go:9: mtx.Lock undefined (type myMutex has no field or method Lock) /tmp/sandbox106401185/main.go:10: mtx.Unlock undefined (type myMutex has no field or method Unlock)

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

作品:

package main

import "sync"

type myLocker struct {  
    sync.Mutex
}

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

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

作品:

package main

import "sync"

type myLocker sync.Locker

func main() {  
    var lock myLocker = new(sync.Mutex)
    lock.Lock() //ok
    lock.Unlock() //ok
}

突破 “for switch” 和 “ for select” 代码块 #

  • 级别:中级

没有标签的 “break” 语句只会使你脱离内部 switch /select 块。如果不能使用 “ return” 语句,则为外循环定义标签是第二件事。

package main

import "fmt"

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

    fmt.Println("out!")
}

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

句中的迭代变量和闭包 #

  • 级别:中级

这是 Go 中最常见的陷阱。for 语句中的迭代变量在每次迭代中都会重复使用。这意味着在 for 循环中创建的每个闭包 (aka 函数文字) 都将引用相同的变量 (它们将在这些 goroutine 开始执行时获得该变量的值)。

不正确:

package main

import (  
    "fmt"
    "time"
)

func main() {  
    data := []string{"one","two","three"}

    for _,v := range data {
        go func() {
            fmt.Println(v)
        }()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: three, three, three
}

最简单的解决方案 (不需要对 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 开发人员来说,这是一个容易犯的错误,将延迟的代码执行规则与变量作用域规则混为一谈。如果你具有一个长期运行的函数,且该函数具有 for 循环,该循环试图在每次迭代中延迟 defer 资源清理调用,则可能会成为问题。

package main

import (  
    "fmt"
    "os"
    "path/filepath"
)

func main() {  
    if len(os.Args) != 2 {
        os.Exit(-1)
    }

    start, err := os.Stat(os.Args[1])
    if err != nil || !start.IsDir(){
        os.Exit(-1)
    }

    var targets []string
    filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !fi.Mode().IsRegular() {
            return nil
        }

        targets = append(targets,fpath)
        return nil
    })

    for _,target := range targets {
        f, err := os.Open(target)
        if err != nil {
            fmt.Println("bad target:",target,"error:",err) //prints error: too many open files
            break
        }
        defer f.Close() //will not be closed at the end of this code block
        //do something with the file...
    }
}

解决该问题的一种方法是将代码块包装在一个函数中。

package main

import (  
    "fmt"
    "os"
    "path/filepath"
)

func main() {  
    if len(os.Args) != 2 {
        os.Exit(-1)
    }

    start, err := os.Stat(os.Args[1])
    if err != nil || !start.IsDir(){
        os.Exit(-1)
    }

    var targets []string
    filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !fi.Mode().IsRegular() {
            return nil
        }

        targets = append(targets,fpath)
        return nil
    })

    for _,target := range targets {
        func() {
            f, err := os.Open(target)
            if err != nil {
                fmt.Println("bad target:",target,"error:",err)
                return
            }
            defer f.Close() //ok
            //do something with the file...
        }()
    }
}

另一种方法是删除 defer 语句

失败类型断言 #

  • 级别:中级

失败的类型断言将为断言语句中使用的目标类型返回「零值」。当它与影子变量混合在一起时,可能导致意外行为。

错误的范例:

package main

import "fmt"

func main() {  
    var data interface{} = "great"

    if data, ok := data.(int); ok {
        fmt.Println("[is an int] value =>",data)
    } else {
        fmt.Println("[not an int] value =>",data) 
        //prints: [not an int] value => 0 (not "great")
    }
}

正确的范例:

package main

import "fmt"

func main() {  
    var data interface{} = "great"

    if res, ok := data.(int); ok {
        fmt.Println("[is an int] value =>",res)
    } else {
        fmt.Println("[not an int] value =>",data) 
        //prints: [not an int] value => great (as expected)
    }
}

阻塞的 Goroutines 和资源泄漏 #

  • 级别:中级

Rob Pike 在 Google I/O 大会上的演讲 「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
}

该函数为每个搜索副本启动 goroutines。每个 goroutine 将其搜索结果发送到结果通道。返回结果通道的第一个值。

其他 goroutines 的结果如何?那 goroutines 本身呢?

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

为了避免泄漏,你需要确保所有 goroutine 都退出。一种潜在的解决方案是使用足够大的缓冲结果通道来保存所有结果。

func First(query string, replicas ...Search) Result {  
    c := make(chan Result,len(replicas))
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

另一种可能的解决方案是使用 select 语句和 default 大小写以及可保存一个值的缓冲结果通道。default 情况确保即使结果通道无法接收消息,goroutine 也不会卡住。

func First(query string, replicas ...Search) Result {  
    c := make(chan Result,1)
    searchReplica := func(i int) { 
        select {
        case c <- replicas[i](query):
        default:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

你还可以使用特殊的取消通道来中断工作。

func First(query string, replicas ...Search) Result {  
    c := make(chan Result)
    done := make(chan struct{})
    defer close(done)
    searchReplica := func(i int) { 
        select {
        case c <- replicas[i](query):
        case <- done:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }

    return <-c
}

为什么演示文稿中包含这些错误? Rob Pike 只是不想使幻灯片复杂化。这是有道理的,但是对于新的 Go 开发人员来说可能是个问题,他们会按原样使用该代码,而不认为它可能会出现问题。

相同地址的不同零大小变量 #

  • 级别:中级

如果你有两个不同的变量,它们不应该有不同的地址吗?好吧,Go 并不是这样:-) 如果变量大小为零,它们可能会在内存中共享完全相同的地址。

package main

import (
  "fmt"
)

type data struct {
}

func main() {
  a := &data{}
  b := &data{}

  if a == b {
    fmt.Printf("same address - a=%p b=%p\n",a,b)
    //prints: same address - a=0x1953e4 b=0x1953e4
  }
}

iota 的第一次使用并不总是从零开始 #

  • 级别:中级

它可能看起来像是一个 iota 标识符就像一个增量运算符。开始一个新的常量声明,第一次使用 iota 时得到 0,第二次使用时得到 1,依此类推。但情况并非总是如此。

package main

import (
  "fmt"
)

const (
  azero = iota
  aone  = iota
)

const (
  info  = "processing"
  bzero = iota
  bone  = iota
)

func main() {
  fmt.Println(azero,aone) //prints: 0 1
  fmt.Println(bzero,bone) //prints: 1 2
}

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

在值实例上使用指针接收器方法 #

  • 级别:高级

只要该值是可寻址的,就可以在该值上调用指针接收器方法。换句话说,在某些情况下,你不需要该方法的值接收器版本。

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

package main

import "fmt"

type data struct {  
    name string
}

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

type printer interface {  
    print()
}

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

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

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

编译错误:

/tmp/sandbox017696142/main.go:21: cannot use data literal (type data) as type printer in assignment: data does not implement printer (print method has pointer receiver)
/tmp/sandbox017696142/main.go:25: cannot call pointer method on m[“x”] /tmp/sandbox017696142/main.go:25: cannot take the address of m[“x”]

更新 map 值字段 #

  • 级别:高级

如果你具有结构值 map,则无法更新单个结构字段。

失败的范例:

package main

type data struct {  
    name string
}

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

编译错误:

/tmp/sandbox380452744/main.go:9: cannot assign to m[“x”].name

它不会工作,因为 map 元素不可寻址。

对于 Go 新手开发者,可能会感到困惑,slice 元素是可寻址的。

package main

import "fmt"

type data struct {  
    name string
}

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

请注意,前一阵子可以在其中一个 Go 编译器 (gccgo) 中更新 map 元素字段,但是该行为很快得到解决:-) 它也被认为是 Go 1.3 的潜在功能。当时还不足以提供支持,因此它仍在待办事项清单上。

首先解决的是使用临时变量。

package main

import "fmt"

type data struct {  
    name string
}

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

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

package main

import "fmt"

type data struct {  
    name string
}

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

顺便说一句,运行此代码会发生什么?

package main

type data struct {  
    name string
}

func main() {  
    m := map[string]*data {"x":{"one"}}
    m["z"].name = "what?" //???
}

「nil」接口和「nil」接口值 #

  • 级别:高级

这是 Go 语言中第二常见的陷阱,因为即使接口看起来像指针,它们也不是指针。接口变量仅在其类型和值字段为「nil」时才为「nil」。

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

package main

import "fmt"

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

    fmt.Println(data,data == nil) //prints: <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++ 中,使用 new 运算符创建变量始终意味着你具有堆变量。在 Go 语言中,即使使用 new()make() 函数,编译器仍会决定将变量分配到何处。编译器根据变量的大小和「转义分析」的结果来选择存储变量的位置。这也意味着可以返回对局部变量的引用,而在其他语言 (如 C 或 C++) 中则不可以。

如果你需要知道变量的分配位置,请将「-m」gc 标志传递给「go build」或「go run」(例如,go run -gcflags -m app.go)。

GOMAXPROCS,并发和并行 #

  • 级别:高级

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

常见的误解是 GOMAXPROCS 代表 Go 将用于运行 goroutine 的 CPU 数量。runtime.GOMAXPROCS() 函数文档使这个问题更加混乱。GOMAXPROCS 变量描述 ( golang.org/pkg/runtime/) 在讨论 OS 线程方面做得更好。

你可以将 GOMAXPROCS 设置为大于 CPU 的数量。从 1.10 版开始,GOMAXPROCS 不再受限制。GOMAXPROCS 的最大值以前是 256,后来在 1.9 中增加到 1024。

package main

import (  
    "fmt"
    "runtime"
)

func main() {  
    fmt.Println(runtime.GOMAXPROCS(-1)) //prints: X (1 on play.golang.org)
    fmt.Println(runtime.NumCPU())       //prints: X (1 on play.golang.org)
    runtime.GOMAXPROCS(20)
    fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 20
    runtime.GOMAXPROCS(300)
    fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 256
}

读写操作重新排序 #

  • 级别:高级

Go 可以对某些操作进行重新排序,但可以确保 goroutine 中发生该行为的整体行为不会改变。但是,它不能保证跨多个 goroutine 的执行顺序。

package main

import (  
    "runtime"
    "time"
)

var _ = runtime.GOMAXPROCS(3)

var a, b int

func u1() {  
    a = 1
    b = 2
}

func u2() {  
    a = 3
    b = 4
}

func p() {  
    println(a)
    println(b)
}

func main() {  
    go u1()
    go u2()
    go p()
    time.Sleep(1 * time.Second)
}

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

1
2

3
4

0
2

0
0

1
4

ab 最有趣的组合是「02」。它显示 b 已在 a 之前更新。

如果你需要跨多个 goroutine 保留读取和写入操作的顺序,则需要使用通道或「sync」包中的适当的方法。

抢占式调度 #

  • 级别:高级

可能有一个流氓 goroutine 阻止了其他 goroutine 的运行。如果你的 for 循环不允许调度程序运行,则可能发生这种情况。

package main

import "fmt"

func main() {  
    done := false

    go func(){
        done = true
    }()

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

for 循环不必为空。只要它包含不触发调度程序执行的代码,这将是一个问题。

调度程序将在 GC,“go” 语句,阻塞通道操作,阻塞系统调用和锁定操作之后运行。当调用非内联函数时,它也可能运行。

package main

import "fmt"

func main() {  
    done := false

    go func(){
        done = true
    }()

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

要查明你在 for 循环中调用的函数是否内联,请将 “-m” gc 标志传递给 “ go build” 或 “ go run”(例如,go build -gcflags -m)。

另一种选择是显式调用调度程序。你可以使用 “运行时” 包中的 Gosched() 函数来完成此操作。

package main

import (  
    "fmt"
    "runtime"
)

func main() {  
    done := false

    go func(){
        done = true
    }()

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

请注意,上面的代码包含一个竞争条件。这样做是故意显示出隐藏的陷阱。

导入 C 和多行导入块 #

  • 级别:Cgo

你需要导入 “C” 包才能使用 Cgo。你可以单行 import 进行此操作,也可以使用 import 块进行此操作。

package main

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

确保在 import C 声明前没有任何空白行。

不能调用带有可变参数的 C 函数 #

  • level: 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: 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))
}


本图书由小熊©2021 版权所有,所有文章采用知识署名-非商业性使用-禁止演绎 4.0 国际进行许可。