Golang面试基础知识点大全 5/5

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{}  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.Dof(s, 4)

或者下面这样也是正确的,

s := S{"ben"}f := (*S).Dof(&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.0var v1 *int64 = (*int64)(&fv)

但是借助于unsafe.Pointer却可以做到不同类型的指针相互转换。但是要注意,使用包unsafe的应用程序可能是不可移植的,而且不受golang 1.x兼容性保证。所以建议不要轻易使用。

将上面错误的例子稍微修改一下,借助于unsafe.Pointer就可以将*float64转换成*int64,

fv := 3.0var 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获得第一手区块链、加密货币新闻报道。

Click to rate this post!
[Total: 0 Average: 0]

人已赞赏
Go语言技术开发名家说每日优选

我用 Go 生成的随机数为什么不随机?随机数是怎样产生的

2020-9-23 10:35:08

Go语言技术开发小白百科每日优选

手把手教你学之golang反射(上)

2020-9-23 10:37:30

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索