评:为什么我不喜欢Go语言式的接口

以io.Writer为例看go中的interface{} — Go语言中文网博客
《Go语言内部实现与标准库》QQ群
想和大家一起交流Go语言、内部实现和标准库的博友们,可以申请加入“Go语言内部实现与标准库” QQ交流群,群号:。
该群杜绝灌水!
- 118,492 浏览数
- 73,795 浏览数
- 45,250 浏览数
- 40,095 浏览数
- 39,005 浏览数
- 34,524 浏览数
- 26,760 浏览数
- 25,270 浏览数
- 24,583 浏览数
- 23,274 浏览数
京ICP备号-1下次自动登录
现在的位置:
& 综合 & 正文
Go语言 nil和interface详解
golang的nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。nil是预先说明的标识符,也即通常意义上的关键字。在golang中,nil只能赋值给指针、channel、func、interface、map或slice类型的变量。如果未遵循这个规则,则会引发panic。对此官方有明确的说明:
golang中的interface类似于java的interface、PHP的interface或C++的纯虚基类。接口就是一个协议,规定了一组成员。这个没什么好说的,本文不打算对宏观上的接口概念和基于接口的范式编程做剖析。golang语言的接口有其独到之处:只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方。这种做法的学名叫做,有人也把它看作是一种静态的Duck Typing。所谓类型T的公开方法完全满足接口I的要求,也即是类型T实现了接口I所规定的一组成员。
在底层,interface作为两个成员来实现,一个类型和一个值。对此官方也有文档说明:,如果您不习惯看英文,这里有一篇柴大的翻译: 。
接下来通过编写测试和gdb来看看interface倒底是什么。会用到反射,如果您不太了解golang的反射是什么,这里有刑星翻译自官方博客的一篇:,原文在:。
$GOPATH/src
----interface_test
--------main.go
main.go的代码如下:
package main
func main() {
var val interface{} = int64(58)
fmt.Println(reflect.TypeOf(val))
fmt.Println(reflect.TypeOf(val))
我们已经知道接口类型的变量底层是作为两个成员来实现,一个是type,一个是data。type用于存储变量的动态类型,data用于存储变量的具体数据。在上面的例子中,第一条打印语句输出的是:int64。这是因为已经显示的将类型为int64的数据58赋值给了interface类型的变量val,所以val的底层结构应该是:(int64, 58)。我们暂且用这种二元组的方式来描述,二元组的第一个成员为type,第二个成员为data。第二条打印语句输出的是:int。这是因为字面量的整数在golang中默认的类型是int,所以这个时候val的底层结构就变成了:(int,
50)。借助于gdb很容易观察到这点:
$ cd $GOPATH/src/interface_test
$ go build -gcflags "-N -l"
$ gdb interface_test
接下来说说interface类型的值和nil的比较问题。这是个比较经典的问题,也算是golang的一个坑。
接着来看代码:
package main
func main() {
var val interface{} = nil
if val == nil {
fmt.Println("val is nil")
fmt.Println("val is not nil")
变量val是interface类型,它的底层结构必然是(type, data)。由于nil是untyped(无类型),而又将nil赋值给了变量val,所以val实际上存储的是(nil, nil)。因此很容易就知道val和nil的相等比较是为true的。
$ cd $GOPATH/src/interface_test
$ go build
$ ./interface_test
val is nil
对于将任何其它有意义的值类型赋值给val,都导致val持有一个有效的类型和数据。也就是说变量val的底层结构肯定不为(nil, nil),因此它和nil的相等比较总是为false。
上面的讨论都是在围绕值类型来进行的。在继续讨论之前,让我们来看一种特例:(*interface{})(nil)。将nil转成interface类型的指针,其实得到的结果仅仅是空接口类型指针并且它指向无效的地址。注意是空接口类型指针而不是空指针,这两者的区别蛮大的,学过C的童鞋都知道空指针是什么概念。
关于(*interface{})(nil)还有一些要注意的地方。这里仅仅是拿(*interface{})(nil)来举例,对于(*int)(nil)、(*byte)(nil)等等来说是一样的。上面的代码定义了接口指针类型变量val,它指向无效的地址(0x0),因此val持有无效的数据。但它是有类型的(*interface{})。所以val的底层结构应该是:(*interface{},
nil)。有时候您会看到(*interface{})(nil)的应用,比如var ptrIface = (*interface{})(nil),如果您接下来将ptrIface指向其它类型的指针,将通不过编译。或者您这样赋值:*ptrIface = 123,那样的话编译是通过了,但在运行时还是会panic的,这是因为ptrIface指向的是无效的内存地址。其实声明类似ptrIface这样的变量,是因为使用者只是关心指针的类型,而忽略它存储的值是什么。还是以例子来说明:
package main
func main() {
var val interface{} = (*interface{})(nil)
if val == nil {
fmt.Println("val is nil")
fmt.Println("val is not nil")
很显然,无论该指针的值是什么:(*interface{}, nil),这样的接口值总是非nil的,即使在该指针的内部为nil。
$ cd $GOPATH/src/interface_test
$ go build
$ ./interface_test
val is not nil
interface类型的变量和nil的相等比较出现最多的地方应该是error接口类型的值与nil的比较。有时候您想自定义一个返回错误的函数来做这个事,可能会写出以下代码:
package main
type data struct{}
func (this *data) Error() string { return "" }
func test() error {
var p *data = nil
func main() {
var e error = test()
if e == nil {
fmt.Println("e is nil")
fmt.Println("e is not nil")
但是很可惜,以上代码是有问题的。
$ cd $GOPATH/src/interface_test
$ go build
$ ./interface_test
e is not nil
我们可以来分析一下。error是一个接口类型,test方法中返回的指针p虽然数据是nil,但是由于它被返回成包装的error类型,也即它是有类型的。所以它的底层结构应该是(*data, nil),很明显它是非nil的。
可以打印观察下底层结构数据:
package main
type data struct{}
func (this *data) Error() string { return "" }
func test() error {
var p *data = nil
func main() {
var e error = test()
d := (*struct {
itab uintptr
data uintptr
})(unsafe.Pointer(&e))
fmt.Println(d)
$ cd $GOPATH/src/interface_test
$ go build
$ ./interface_test
正确的做法应该是:
package main
type data struct{}
func (this *data) Error() string { return "" }
func bad() bool {
return true
func test() error {
var p *data = nil
if bad() {
return nil
func main() {
var e error = test()
if e == nil {
fmt.Println("e is nil")
fmt.Println("e is not nil")
&&&&推荐文章:
【上篇】【下篇】为什么Go是一种设计糟糕的编程语言 - 文章 - 伯乐在线
& 为什么Go是一种设计糟糕的编程语言
好吧,我承认这个标题有点放肆。我多告诉你一点:我爱肆意妄言的标题,它能够吸引注意力。不管怎样,在这篇博文中我会试图证明 Go 是一个设计得很糟糕的语言(剧透:事实上它是)。我已经摆弄 Go 有几个月了,而且,我想我在六月某个时候运行了第一个 helloworld 程序。虽然我的数学不太好,但在那之后已经有四个月了,并且我的 Github 上已经有了几个 package。不必多说,我仍完全没有在生产中使用 Go 的经验,所以把我说的有关 “编码支持”、“部署”以及相关内容当作不可尽信的吧。
我喜欢 Go语言。自从试用了它以后我就爱上了。我花了几天来接受 Go 的语言习惯,来克服没有泛型的困难,了解奇怪的错误处理和 Go 的所有典型问题。我读了 ,以及 Dave Cheney 的博客上的许多文章,而且注意与 Go 有关的一切动向等等。我可以说我是一个活跃的社区成员!我爱 Go 而且我无法自拔—Go 令人惊奇。然而依我拙见,与它所宣传的正好相反,Go 是一个设计糟糕、劣质的语言。
Go 被认为是一个简练的编程语言。根据 Rob Pike 所说,他们使出了浑身解数来使这个语言的规范简单明了。这门语言的这一方面是令人惊奇的:你可以在几小时内学会基础并且直接开始编写能运行的代码,大多数情况下 Go 会如所期待的那样工作。你会被激怒,但是希望它管用。现实并不一样,Go语言并不是一个简洁,它只是低劣。以下有一些论点来证明。
理由1. 切片(Slice)操作压根就不对!
切片很棒,我真的很喜欢这个概念和一些用法。但是让我们花一秒钟,想象一下我们真的想要去用切片写一些代码。显而易见,切片存在于这门语言的灵魂中,它让 Go 强大。但是,再一次,在“理论”讨论的间隙,让我们想象一下我们有时会写一些实实在在的代码。以下列出的代码展示了你在 Go 中如何做列表操作。
// 请给我一些数字!
numbers := []int{1, 2, 3, 4, 5}
log(numbers)
// 1. [1 2 3 4 5]
log(numbers[2:])
// 2. [3 4 5]
log(numbers[1:3])
// 3. [2 3]
// 有趣的是,你不能使用负数索引
// 来自 Python 的 numbers[:-1] 并不能正确工作,相反的是,
// 你必须这样做:
log(numbers[:len(numbers)-1])
// 4. [1 2 3 4]
// 可读性真实“太好了”,Pike 先生!干的漂亮!
// 现在,让我们在尾部插入一个6:
numbers = append(numbers, 6)
log(numbers) // 5. [1 2 3 4 5 6]
// 把3从numbers中移除 :
numbers = append(numbers[:2], numbers[3:]...)
log(numbers)
// 6. [1 2 4 5 6]
// 想要插入一些数?别急,这里是一个Go语言*通用*最佳实践
// 我特别喜欢。。。哈哈哈。
numbers = append(numbers[:2], append([]int{3}, numbers[2:]...)...)
log(numbers)
// 7. [1 2 3 4 5 6]
// 为了拷贝一份切片,你需要这样做:
copiedNumbers := make([]int, len(numbers))
copy(copiedNumbers, numbers)
log(copiedNumbers)
// 8. [1 2 3 4 5 6]
//还有一些其他操作。。。
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// 请给我一些数字!numbers := []int{1, 2, 3, 4, 5}&log(numbers)&&&&&&&& // 1. [1 2 3 4 5]log(numbers[2:])&&&& // 2. [3 4 5]log(numbers[1:3])&&&&// 3. [2 3]&// 有趣的是,你不能使用负数索引//// 来自 Python 的 numbers[:-1] 并不能正确工作,相反的是,// 你必须这样做://log(numbers[:len(numbers)-1])&&&&// 4. [1 2 3 4]&// 可读性真实“太好了”,Pike 先生!干的漂亮!//// 现在,让我们在尾部插入一个6://numbers = append(numbers, 6)&log(numbers) // 5. [1 2 3 4 5 6]&// 把3从numbers中移除 ://numbers = append(numbers[:2], numbers[3:]...)&log(numbers)&&&&// 6. [1 2 4 5 6]&// 想要插入一些数?别急,这里是一个Go语言*通用*最佳实践//// 我特别喜欢。。。哈哈哈。//numbers = append(numbers[:2], append([]int{3}, numbers[2:]...)...)&log(numbers)&&&&// 7. [1 2 3 4 5 6]&// 为了拷贝一份切片,你需要这样做://copiedNumbers := make([]int, len(numbers))copy(copiedNumbers, numbers)&log(copiedNumbers)&&&&// 8. [1 2 3 4 5 6]&//还有一些其他操作。。。
信不信由你,这是 Go 程序员每天如何转换切片的真实写照。而且我们没有任何泛型机制,所以,哥们,你不能创造一个漂亮的 insert() 函数来掩盖这个痛苦。我在 playgroud 贴了这个,所以你不应该相信我:自己双击一下去亲自看看。
理由2. Nil 接口并不总是 nil :)
他们告诉我们“在 Go 中错误不只是字符串”,并且你不该把它们当字符串对待。比如,来自 Docker 的
在他精彩的“Go 中的7个失误以及如何避免”中如此讲过。
他们也说我应该总是返回 error 接口类型(为了一致性、可读性等等)。我在以下所列代码中就是这么做的。你会感到惊讶,但是这个程序真的会跟 Pike 先生 say hello,但是这是所期待的吗?
package main
import "fmt"
type MagicError struct{}
func (MagicError) Error() string {
return "[Magic]"
func Generate() *MagicError {
return nil
func Test() error {
return Generate()
func main() {
if Test() != nil {
fmt.Println("Hello, Mr. Pike!")
1234567891011121314151617181920212223
package main&import "fmt"&type MagicError struct{}&func (MagicError) Error() string { return "[Magic]"}&func Generate() *MagicError { return nil}&func Test() error { return Generate()}&func main() { if Test() != nil {
fmt.Println("Hello, Mr. Pike!") }}
是的,我知道为什么这会发生,因为我阅读了一堆复杂的关于接口和接口在 Go 中如何工作的资料。但是对于一个新手……拜托哥们,这是当头一棒!实际上,这是一个常见的陷阱。如你所见,没有这些让人心烦意乱的特性的 Go 是一个直接易学的语言,它偶尔说 nil 接口并不是)
理由3. 可笑的变量覆盖
为了以防万一你对这个术语不熟悉,让我引用一下 Wikipedia:”当在某个作用域(判定块、方法或者内部类)中声明的一个变量与作用域外的一个变量有相同的名字,变量覆盖就会发生。“看上去挺合理,一个相当普遍的做法是,多数的语言支持变量覆盖而且这没有问题。Go 并不是例外,但是却不太一样。下面是覆盖如何工作的:
package main
import "fmt"
func Secret() (int, error) {
return 42, nil
func main() {
number := 0
fmt.Println("before", number) // 0
// meet the shadowing
number, err := Secret()
if err != nil {
panic(err)
fmt.Println("inside", number) // 42
fmt.Println("after", number) // 0
12345678910111213141516171819202122232425
package main&import "fmt"&func Secret() (int, error) { return 42, nil}&func main() { number := 0& fmt.Println("before", number) // 0& {
// meet the shadowing
number, err := Secret()
if err != nil {
panic(err)
fmt.Println("inside", number) // 42 }& fmt.Println("after", number) // 0}
是的,我也认识到 := 操作符制造了一个新的变量并且赋了一个右值,所以根据语言规范这是一个完全合法的行为。但是这里有件有意思的事:试着去掉内部作用域——它会如期望的运行(”在42之后“)。否则,就跟变量覆盖问个好吧。
无需赘言,这不是什么我在午饭时想起来的一个好玩的例子,它是人们早晚会遇到的真实的东西。这周的早些时候我重构了一些 Go 代码,就遇到了整个问题两次。编译没问题,代码检查没问题,什么都没问题——代码就是不正常运行。
理由4. 你不能传递把 []struct 作为 []interface 传递
接口很棒,Pike&Co. 一直说它就是 Go 语言的一切:接口事关你如何处理泛型,如何做 mock 测试,它是多态的实现方法。让我告诉你吧,当我阅读“Effective Go”的时候我真心爱着接口,而且我一直爱着它。除了上面我提出的“nil 接口不是 nil”的问题外,这里有另一个令人讨厌的事让我认为接口在 Go 语言中没有得到头等支持。基本上,你不能传递一个结构的切片到一个接收接口类型切片的函数上:
package main
type FancyInt int
func (x FancyInt) String() string {
return strconv.Itoa(int(x))
type FancyRune rune
func (x FancyRune) String() string {
return string(x)
// 实际上,任何具有String()方法的对象
type Stringy interface {
String() string
// String, made of string representations of items given.
func Join(items []Stringy) (joined string) {
for _, item := range items {
joined += item.String()
func main() {
numbers := []FancyInt{1, 2, 3, 4, 5}
runes := []FancyRune{'a', 'b', 'c'}
// You can't do this!
// fmt.Println(Join(numbers))
// fmt.Println(Join(runes))
// prog.go:40: cannot use numbers (type []FancyInt) as type []Stringy in argument to Join
// prog.go:41: cannot use runes (type []FancyRune) as type []Stringy in argument to Join
// 相反,你应该这样做:
properNumbers := make([]Stringy, len(numbers))
for i, number := range numbers {
properNumbers[i] = number
properRunes := make([]Stringy, len(runes))
for i, r := range runes {
properRunes[i] = r
fmt.Println(Join(properNumbers))
fmt.Println(Join(properRunes))
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
package main&import ( "fmt" "strconv")&type FancyInt int&func (x FancyInt) String() string { return strconv.Itoa(int(x))}&type FancyRune rune&func (x FancyRune) String() string { return string(x)}&// 实际上,任何具有String()方法的对象type Stringy interface { String() string}&// String, made of string representations of items given.func Join(items []Stringy) (joined string) { for _, item := range items {
joined += item.String() }& return}&func main() { numbers := []FancyInt{1, 2, 3, 4, 5} runes := []FancyRune{'a', 'b', 'c'}& // You can't do this! // // fmt.Println(Join(numbers)) // fmt.Println(Join(runes)) // // prog.go:40: cannot use numbers (type []FancyInt) as type []Stringy in argument to Join // prog.go:41: cannot use runes (type []FancyRune) as type []Stringy in argument to Join // // 相反,你应该这样做: //& properNumbers := make([]Stringy, len(numbers)) for i, number := range numbers {
properNumbers[i] = number }& properRunes := make([]Stringy, len(runes)) for i, r := range runes {
properRunes[i] = r }& fmt.Println(Join(properNumbers)) fmt.Println(Join(properRunes))}
不出意外,这是个已知的根本没有被当作问题的问题。它只是 Go 的又一个可笑的事,对吧?我真的推荐你阅读一下相关的 wiki,你会发现为什么“传递结构切片作为借口切片”不可行。但是呀,好好想想!我们可以做到,这里没什么魔法,这只是编译器的问题。看,在 49-57行 我做了一个由 []struct 到 []interface的显式转换。为什么 Go 编译器不为我做这些?是的显示要比隐式好,但是WTF?
我只是无法忍受人们看着这种狗屁语言又一直说“好,挺好的”。并不是。这些让 Go 变成了一个糟糕的语言。
理由5. 不起眼的 range“按值”循环
这是我曾经遇到过的第一个语言问题。好吧,在 Go 中有一个 “for-range”循环,是用来遍历切片和监听 channel 的。它到处都用得到而且还不错。然而这里有一个小问题,大多数新手被坑在这上面:range 循环只是按值的,它只是值拷贝,你不能真的去做什么,它不是 C++ 中的 foreach。
package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4}
for _, number := range numbers {
fmt.Println(numbers) // [0 1 2 3 4]
for i, _ := range numbers {
numbers[i]++
fmt.Println(numbers) // [1 2 3 4 5]
12345678910111213141516171819
package main&import "fmt"&func main() { numbers := []int{0, 1, 2, 3, 4}& for _, number := range numbers {
number++ }& fmt.Println(numbers) // [0 1 2 3 4]& for i, _ := range numbers {
numbers[i]++ }& fmt.Println(numbers) // [1 2 3 4 5]}
请注意,我没有抱怨 Go 里没有按引用的 range,我抱怨的是 range 太不起眼。动词“range”有点像是说“遍历项目“,而不是”遍历项目的拷贝“。让我们看一眼”Effective Go“中的 For,它听起来一点也不像”遍历切片中的拷贝值“,一点也不。我同意这是个小问题,我很快(几分钟)就克服了它,但是没有经验的 gopher 也许会花上一些时间调试代码,惊讶于为什么值没有改变。你们至少可以在”Effective Go“里面把这点讲述明白。
理由6. 可疑的编译器严谨性
就如我之前已经告诉你的,Go被认为是一个有着严谨的编译器的,简单明了并且可读性高的语言。比如,你不能编译一个带有未使用的 import 的程序。为什么?只是因为 Pike 先生认为这是对的。信不信由你,未使用的 import 不是世界末日,我完全可以与其共存。我完全同意它不对而且编译器不惜打印出相关的警告,但是为什么你为了这么一个小事中止编译?就为了未使用的 import,当真?
Go1.5 引入了一个有趣的语言变化:现在你可以列出 map 字面量,而不必显示列出被包含的类型名。这花了他们五年(甚至更多)来认识到显示类型列出被滥用了。
另一个我在 Go 语言里非常享受的事情:逗号。你看,在 Go 中你可以自由地定义多行 import、const 或者 var 代码块:
"/some_guy/fancy"
One int = iota
VarName int = 35
12345678910111213
import (&&&&"fmt"&&&&"math"&&&&"/some_guy/fancy")const (&&&&One int = iota&&&&Two&&&&Three)var (&&&&VarName int = 35)
好吧,这挺好的。但是一旦它涉及到“可读性”,Rob Pike 认为加上逗号会很棒。某一刻,在加上逗号以后,他决定你应该也把结尾的逗号留着!所以你并不这样写:
numbers := []Object{
Object{"bla bla", 42}
Object("hahauha", 69}
numbers := []Object{&&&&Object{"bla bla", 42}&&&&Object("hahauha", 69}}
你必须这样写:
numbers := []Object{
Object{"bla bla", 42},
Object("hahauha", 69},
numbers := []Object{&&&&Object{"bla bla", 42},&&&&Object("hahauha", 69},}
我仍然怀疑为什么我们在 import/var/consts 代码块中可以忽略逗号,但是在列表和映射中不能。无论如何,Rob Pike 比我清楚!可读性万岁!
理由7. Go generate 太诡异了
首先,你要知道我没有反对代码生成。对于 Go 这样一个粗劣的语言,这也许是仅有的可用来避免拷贝-粘贴一些常见的东西的途径。然而,Go:generate——一个 Go 用户到处都用的代码生成工具,现在仅仅是垃圾而已。好吧,公平来说,这个工具本身还好,我喜欢它。而整个的方式是错的。我们看看吧,你要通过使用特别的魔法命令来生成一些代码。对,通过代码注释中的一些神奇的字节序列来做代码生成。
注释是用来解释代码,而不是生成代码。不过神奇的注释在当今的 Go 中是一种现象了。非常有意思的是,没人在乎,大家觉得这就挺好的。依我愚见,这绝对比吓人的未使用的 import 要糟糕。
如你所见,我没有抱怨泛型、错误处理、语法糖和其他 Go 相关的典型问题。我同意泛型不至关重要,但如果你去掉泛型,请给我们一些正常的代码生成工具而不是随机的乱七八糟的狗屎神奇注释。如果你去掉异常,请给我们安全地把接口与 nil 比较的能力。如果你去掉语法糖,请给我们一些能够如预期工作的代码,而不是一些像变量遮蔽这样的“哎呦卧槽“的东西。
总而言之,我会继续使用 Go。理由如下:因为我爱它。我恨它因为它就是堆垃圾,但是我爱它的社区,我爱它的工具,我爱巧妙的设计决定(接口你好)和整个生态。
嘿伙计,想尝试尝试 Go 吗?
(译者注:原文一些理由并不站得住脚,原文链接中的评论也值得一看。)
关于作者:
可能感兴趣的话题
为什么没有反方辩友发言?
关于伯乐在线博客
在这个信息爆炸的时代,人们已然被大量、快速并且简短的信息所包围。然而,我们相信:过多“快餐”式的阅读只会令人“虚胖”,缺乏实质的内涵。伯乐在线内容团队正试图以我们微薄的力量,把优秀的原创文章和译文分享给读者,为“快餐”添加一些“营养”元素。
新浪微博:
推荐微信号
(加好友请注明来意)
– 好的话题、有启发的回复、值得信赖的圈子
– 分享和发现有价值的内容与观点
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 翻译传播优秀的外文文章
– 国内外的精选文章
– UI,网页,交互和用户体验
– 专注iOS技术分享
– 专注Android技术分享
– JavaScript, HTML5, CSS
– 专注Java技术分享
– 专注Python技术分享
& 2017 伯乐在线}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信