Golang可能会踩的58个坑之中级篇1

简介: Golang可能会踩的58个坑之中级篇

前言

Go 是一门简单有趣的编程语言,与其他语言一样,在使用时不免会遇到很多坑,不过它们大多不是 Go 本身的设计缺陷。如果你刚从其他语言转到 Go,那这篇文章里的坑多半会踩到。


如果花时间学习官方 doc、wiki、讨论邮件列表、 Rob Pike 的大量文章以及 Go 的源码,会发现这篇文章中的坑是很常见的,跳过这些坑,能减少大量调试代码的时间。


中级篇:36-51

36.关闭 HTTP 的响应体

使用 HTTP 标准库发起请求、获取响应时,即使你不从响应中读取任何数据或响应为空,都需要手动关闭响应体。初学者很容易忘记手动关闭,或者写在了错误的位置:


// 请求失败造成 panic
func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    defer resp.Body.Close()    // resp 可能为 nil,不能读取 Body
    if err != nil {
        fmt.Println(err)
        return
    }
    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)
    fmt.Println(string(body))
}
func checkError(err error) {
    if err != nil{
        log.Fatalln(err)
    }
}

上边的代码能正确发起请求,但是一旦请求失败,变量 resp 值为 nil,造成 panic

panic: runtime error: invalid memory address or nil pointer dereference

应该先检查 HTTP 响应错误为 nil,再调用 resp.Body.Close() 来关闭响应体:

// 大多数情况正确的示例
func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    checkError(err)
    defer resp.Body.Close()    // 绝大多数情况下的正确关闭方式
    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)
    fmt.Println(string(body))
}

输出:

Get https://api.ipify.org?format=...: x509: certificate signed by unknown authority

绝大多数请求失败的情况下,resp 的值为 nil 且 err 为 non-nil。但如果你得到的是重定向错误,那它俩的值都是 non-nil,最后依旧可能发生内存泄露。2 个解决办法:


可以直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体。

手动调用 defer 来关闭响应体:

// 正确示例
func main() {
    resp, err := http.Get("http://www.baidu.com")
    // 关闭 resp.Body 的正确姿势
    if resp != nil {
        defer resp.Body.Close()
    }
    checkError(err)
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)
    fmt.Println(string(body))
}

resp.Body.Close() 早先版本的实现是读取响应体的数据之后丢弃,保证了 keep-alive 的 HTTP 连接能重用处理不止一个请求。但 Go 的最新版本将读取并丢弃数据的任务交给了用户,如果你不处理,HTTP 连接可能会直接关闭而非重用,参考在 Go 1.5 版本文档。


如果程序大量重用 HTTP 长连接,你可能要在处理响应的逻辑代码中加入:

_, err = io.Copy(ioutil.Discard, resp.Body) // 手动丢弃读取完毕的数据

如果你需要完整读取响应,上边的代码是需要写的。比如在解码 API 的 JSON 响应数据:

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

37.关闭 HTTP 连接

一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 “net/http” 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。解决办法有 2 个,请求结束后:


直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。

设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接。

// 主动关闭连接
func main() {
    req, err := http.NewRequest("GET", "http://golang.org", nil)
    checkError(err)
    req.Close = true
    //req.Header.Add("Connection", "close")    // 等效的关闭方式
    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }
    checkError(err)
    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)
    fmt.Println(string(body))
}

你可以创建一个自定义配置的 HTTP transport 客户端,用来取消 HTTP 全局的复用连接:

func main() {
    tr := http.Transport{DisableKeepAlives: true}
    client := http.Client{Transport: &tr}
    resp, err := client.Get("https://golang.google.cn/")
    if resp != nil {
        defer resp.Body.Close()
    }
    checkError(err)
    fmt.Println(resp.StatusCode)    // 200
    body, err := ioutil.ReadAll(resp.Body)
    checkError(err)
    fmt.Println(len(string(body)))
}

根据需求选择使用场景:

  • 若你的程序要向同一服务器发大量请求,使用默认的保持长连接。
  • 若你的程序要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接。或增加最大文件打开数 fs.file-max 的值。


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

在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理,比如下边的代码会造成 panic:


func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}
    if err := json.Unmarshal(data, &result); err != nil {
        log.Fatalln(err)
    }
    fmt.Printf("%T\n", result["status"])    // float64
    var status = result["status"].(int)    // 类型断言错误
    fmt.Println("Status value: ", status)
}
panic: interface conversion: interface {} is float64, not int

如果你尝试 decode 的 JSON 字段是整型,你可以:

  • 将 int 值转为 float 统一使用
  • 将 decode 后需要的 float 值转为 int 使用
    // 将 decode 的值转为 int 使用


func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}
    if err := json.Unmarshal(data, &result); err != nil {
        log.Fatalln(err)
    }
    var status = uint64(result["status"].(float64))
    fmt.Println("Status value: ", status)
}
  • 使用 Decoder 类型来 decode JSON 数据,明确表示字段的值类型
// 指定字段类型
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 {
        log.Fatalln(err)
    }
    var status, _ = result["status"].(json.Number).Int64()
    fmt.Println("Status value: ", status)
}
 // 你可以使用 string 来存储数值数据,在 decode 时再决定按 int 还是 float 使用
 // 将数据转为 decode 为 string
 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 {
          log.Fatalln(err)
      }
    var status uint64
      err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status);
    checkError(err)
       fmt.Println("Status value: ", status)
}

使用 struct 类型将你需要的数据映射为数值型

// struct 中指定字段类型
func main() {
      var data = []byte(`{"status": 200}`)
      var result struct {
          Status uint64 `json:"status"`
      }
      err := json.NewDecoder(bytes.NewReader(data)).Decode(&result)
      checkError(err)
    fmt.Printf("Result: %+v", result)
}
  • 可以使用 struct 将数值类型映射为 json.RawMessage 原生数据类型 适用于如果 JSON 数据不着急 decode 或 JSON 某个字段的值类型不固定等情况:


// 状态名称可能是 int 也可能是 string,指定为 json.RawMessage 类型
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"`
        }
        err := json.NewDecoder(bytes.NewReader(record)).Decode(&result)
        checkError(err)
        var name string
        err = json.Unmarshal(result.Status, &name)
        if err == nil {
            result.StatusName = name
        }
        var code uint64
        err = json.Unmarshal(result.Status, &code)
        if err == nil {
            result.StatusCode = code
        }
        fmt.Printf("[%v] result => %+v\n", idx, result)
    }

39.struct、array、slice 和 map 的值比较

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

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

如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。

type data struct {
    num    int
    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)
}

invalid operation: v1 == v2 (struct containing [10]func() bool cannot

be compared)


Go 提供了一些库函数来比较那些无法使用 == 比较的变量,比如使用 “reflect” 包的 DeepEqual() :

// 比较相等运算符无法比较的元素
func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))    // true
    m1 := map[string]string{"one": "a", "two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2))    // true
    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
       // 注意两个 slice 相等,值和顺序必须一致
    fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2))    // true
}

这种比较方式可能比较慢,根据你的程序需求来使用。DeepEqual() 还有其他用法:


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

注意:

  • DeepEqual() 并不总适合于比较 slice
func main() {
    var str = "one"
    var in interface{} = "one"
    fmt.Println("str == in: ", reflect.DeepEqual(str, in))    // true
    v1 := []string{"one", "two"}
    v2 := []string{"two", "one"}
    fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))    // false
    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))    // false
}

如果要大小写不敏感来比较 byte 或 string 中的英文文本,可以使用 “bytes” 或 “strings” 包的 ToUpper() 和 ToLower() 函数。比较其他语言的 byte 或 string,应使用 bytes.EqualFold() 和 strings.EqualFold()


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


reflect.DeepEqual() 认为空 slice 与 nil slice 并不相等,但注意 byte.Equal() 会认为二者相等:

func main() {
    var b1 []byte = nil
    b2 := []byte{}
    // b1 与 b2 长度相等、有相同的字节序
    // nil 与 slice 在字节上是相同的
    fmt.Println("b1 == b2: ", bytes.Equal(b1, b2))    // true
}

40.从 panic 中恢复

在一个 defer 延迟执行的函数中调用 recover() ,它便能捕捉 / 中断 panic


// 错误的 recover 调用示例
func main() {
    recover()    // 什么都不会捕捉
    panic("not good")    // 发生 panic,主程序退出
    recover()    // 不会被执行
    println("ok")
}
// 正确的 recover 调用示例
func main() {
    defer func() {
        fmt.Println("recovered: ", recover())
    }()
    panic("not good")
}

从上边可以看出,recover() 仅在 defer 执行的函数中调用才会生效。

// 错误的调用示例
func main() {
    defer func() {
        doRecover()
    }()
    panic("not good")
}
func doRecover() {
    fmt.Println("recobered: ", recover())
}

recobered: panic: not good

相关文章
|
20天前
|
Kubernetes Go 云计算
Golang 入门技术文档
**Golang 技术文档摘要:** Golang,由Google开发,是一种静态强类型、编译型语言,广泛应用于云计算、网络编程和分布式系统。本文档介绍了Golang的基础和特性,包括安装配置、 HelloWorld 示例、基本语法,如变量推导、函数多返回值和并发编程(goroutine、channel)。Golang的并发模型基于轻量级goroutine和channel,支持高效并发处理。此外,文档还提及了接口和多态性,展示了如何使用接口实现类型间的交互。Golang在Docker、Kubernetes等项目中得到应用,适用于后端服务开发。【6月更文挑战第9天】
21 1
|
2月前
|
Go 索引
Golang随笔之《Go专家编程》查漏补缺
Golang随笔之《Go专家编程》查漏补缺
44 5
|
12月前
|
消息中间件 缓存 JSON
Golang面试前一夜准备:1-5题
Golang面试前一夜准备:1-5题
|
8月前
|
安全 编译器 Go
详细 golang基础知识学习记录
详细 golang基础知识学习记录
|
9月前
|
缓存 安全 Go
No.8 Golang开发新手常犯的50个错误(上)
No.8 Golang开发新手常犯的50个错误
|
9月前
|
JSON Go API
No.8 Golang开发新手常犯的50个错误(下)
No.8 Golang开发新手常犯的50个错误
|
12月前
|
缓存 安全 Java
Golang面试前一夜准备:14-15题
Golang面试前一夜准备:14-15题
|
12月前
|
监控 算法 安全
Golang面试前一夜准备:6-10题
Golang面试前一夜准备:6-10题
|
12月前
|
安全 算法 Java
Golang面试前一夜准备
Golang面试前一夜准备
|
12月前
|
存储 缓存 分布式计算
Golang面试前一夜准备:11-13题
Golang面试前一夜准备:11-13题