最近准备写一些关于golang的技术博文,本文是之前在GitHub上看到的golang技术译文,感觉很有帮助,先给各位读者分享一下。

前言

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

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

初级篇:1-35

1. 左大括号 { 一般不能单独放一行

在其他大多数语言中,{ 的位置你自行决定。Go 比较特别,遵守分号注入规则(automatic semicolon injection):编译器会在每行代码尾部特定分隔符后加 ; 来分隔多条语句,比如会在 ) 后加分号:

// 错误示例
func main()
{
println("hello world")
} // 等效于
func main(); // 无函数体
{
println("hello world")
}
./main.go: missing function body
./main.go: syntax error: unexpected semicolon or newline before {
// 正确示例
func main() {
println("hello world")
}
注意代码块等特殊情况: // { 并不遵守分号注入规则,不会在其后边自动加分,此时可换行
func main() {
{
println("hello world")
}
}

  

考:Golang中自动加分号的特殊分隔符

2. 未使用的变量

如果在函数体代码中有未使用的变量,则无法通过编译,不过全局变量声明但不使用是可以的。

即使变量声明后为变量赋值,依旧无法通过编译,需在某处使用它

// 错误示例
var gvar int // 全局变量,声明不使用也可以 func main() {
var one int // error: one declared and not used
two := 2 // error: two declared and not used
var three int // error: three declared and not used
three = 3
} // 正确示例
// 可以直接注释或移除未使用的变量
func main() {
var one int
_ = one two := 2
println(two) var three int
one = three var four int
four = four
}

3. 未使用的 import

如果你 import 一个包,但包中的变量、函数、接口和结构体一个都没有用到的话,将编译失败。

可以使用 _ 下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package 的 init()

// 错误示例
import (
"fmt" // imported and not used: "fmt"
"log" // imported and not used: "log"
"time" // imported and not used: "time"
) func main() {
} // 正确示例
// 可以使用 goimports 工具来注释或移除未使用到的包
import (
_ "fmt"
"log"
"time"
) func main() {
_ = log.Println
_ = time.Now
}

  

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

// 错误示例
myvar := 1 // syntax error: non-declaration statement outside function body
func main() {
} // 正确示例
var myvar = 1
func main() {
}

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

不能用简短声明方式来单独为一个变量重复声明, := 左侧至少有一个新变量,才允许多变量的重复声明:

// 错误示例
func main() {
one := 0
one := 1 // error: no new variables on left side of :=
} // 正确示例
func main() {
one := 0
one, two := 1, 2 // two 是新变量,允许 one 的重复声明。比如 error 处理经常用同名变量 err
one, two = two, one // 交换两个变量值的简写
}

6. 不能使用简短声明来设置字段的值

struct 的变量字段不能使用 := 来赋值以使用预定义的变量来避免解决:

// 错误示例
type info struct {
result int
} func work() (int, error) {
return 3, nil
} func main() {
var data info
data.result, err := work() // error: non-name data.result on left side of :=
fmt.Printf("info: %+v\n", data)
} // 正确示例
func main() {
var data info
var err error // err 需要预声明 data.result, err = work()
if err != nil {
fmt.Println(err)
return
} fmt.Printf("info: %+v\n", data)
}

7. 不小心覆盖了变量

对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 := 是一个赋值操作符。

如果你在新的代码块中像下边这样误用了 :=,编译不会报错,但是变量不会按你的预期工作:

func main() {
x := 1
println(x) //
{
println(x) //
x := 2
println(x) // 2 // 新的 x 变量的作用域只在代码块内部
}
println(x) //
}

这是 Go 开发者常犯的错,而且不易被发现。

可使用 vet 工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用:

> go tool vet -shadow main.go
main.go:9: declaration of "x" shadows declaration at main.go:5
注意 vet 不会报告全部被覆盖的变量,可以使用 go-nyet 来做进一步的检测: > $GOPATH/bin/go-nyet main.go
main.go:10:3:Shadowing variable `x`

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

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

// 错误示例
func main() {
var x = nil // error: use of untyped nil
_ = x
} // 正确示例
func main() {
var x interface{} = nil
_ = x
}

9. 直接使用值为 nil 的 slice、map

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

// map 错误示例
func main() {
var m map[string]int
m["one"] = 1 // error: panic: assignment to entry in nil map
// m := make(map[string]int)// map 的正确声明,分配了实际的内存
} // slice 正确示例
func main() {
var s []int
s = append(s, 1)
}

10. map 容量

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

// 错误示例
func main() {
m := make(map[string]int, 99)
println(cap(m)) // error: invalid argument m1 (type map[string]int) for cap
}

11. string 类型的变量值不能为 nil

对那些喜欢用 nil 初始化字符串的人来说,这就是坑:

// 错误示例
func main() {
var s string = nil // cannot use nil as type string in assignment
if s == nil { // invalid operation: s == nil (mismatched types string and nil)
s = "default"
}
} // 正确示例
func main() {
var s string // 字符串类型的零值是空串 ""
if s == "" {
s = "default"
}
}

12. Array 类型的值作为函数参数

在 C/C++ 中,数组(名)是指针。将数组作为参数传进函数时,相当于传递了数组内存地址的引用,在函数内部会改变该数组的值。

在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:

// 数组使用值拷贝传参
func main() {
x := [3]int{1,2,3} func(arr [3]int) {
arr[0] = 7
fmt.Println(arr) // [7 2 3]
}(x)
fmt.Println(x) // [1 2 3] // 并不是你以为的 [7 2 3]
}

如果想修改参数数组:

  • 直接传递指向这个数组的指针类型:
  • // 传址会修改原数据
    func main() {
    x := [3]int{1,2,3} func(arr *[3]int) {
    (*arr)[0] = 7
    fmt.Println(arr) // &[7 2 3]
    }(&x)
    fmt.Println(x) // [7 2 3]
    }

    直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)

  • // 会修改 slice 的底层 array,从而修改 slice
    func main() {
    x := []int{1, 2, 3}
    func(arr []int) {
    arr[0] = 7
    fmt.Println(x) // [7 2 3]
    }(x)
    fmt.Println(x) // [7 2 3]
    }

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

    与其他编程语言中的 for-in 、foreach 遍历语句不同,Go 中的 range 在遍历时会生成 2 个值,第一个是元素索引,第二个是元素的值:

  • // 错误示例
    func main() {
    x := []string{"a", "b", "c"}
    for v := range x {
    fmt.Println(v) // 1 2 3
    }
    } // 正确示例
    func main() {
    x := []string{"a", "b", "c"}
    for _, v := range x { // 使用 _ 丢弃索引
    fmt.Println(v)
    }
    }

    14. slice 和 array 其实是一维数据

    看起来 Go 支持多维的 array 和 slice,可以创建数组的数组、切片的切片,但其实并不是。

    对依赖动态计算多维数组值的应用来说,就性能和复杂度而言,用 Go 实现的效果并不理想。

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

    1. 使用原始的一维数组:要做好索引检查、溢出检测、以及当数组满时再添加值时要重新做内存分配。

    2. 使用“独立”的切片分两步:

    • 创建外部 slice
    • 对每个内部 slice 进行内存分配

      注意内部的 slice 相互独立,使得任一内部 slice 增缩都不会影响到其他的 slice

    • // 使用各自独立的 6 个 slice 来创建 [2][3] 的动态多维数组
      func main() {
      x := 2
      y := 4 table := make([][]int, x)
      for i := range table {
      table[i] = make([]int, y)
      }
      }
      1. 使用“共享底层数组”的切片
      • 创建一个存放原始数据的容器 slice
      • 创建其他的 slice
      • 切割原始 slice 来初始化其他的 slice
      • func main() {
        h, w := 2, 4
        raw := make([]int, h*w) for i := range raw {
        raw[i] = i
        } // 初始化原始 slice
        fmt.Println(raw, &raw[4]) // [0 1 2 3 4 5 6 7] 0xc420012120 table := make([][]int, h)
        for i := range table { // 等间距切割原始 slice,创建动态多维数组 table
        // 0: raw[0*4: 0*4 + 4]
        // 1: raw[1*4: 1*4 + 4]
        table[i] = raw[i*w : i*w + w]
        } fmt.Println(table, &table[1][0]) // [[0 1 2 3] [4 5 6 7]] 0xc420012120
        }

        更多关于多维数组的参考

        go-how-is-two-dimensional-arrays-memory-representation

        what-is-a-concise-way-to-create-a-2d-slice-in-go

        15. 访问 map 中不存在的 key

        和其他编程语言类似,如果访问了 map 中不存在的 key 则希望能返回 nil,比如在 PHP 中:

      • > php -r '$v = ["x"=>1, "y"=>2]; @var_dump($v["z"]);'
        NULL

        Go 则会返回元素对应数据类型的零值,比如 nil'' 、false 和 0,取值操作总有值返回,故不能通过取出来的值来判断 key 是不是在 map 中。

        检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可:

      • // 错误的 key 检测方式
        func main() {
        x := map[string]string{"one": "2", "two": "", "three": "3"}
        if v := x["two"]; v == "" {
        fmt.Println("key two is no entry") // 键 two 存不存在都会返回的空字符串
        }
        } // 正确示例
        func main() {
        x := map[string]string{"one": "2", "two": "", "three": "3"}
        if _, ok := x["two"]; !ok {
        fmt.Println("key two is no entry")
        }
        }

        16. string 类型的值是常量,不可更改

        尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。

        string 类型的值是只读的二进制 byte slice,如果真要修改字符串中的字符,将 string 转为 []byte 修改后,再转为 string 即可:

      • // 修改字符串的错误示例
        func main() {
        x := "text"
        x[0] = "T" // error: cannot assign to x[0]
        fmt.Println(x)
        } // 修改示例
        func main() {
        x := "text"
        xBytes := []byte(x)
        xBytes[0] = 'T' // 注意此时的 T 是 rune 类型
        x = string(xBytes)
        fmt.Println(x) // Text
        }

        注意: 上边的示例并不是更新字符串的正确姿势,因为一个 UTF8 编码的字符可能会占多个字节,比如汉字就需要 3~4 个字节来存储,此时更新其中的一个字节是错误的。

        更新字串的正确姿势:将 string 转为 rune slice(此时 1 个 rune 可能占多个 byte),直接更新 rune 中的字符

      • func main() {
        x := "text"
        xRunes := []rune(x)
        xRunes[0] = '我'
        x = string(xRunes)
        fmt.Println(x) // 我ext
        }

        17. string 与 byte slice 之间的转换

        当进行 string 和 byte slice 相互转换时,参与转换的是拷贝的原始值。这种转换的过程,与其他编程语的强制类型转换操作不同,也和新 slice 与旧 slice 共享底层数组不同。

        Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:

        • 在 map[string] 中查找 key 时,使用了对应的 []byte,避免做 m[string(key)] 的内存分配
        • 使用 for range 迭代 string 转换为 []byte 的迭代:for i,v := range []byte(str) {...}

        雾:参考原文

Golang 需要避免踩的 50 个坑1的更多相关文章

  1. Golang 新手可能会踩的 50 个坑【转】

    译文:https://github.com/wuYin/blog/blob/master/50-shades-of-golang-traps-gotchas-mistakes.md 原文:50 Sha ...

  2. Golang 新手可能会踩的 50 个坑

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

  3. Golang 需要避免踩的 50 个坑(三)

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

  4. Golang 需要避免踩的 50 个坑(二)

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

  5. Redis上踩过的一些坑

    来自: http://blog.csdn.net//chenleixing/article/details/50530419 上上周和同事(龙哥)参加了360组织的互联网技术训练营第三期,美团网的DB ...

  6. Go 语言从新手到大神:每个人都会踩的五十个坑(转)

    Go语言是一个简单却蕴含深意的语言.但是,即便号称是最简单的C语言,都能总结出一本<C陷阱与缺陷>,更何况Go语言呢.Go语言中的许多坑其实并不是因为Go自身的问题.一些错误你再别的语言中 ...

  7. [转帖]美团在Redis上踩过的一些坑-4.redis内存使用优化

    美团在Redis上踩过的一些坑-4.redis内存使用优化 博客分类: 运维 redis redisstringhash优化segment-hash  转载请注明出处哈:http://carlosfu ...

  8. 三分之一的程序猿之社交类app踩过的那些坑

    三分之一的程序猿之社交类app踩过的那些坑 万众创新,全民创业.哪怕去年陌生人社交不管融资与否都倒闭了不知道多少家,但是依然有很多陌生人社交应用层出不穷的冒出来.各种脑洞大开,让人拍案叫起. 下面我们 ...

  9. 【Fine原创】JMeter分布式测试中踩过的那些坑

    最近因为项目需要,研究了性能测试的相关内容,并且最终选用了jmeter这一轻量级开源工具.因为一直使用jmeter的GUI模式进行脚本设计,到测试执行阶段工具本身对资源的过量消耗给性能测试带来了瓶颈, ...

随机推荐

  1. <console>:14: error: not found: value spark import spark.implicits.

    启动 ./spark-shell 出现问题 启动 hadoop, 并创建,解决 hadoop fs -mkdir /directory 解决了

  2. Spring Boot中以代码方式配置Tomcat

    在Spring Boot2.0以上配置嵌入式Servlet容器时EmbeddedServletContainerCustomizer类不存在,经网络查询发现被WebServerFactoryCusto ...

  3. 【oracle】lpad函数 作用(填充)

  4. wait函数和waitpid的使用和总结

    wait和waitpid出现的原因 SIGCHLD --当子进程退出的时候,内核会向父进程发送SIGCHLD信号,子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止) --子进程退出时,内 ...

  5. MySQL使用的几条注意事项和1449错误解决方案

    一.如何在Windows终端连接MySQL? 使用cmd进入终端,然后要进入到你安装MySQL的bin目录下(如果没有的话,会出现该命令无效之类的错误),然后使用命令mysql -u root -p, ...

  6. [LeetCode] 51. N-Queens N皇后问题

    The n-queens puzzle is the problem of placing n queens on an n×n chessboard such that no two queens ...

  7. 4,VS常见问题解决(一闪而过、等问题)不断更新

    当然入门学c可以用VS,但是深入就不应该用VS(MSVC)编译器了,毕竟VS2017还没有完全支持C99(这个从VS2017没有实现变长数组可见) 但是想看c源码,还是 1. 推荐用 *nix系统 2 ...

  8. Elasticsearch由浅入深(八)搜索引擎:mapping、精确匹配与全文搜索、分词器、mapping总结

    下面先简单描述一下mapping是什么? 自动或手动为index中的type建立的一种数据结构和相关配置,简称为mappingdynamic mapping,自动为我们建立index,创建type,以 ...

  9. MySQL binlog三种模式

    1.1 Row Level  行模式 日志中会记录每一行数据被修改的形式,然后在slave端再对相同的数据进行修改 优点:在row level模式下,bin-log中可以不记录执行的sql语句的上下文 ...

  10. 熟悉使用ssm框架完成项目

    羡慕那些一些博客就能写好多的人,总是能写的长篇大论的,而我就是简短的而且还伴随着语句不通顺等等,只写一点点,归根结底还是自己懒得写! 1.首先了解框架内容,拿到源码,先看配置文件 2.然后修改数据库建 ...