写Go代码的你是否遇到过这些问题?弄明白了吗?

这几天整理了一些 Go 语言的小知识点,在这里分享给大家,大家在工作过程中多多少少都会遇到的,希望有帮助。多看几遍下面这些题,大家在敲代码时会少走很多弯路的。

我们看下面的代码:

写Go代码的你是否遇到过这些问题?弄明白了吗?

map

大家想一想这里输出的结果是什么?012 or 333?是的,你肯定答对了,这里打印出来的是333。这个其实很简单,对于指针熟悉的同学都会明白,首先 map 的 key 是 int 类型,而 value 是 int类型的指针,在 for 循环中,变量 i 的值从0到3,但是 i 的指针没有变,而且最终的 i 的值是 3,3 < 3 是不成立的,所以此时 i 的值为3并且 for 循环结束,因此 m 的所有 value 都是3了。那么如果我们想打印出来结果是 0、1、2应该怎么弄呢?

这里我说两个简单的方案:第一个是将 map 的 value 也转换成 int 类型,另外一个就是在 for 循环中,增加一个临时变量,保存 i 的值,然后将临时变量的指针存入 m 中。这两种方法都可以,主要看实际情况中 map 需要是什么的类型了。

这里还有一点关于 map 的初始化的一个建议,

m := make(map[int]*int)
m1 := make(map[int]*int,10)
var m2 map[int]*int

这里我列出了三种 map 初始化的方式,第一种和第二种的区别是有默认大小,第三种知识声明了变量,第三种我们在使用的时候需要注意的是,此时并不能向 m2 中添加,需要再使用 make 函数初始化一下再使用。对于第一种和第二种我们可以直接使用,但是这两种方法对于我们的性能有很大影响,对于第一种,我们默认大小就是0,当添加元素时,会重新分配空间,也就是说,当此时 map 的空间不能存下新的元素时,系统会重新分配空间,这样使程序的性能大大的下降,这里和 slice 很类似,slice 会在空间不够的情况下再寻找当前大小二倍的空间,这个过程很消耗时间性能的。所以在我们已知或者预计大小时,在初始化时就指定大小。

我们接着看下一个题:

type T struct {
name string
}
func main() {
m := map[string]T{"x": {"one"}}
m["x"].name = "two"
print(m["x"])
}

大家思考一下这里,上面的代码是输出什么结果?或者还是编译不通过呢?

聪明的你肯定又对了,这里是编译不通过的:cannot assign to struct field m["x"].name in map。首先按照我们的逻辑看是没有问题的,但是我们需要记住的一点就是:map中的元素不是变量,因此不能寻址。map 作为一个封装好的数据结构,由于它底层可能会由于数据扩张而进行迁移,所以拒绝直接寻址,避免产生野指针,map 中的key在不存在的时候,赋值语句其实会进行新的k-v值的插入,所以拒绝直接寻址结构体内的字段,以防结构体不存在的时候可能造成的错误。

那么我们如何解决这个问题呢?我的建议是可以将 map 的 value 改为指针类型,这时是可以的,编译通过,而且也是我们需要的值。

接着看:

写Go代码的你是否遇到过这些问题?弄明白了吗?

接口

这里大家觉得 Value struct 是否实现了 Copyable 接口?

首先我们要知道所有 struct 都实现了empty interface{} 接口,所以 Value 也实现了 empty interface{},这是没错的,但是即使如此,我们也不可以认为 Value 实现了 Copyable 接口,官方给出的原因是:In Go method types must match exactly,也就是说一定要完全匹配。在上面的例子中,我们将 Copy() 的返回值改为 interface{} 之后,就可以了。

我们接着往下看:

写Go代码的你是否遇到过这些问题?弄明白了吗?

接口

大家觉得上面的 T 和 T2 哪一个实现了 Equaler 接口呢?

通过 Copyable 的例子,相信大家这里都有自己的答案了,没错,这里 T2 实现了 Equaler 接口。这两个例子主要就是告诉大家,Go 中实现接口时需要注意这一点,虽然这种情况在其他语言里可以通过,但是 Go 是不允许的?

这里额外再说一点,Go 中是没有 implements 关键字的,想要实现某个接口,只需要实现了接口里所有的方法即可,在一些大的项目中,我们的一个 struct 是很容易实现很多接口的,即使某些接口不是我们所需要的,那么我们如何知道这个 struct 需要实现哪些接口呢?这里官方给出了两个建议:第一个就是:

type T struct{}
var _ I = T{}
var _ I = (*T)(nil)

在这里我们断言 T 实现了 I 接口,而且这样写还有一个好处就是如果 T 没有实现 I 接口,这里编译是不通过的。

还有一种方法:

type Fooer interface {
Foo()
ImplementsFooer()
}

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

就是在我们的接口中增加一个独有的方法,而方法名字就是见名知意,如果要实现这个接口,我们就必须有这个方法,如果我们一个 struct 需要实现多个接口,这个 struct 就需要具有多个这个见名知意的方法。

以上这两种方法都有各自的好处,第一个可以看出来 struct 实现了哪些接口,而且如果没有实现的话,编译不通过。第二个好处就是见名知意,我看到这个函数我就知道这个 struct 实现了哪个(哪些)接口。任何事务都是没有完美的,这样写也是有缺点的,增加了代码量,如果一个 struct 实现了50个接口,那么就需要在每个接口中都实现见名知意的方法,而且 struct 都要实现。具体情况就看大家需求吧。在我之前关于 Go 语言接口的文章中,有很多人评论这种实现接口的方式不利于阅读代码,所以在这里给大家一点建议,这种方式确实是利于阅读代码的。

好了,接着下一题:

func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p
}

这里说明一下,MyError 实现了 error 接口。大家觉得这样的代码有什么问题吗?

if err != nil 这样的代码相信大家已经敲了无数次了,在这个例子中,我们使用自己的错误类型,但是返回值是 error 类型。这时在函数调用的地方我们如果还用 if err != nil 的方式来判断就不可以了,这样返回的永远都不是 nil。这个问题就需要大家对 Go 语言的 interface 有一定的了解了(大家可以看我的文章:go语言的interface为什么好用?),这里再简单说一下,对于接口我们判断是否为 nil,需要接口的 type 和 value 都是 nil 才可以,这个例子中,返回值 p ,它的 Type 是 myError,Value 虽然是 nil,但是 p 并不是 nil。如果大家一定要使用自己的错误类型,在返回的地方一定要注意一下,我们可以直接使用 return nil 代替现在的写法。

我们看最后一段代码:

type T1 int
type T2 int
var t1 T1
var x = T2(t1)
var st1 []T1
var sx = ([]T2)(st1)

大家觉得上面的代码哪里有问题?

这个主要是类型转换的问题,T1、T2 的底层都是 int 类型,我们可以将变量 t1 直接强制转换成 T2 类型,这是没有问题的。但是对于 st1 是 t1 类型的数组,这里我们就不可以直接强制转换为 T2 类型的数组了。这里也是大家写代码是需要注意的一点。

好了,今天就说这么多吧,这些题都不是很难,但是在工作中都是非常容易遇到的,有时候我们遇到了需要很长时间查阅资料等才弄明白,如果能事先明白这些,工作中就会少走弯路。上面的例子有很多是来自 Go 语言的官方网站,大家也可以去自行阅读,关于 Go 语言的起源等,官方都给出了解释。

如果有更好的答案或者题欢迎评论讨论,感谢阅读,欢迎转发~


分享到:


相關文章: