最近准备写一些关于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. Comet OJ 计算机(computer)

    Comet OJ 计算机(computer) 题目传送门 题目描述 小 X 有一台奇怪的计算机. 这台计算机首先会读入一个正整数 nn,然后生成一个包含 nn 个数的序列 aa. 一开始 a_i(1 ...

  2. LG2893/POJ3666 「USACO2008FEB」Making the Grade 线性DP+决策集优化

    问题描述 LG2893 POJ3666 题解 对于\(A\)中的每一个元素,都将存在于\(B\)中. 对\(A\)离散化. 设\(opt_{i,j}\)代表\([1,i]\),结尾为\(j\)的最小代 ...

  3. impala进阶

    一.impala存储 1.文件类型 2.压缩方式 二.impala分区 1.创建分区方式 partitioned by 创建表时,添加该字段指定分区列表: create table t_person( ...

  4. DVWA CSRF 通关教程

    CSRF 介绍 CSRF,全称Cross-site request forgery,即跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie.会话等),诱骗其点击恶意链接或者访问包含攻击代码 ...

  5. linux-根目录添加内存

    01,添加磁盘到服务器 02, 查看添加的内容 这个是新添加的 03, 创建新分区 [root@oracle01 ~]# fdisk /dev/sdb --格式化分析 Welcome ). Chang ...

  6. 2条最佳实践App疯狂增长逻辑

    2条最佳实践App疯狂增长逻辑 1.不断打造和强化产品的不可或缺属性 产品的核心价值是什么?对你的客户来言,为什么是你? 2.等待“阿哈时刻” 进入快速推广 用户使用产品眼前一亮的时刻,是用户真正发现 ...

  7. 实战django(二)--登录实现记住我

    上节初步实现了登录和注册模块,这节我们进一步实现“记住我”功能. 大体功能分为以下模块: 1.在登录时如果勾选记住我,那么就将用户username存进cookie中,跳转到index页面: 2.此时, ...

  8. python数据分析开发中的常用整理

    Pandas操作 python使用pandas读取csv import pandas as pd #数据筛选 usetTable = pd.read_csv(filename,header = 0) ...

  9. Oracle性能调优之虚拟索引用法简介

    本博客记录一下Oracle虚拟索引的用法,虚拟索引是定义在数据字典中的伪索引,可以说是伪列,没有修改的索引字段的.虚拟索引的目的模拟索引,不会增加存储空间的使用,有了虚拟索引,开发者使用执行计划的时候 ...

  10. rem与em的使用和区别

    区别是:浏览器根据谁来转化成px值. 当使用rem单位,转换为像素大小取决于根元素的字体大小,即HTML元素的字体大小. 有一个比较普遍的误解,认为em单位是相对于父元素的字体大小.事实上,根据W3C ...