Golang虽然易学易用,但里面有很多坑,不仅新手容易迷惑,对于使用Golang好几年的开发者也很有可能犯错。这些坑自然也是Golang面试过程中容易问的点。
所以零君用5篇文章详细、系统的总结了这些知识点(坑),本文是此系列的第五篇(最后一篇)文章。
接收器(Receiver)
在golang中,方法(function)和函数(method)是两个不同的概念;方法是带接收器(receiver)的函数。虽然golang不是面向对象的语言,但这里也体现了面向对象中的封装的思想。前一篇文章中讲的,在struct里面内嵌字段的方式则体现了继承的思想;golang中的Interface更是抽象和多态的体现。
只有defined type(用关键字type定义的类型)才能用作方法的receiver,并且defined type与method必须定义在同一个包中。例如,下面的写法就是错误的,编译通不过,因为map不是一个defined type,
func (i map[string]int) DoSomething() {
fmt.Println("do somthing")
}
正确的写法应该是:
type MyMap map[string]int
func (i MyMap) DoSomething() {
fmt.Println("do somthing")
}
receiver既可以是defined type(为了行文简洁,后面用T直接代替),也可以是指向defined type的指针。例如,下面的例子中,receiver的类型是结构体S,有两个方法与S绑定,其中Do1()绑定的是S,而Do2()绑定的*S。
type S struct{}
func (s S) Do1() {
fmt.Println("S Do1()")
}
func (s *S) Do2() {
fmt.Println("S Do1()")
}
这里要注意,既可以使用T类型的变量,也可以使用*T类型的指针变量调用与T或*T绑定的任意方法。例如,下面的例子则是使用S类型的变量调用Do1()和Do2(),
= S{} :
s.Do1()
s.Do2()
而下面的例子则是使用*S类型的指针变量调用方法,
s := &S{}
s.Do1()
s.Do2()
实际上,在下面两种情况下,golang编译器会自动做转换:
1、方法前面的receiver参数是T,而用*T去调用该方法,golang会自动先将*T转换成T,然后再调用方法;
2、方法前面的receiver参数是*T,而用T去调用该方法,golang会自动先将T转换成&T,然后再调用方法;
无论是哪种调用组合,只要方法前面的receiver参数是*T,那么方法内对receiver参数的修改,都会保存下来,调用者会看到这些修改。反之,如果方面前面的receiver参数是T,那么方法只是在receiver的副本上修改。例如,下面的例子中,只有方法Do2()对receiver参数的修改会被调用者看到。
type S struct {
age int
}
func (s S) Do1() {
fmt.Println("S Do1()")
s.age = 11
}
func (s *S) Do2() {
fmt.Println("S Do2()")
s.age = 12
}
当receiver/method与接口结合起来使用,又稍微有点不同了。例如,下面的例子中定义了一个接口,在接口中定义了两个方法Do1和Do2。结构体S绑定了两个方法,Do1对应的receiver参数是类型S,而Do2对应的receiver参数是类型*S。函数test的参数是Interface类型的变量。
type Interface interface {
Do1()
Do2()
}
type S struct{}
func (s S) Do1() {
fmt.Println("S Do1()")
}
func (s *S) Do2() {
fmt.Println("S Do2()")
}
func test(h Interface) {
h.Do1()
h.Do2()
}
这里一定要注意,调用上面例子中的函数test时,必须传入*S类型的变量。例如,下面这种调用方法是错误的,编译通不过,因为编译器认为S并没有实现接口Interface,更进一步的原因是S没有实现Interface中的Do2,因为Do2对应的receiver是*S。
s := S{}
test(s)
解决办法有两个,第一种解决办法是传*S类型的变量给函数test,如下:
s := S{}
test(&s)
另一种解决办法就是将Do2前面的receiver参数改成S,如下:
func (s S) Do2() {
fmt.Println("S Do2()")
}
以上两种解决方案各有优缺点。当需要在方法内修改receiver参数时,receiver参数必须用*T。如果方法内不修改receiver参数,再看receiver包含的数据是否很大,如果很大,receiver参数也尽量采用*T的形式,否则可以考虑采用T的形式。
另外,要注意的是,一个方法与”将receiver参数作为第一个参数的函数”是等价的,它们本质上具有相同的类型,所以可以将方法调用转化为相应的普通函数调用。例如,下面为结构体S绑定了一个方法Do。
type S struct {
name string
}
func (s S) Do(v int) {
fmt.Println(s.name, v)
}
正常情况下,用类似下面的代码来调用方法Do,
s := S{"ben"}
s.Do(4)
或者用指针形式的receiver,也是正确的,
s := &S{"ben"}
s.Do(4)
其实还可以将方法调用转换成普通函数的方式,例如,下面的例子将receiver作为第一个参数调用函数f,
s := S{"ben"}
f := S.Do
f(s, 4)
或者下面这样也是正确的,
s := S{"ben"}
f := (*S).Do
f(&s, 4)
最后捎带提醒一下,当你为某个defined type实现了方法”String() string”,就一定不要在方法内调用fmt.Sprintf,否则会引起无限递归循环调用。因为实现了方法”String() string”,就相当于实现了接口Stringer(如下)。那么通过fmt.PrintXXX输出defined type类型的变量时,就会自动调用String()方法,如果String()方法内调用了fmt.Sprintf,那么又会自动递归调用String()方法,从而无限递归循环调用下去,最终会栈溢出。
// Stringer is implemented by any value that has a String method,
// which defines the ``native'' format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
String() string
}
函数返回值
在golang中,如果一个函数有返回值,可以为每个返回值指定一个变量名,也可以不指定。但是要记住,要么全部都指定,要么都不指定,不允许部分指定。例如,下面的函数test有两个返回值,而且指定了变量名,分别为a和err,
func test() (a int, err error) {
return 4, nil
}
上面的例子体现不出指定变量名的用处,稍微修改一下,就明白了。下面的例子与上面的代码作用完全相同,因为函数体内已经为返回变量设置了值,所以return后面不用再列出返回值了。当然,return后面也可以列出返回值,如果列出了,那么最终的返回值就是return后面列出的返回值。
func test() (a int, err error) {
a = 4
err = nil
return
}
带命名的返回值本身并不复杂,但与defer结合起来使用就容易使人迷惑了。记住一个原则,函数执行return的流程是:先设置返回变量的值,然后再执行defer函数。例如,下面的例子,当执行到return时,先将5和nil分别设置给返回变量a和err;然后再执行defer函数,执行a++,返回变量a的值变成了6。所以最后返回值是6和nil。
func test() (a int, err error) {
defer func() {
a++
}()
return 5, nil
}
另外一个容易踩的坑是与变量的作用域相关。例如,下面的例子中,在for循环内的变量a是一个新定义的临时变量,与返回变量a没有关系。
func test() (a int, err error) {
for i := 0; i < 3; i++ {
a := calc()
fmt.Println(a)
}
return
}
当一个返回变量被一个同名的临时变量或常量掩盖时,是不允许在该临时变量的作用域内执行不带返回参数的return。例如,下面例子中第5行的return不带返回值,这样的写法是错误的,编译通不过;因为返回变量a和err被for循环内的同名临时变量掩盖了。解决办法有两个:1、return后面列出返回值;2、跳出临时变量的作用域之后,再用不带返回值的return。
func test() (a int, err error) {
for i := 0; i < 3; i++ {
a, err := doSomthing()
if err != nil {
return
}
fmt.Println(a, err)
}
return
}
包(package)
包的主要作用是组织源代码文件。包中有一种特殊的函数init,没有参数,也没有返回值,用来执行初始化的操作。注意,函数init是在包被import时,自动执行的,不能显示调用。
同一个包中可以定义多个init函数,位于同一个源文件中的init函数按照定义的先后顺序执行,不同源文件中的init则没有明确的顺序。但是如果当前包又import了其它包,那么会先执行依赖包中的init函数。
如果一个包被import了多次,它的init函数只会执行一次。
与变量定义之后必须使用一样,import了一个包之后,也必须使用这个包。如果只想利用import一个包产生的副作用(执行init函数),那么可以采用下划线的形式,例如,下面的例子import了包”time”,但并不打算使用该包,
import (
"fmt"
_ "time"
)
一般情况下,包的名字通常与目录名相同,同一个目录下的所有文件属于同一个包。但是有一个特例,就是测试源文件允许定义在不同的包,但包名必须是在原包名后加上“_test”。例如假设有一个包名是”abc”,源文件都在目录”abc”里面;但是测试源文件虽然也在目录”abc”里面,但包名可以是”abc_test”。golang标准库中有很多这样的例子,例如src/runtime/string.go文件位于包runtime中,而src/runtime/string_test.go位于包runtime_test中。这样带来的一个好处是,在测试程序中只能看到对应的源文件中导出的变量和函数,与实际使用场景完全一样。
new & make
new和make都是golang中的预定义标识符,都是golang的内置函数。它们的区别在于new是在运行时为某个类型T的变量分配内存,返回的是指向新分配内存的指针*T。make虽然也用来分配内存,但make只能用来操作slice、map、channel这三种类型,而且返回值是已经初始化过的值,而不是指针。
这里附带提一句,在一个函数中返回一个临时变量的地址之后,只要调用者继续持有该指针,不用担心该变量的空间会被回收。例如,下面的例子中,函数test返回一个结构体临时变量的指针,只要调用者继续持有该指针,这个结构体的空间就不会被回收,
type S struct {
age int
name string
}
func test() *S {
return &S{
age: 30,
name: "ben",
}
}
unsafe.Pointer
golang中,不同类型的指针之间不能进行类型转换。例如,下面的写法是错误的,
fv := 3.0
var v1 *int64 = (*int64)(&fv)
但是借助于unsafe.Pointer却可以做到不同类型的指针相互转换。但是要注意,使用包unsafe的应用程序可能是不可移植的,而且不受golang 1.x兼容性保证。所以建议不要轻易使用。
将上面错误的例子稍微修改一下,借助于unsafe.Pointer就可以将*float64转换成*int64,
fv := 3.0
var v1 *int64 = (*int64)(unsafe.Pointer(&fv))
包unsafe的另一个典型的应用场景是修改struct的字段。例如,下面的例子,通过结合使用unsafe.Pointer、unsafe.Offsetof以及uintptr,完成了对结构体成员的修改,
package main
import (
"fmt"
"unsafe"
)
type S struct {
age int
name string
}
func main() {
s := S{20, "ben"}
ps := unsafe.Pointer(&s)
pAge := (*int)(ps)
*pAge = 30
pName := (*string)(unsafe.Pointer(uintptr(ps) + unsafe.Offsetof(s.name)))
*pName = "alice"
fmt.Println(s)
}
sort & heap
golang标准库中的两个包sort和heap提供了对排序和堆的支持。sort中定义了下面的接口,任何容器只要实现了这个接口,就可以直接调用sort.Sort(h)进行排序,或者调用sort.Sort(sort.Reverse(h))进行反向排序。
// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
sort为slice提供了一个更便捷的排序函数sort.Slice。应用层只需要提供Less函数的实现。例如,下面的例子就是一个使用sort.Slice的例子,最后输出为:
[1 2 3 4]
package main
import (
"fmt"
"sort"
)
func main() {
s := []int{1, 4, 2, 3}
sort.Slice(s, func(i, j int) bool {
return s[i] < s[j]
})
fmt.Println(s)
}
包heap中定义了下面的接口,
type Interface interface {
sort.Interface
Push(x interface{}) // add x as element Len()
Pop() interface{} // remove and return element Len() - 1.
}
任何容器只要实现了上面的接口,便可直接调用包heap中的下列函数,
func Init(h Interface)
func Push(h Interface, x interface{})
func Pop(h Interface) interface{}
func Remove(h Interface, i int) interface{}
func Fix(h Interface, i int)
下面是一个完整的使用heap的例子,最后输出结果为:
3 4 5 6 7 9
package main
import (
"container/heap"
"fmt"
)
type S struct {
items []interface{}
}
func (s *S) Len() int {
return len(s.items)
}
func (s *S) Less(i, j int) bool {
return s.items[i].(int) < s.items[j].(int)
}
func (s *S) Swap(i, j int) {
s.items[i], s.items[j] = s.items[j], s.items[i]
}
func (s *S) Push(v interface{}) {
s.items = append(s.items, v)
}
func (s *S) Pop() interface{} {
n := s.Len()
ret := s.items[n-1]
s.items = s.items[:(n - 1)]
return ret
}
func main() {
s := S{
items: []interface{}{4, 9, 3, 5, 7},
}
heap.Init(&s)
heap.Push(&s, 6)
for s.Len() > 0 {
fmt.Printf("%d ", heap.Pop(&s))
}
}
上面的例子不算太复杂,就不做过多的解释了。我在自己的开源项目gocontainer中对golang标准库中的heap的实现做了一些改动,里面有更详细的关于heap的介绍。具体请参考:
https://github.com/ahrtr/gocontainer/blob/master/utils/heap.go
不管是sort还是heap,golang标准库只是提供了标准的接口以及函数,供应用层使用。应用层需要实现标准库定义的接口,并且需要自己管理数据结构,但是可以利用标准库提供的函数来操作这些数据结构。
总结
这个系列的五篇文章终于都写完了,我相信对于各个层次的golang开发者都会有帮助。当然前提是,您能沉下心来仔细阅读。不过注意一点,阅读这五篇文章,要求读者最好对golang的语法有最基本的了解。
如果您有任何建议,欢迎给我留言!
–5/5 END–
在比特币日报读懂区块链和数字货币,加入Telegram获得第一手区块链、加密货币新闻报道。