Go语言中的常见陷阱

原文:Common Gotchas in Go
作者:Mike JS. Choi
翻译:雁惊寒

摘要:本文介绍了Go初学者很可能会遇到的三个常见陷阱。以下是译文。

我最近开发了我的第一个真正的Go程序。它叫“Fix All Conflicts(译者注:修复所有的冲突)”,或简称为fac。这是一个简单易用的控制台程序,用于解决git合并冲突。我之所以开发这么个工具,是因为我一直都没有找到一个好用的合并工具。

开发的过程非常有意思,我在这个过程中学到了很多东西。所以,我决定记录下初学者很可能会遇到的一些常见“陷阱”!

有时候,地鼠很可能相当有侵略性。

1. Range

range函数是Go中最常用的函数之一。下面是range函数的使用示例。请注意,基于一些疯狂的原因,我们决定让动物园里所有的动物都拥有999条腿。

type Animal struct {
    name string
    legs int
}

func main() {
  zoo := []Animal{ Animal{ "Dog", 4 },
                   Animal{ "Chicken", 2 },
                   Animal{ "Snail", 0 },
                 }

  fmt.Printf("-> Before update %v\n", zoo)

  for _, animal := range zoo {
    // Oppps! `animal` is a copy of an element
    animal.legs = 999
  }

  fmt.Printf("\n-> After update %v\n", zoo)
}

上面的代码看起来没什么问题。但是,你可能会惊讶地发现两个fmt.Printf()语句打印出来的结果是相同的。

-> Before update [{Dog 4} {Chicken 2} {Snail 0}]
-> After update [{Dog 4} {Chicken 2} {Snail 0}]

教训

range的value属性(这里是animal)是zoo的值的一个副本,而不是指向zoo中的值的指针

修复

要修改数组中元素的值,我们必须通过它的指针来修改。

for idx, _ := range zoo {
  zoo[idx].legs = 999
}

这个看起来可能很平常,但你可能会惊讶地发现这是最常见的错误之一。

2. The … thingy

你可能会在C语言中使用关键字来创建变长参数函数, 变长参数函数接受数量或类型可变的参数。

在C语言中,你必须调用va_arg宏来访问可选参数。如果用其他方式来使用可变参数,编译器就会报错。

int add_em_up (int count,...) {
  ...
  va_start (ap, count);         /* Initialize the argument list */
  for (i = 0; i < count; i++)
      sum += va_arg(ap, int);   /* Get the next argument value */
  va_end (ap);                  /* Clean up */
  return sum
}

然而,在Go中,情况有点相似,但又有很大的不同。下面是Go中的一个可变参数函数myFprint。请注意它是如何使用可变参数a的。

func myFprint(format string, a ...interface{}) {
    if len(a) == 0 {
        fmt.Printf(format)
    } else {
        // ⚠️ `a` should be `a...`
        fmt.Printf(format, a)
        // ✅
        fmt.Printf(format, a...)
    }
}

func main() {
    myFprint("%s : line %d\n", "file.txt", 49)
}
[file.txt %!s(int=49)] : line %!d(MISSING)
file.txt : line 49

你可能会认为编译器会因为我们错误地使用了可变参数a而报错。但是,请注意,fmt.Sprintf只是用了a中的第一个参数。

教训

在Go中,可变参数函数会被编译器转换成slices

这意味着可变参数a实际上只是一个slice。正因为如此,下面的代码是完全正确的。

// `a` is just a slice!
for _, elem := range a {
    fmt.Println(elem)
}

修复

记住,在使用可变参数的地方,请输入三个点(…)!

3. Slicing 切片

如果你了解Python中的slicing的话,你应该会知道Python中的slicing其实是给了你一个新的列表,该列表中的元素是对复制过去的元素的引用。因此,Python的代码是这样的。

a = [1, 2, 3]
b = a[:2]           # �� 完全是一个新的list
b[0] = 999
>>> a
[1, 2, 3]
>>> b
[999, 2]

但是,如果你在Go中编写同样的代码的话,就会遇到其他问题。

func main() {
  data := []int{1,2,3}
  slice := data[:2]
  slice[0] = 999

  fmt.Println(data)
  fmt.Println(slice)
}

教训

在Go中,切片与原始片共享相同的数组空间及其容量。因此,如果更改切片中的元素,也会改变原始数组中的内容。

修复

如果你想得到一个单独的切片,有两个选择。

// Option #1
// appending elements to a nil slice
// `...` changes slice to arguments for the variadic function `append`
a := append([]int{}, data[:2]...)

// Option #1
// Create slice with length of 2
// copy(dest, src)
a := make([]int, 2)
copy(a, data[:2]

根据StackOverflow中的一篇文章所述append的速度比make. + copy更快一些。

1月13日,SDCC 2017之数据库线上峰会即将强势来袭,秉承干货实料(案例)的内容原则,邀请了来自阿里巴巴、腾讯、微博、网易等多家企业的数据库专家及高校研究学者,围绕Oracle、MySQL、PostgreSQL、Redis等热点数据库技术展开,从核心技术的深挖到高可用实践的剖析,打造精华压缩式分享,举一反三,思辨互搏,报名及更多详情可点击此处查看
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值