Interview Questions for Go

Published: by Creative Commons Licence

基础

  1. Go语言的特征:静态类型、编译型、并发型、垃圾回收、快速编译、跨平台。

  2. Go语言的基本类型:布尔型、数字类型、字符串类型、派生类型。

数组

  1. 数组与切片的区别:

    数组 切片
    长度固定 长度可变
    值类型 引用类型

    引申:[3]int[4]int是不同的类型,但[]int是一个类型。

  2. 切片的数据结构

     type slice struct {
         array unsafe.Pointer    // 指向底层数组的指针
         len   int            // 切片的长度
         cap   int         // 切片的容量
     }
    
  3. 修改切片中的值:

     func main() {
         s := []int{1, 2, 3}
         modifySlice(s)
         fmt.Println(s) // [1, 2, 3]
     }
    
     func modifySlice(s []int) {
         s[0] = 100
     }
    

    传递切片时,传递的是切片的引用,所以在函数内部修改切片的值,会影响到原切片。

  4. 切片的扩容策略:

    • 如果新申请的容量小于256,则新容量为原容量的2倍。
    • 若新申请的容量大于等于256,则newcap = oldcap+(oldcap+3*256)/4
    • 使用growslice函数进内存对齐。
     func func1() {
         s := []int{5}
         s = append(s, 7)
         s = append(s, 9)
         x := append(s, 11)
         y := append(s, 12)
         fmt.Println(s, x, y)    // [5 7 9] [5 7 9 12] [5 7 9 12]
     }
    
     func func2() {
         s := []int{1,2}
         s = append(s,4,5,6)
         fmt.Printf("len=%d, cap=%d",len(s),cap(s))  // len=5, cap=6
     }
    
  5. append函数:

    • append函数返回值是一个新的切片,且必须作为返回值接收。
    • append(slice, elem1, elem2)
    • append(slice, slice2...)
  6. 切片作为函数参数时:

    • 如果直接传递切片,实参slice并不会被函数操作改变。
    • 如果传递切片的指针,实参slice会被函数操作改变。
    • 但是通过指针传递切片,会导致切片的底层数组被修改,所以不推荐。
     func main() {
         s := []int{1, 2, 3}
         modifySlice(s)
         fmt.Println(s) // [1, 2, 3]
         modifySlice2(&s)
         fmt.Println(s) // [100, 2, 3]
     }
    
     func modifySlice(s []int) {
         s[0] = 100
     }
    
     func modifySlice2(s *[]int) {
         (*s)[0] = 100
     }
    

Map

  1. Map的数据结构:

     type hmap struct {
         count int
         flags uint8
         B     uint8
         noverflow uint16
         hash0 uint32
         buckets unsafe.Pointer
         oldbuckets unsafe.Pointer
         nevacuate uintptr
         extra *mapextra
     }
    
  2. go的map是使用哈希表实现的,并采用链表法解决哈希冲突。

  3. map的操作

    • make(map[keyType]valueType, cap):创建map。
    • map[key] = value:添加元素。
    • delete(map, key):删除元素。
    • value, ok := map[key] or value = map[key]:获取元素。
    • len(map):获取元素个数。
  4. map的遍历

     func main() {
         m := map[string]int{
             "a": 1,
             "b": 2,
             "c": 3,
         }
         for k, v := range m {
             fmt.Println(k, v)
         }
     }
    
  5. map的key

    • map的key必须支持==!=操作。
    • map的key不能是函数类型、map类型和切片类型。
    • struct类型不包含上述字段,也可以作为key。
    • 无序性:map的遍历顺序与添加顺序无关。
  6. 无法对map进行取址操作,因为map可能会进行扩容,导致地址发生变化。

  7. 比较两个map是否相等:

    • map只能与nil比较。
    • map不能使用==比较,只能遍历比较。
     func main() {
         m1 := map[string]int{
             "a": 1,
             "b": 2,
             "c": 3,
         }
         m2 := map[string]int{
             "a": 1,
             "b": 2,
             "c": 3,
         }
         fmt.Println(m1 == m2) // invalid operation: m1 == m2 (map can only be compared to nil)
         fmt.Println(m1 == nil) // false
         fmt.Println(m1Equal(m1, m2)) // true
     }
    
     func m1Equal(m1, m2 map[string]int) bool {
         if len(m1) != len(m2) {
             return false
         }
         for k, v := range m1 {
             if v2, ok := m2[k]; !ok || v != v2 {
                 return false
             }
         }
         return true
     }
    
  8. map不是线程安全的,如果需要并发读写,需要加锁。

  9. map的扩容条件:

    • 装载因子超过阈值(6.5)
    • overflow 的 bucket 数量过多
    • 采用渐进式搬迁扩容策略。

接口

  1. 值类型接收者会隐含地创建指针型接收者的方法。参考:值接收者和指针接收者
     type greeter interface {
         greet()
         farewell()
     }
    
     type english struct {
     }
    
     func (e english) greet() {
         fmt.Println("Hello")
     }
    
     func (e *english) farewell() {
         fmt.Println("Goodbye")
     }
    
     func main() {
         var e greeter = &english{}
         e.greet()
         e.farewell()
     }
    

    这里的e是一个指针,但是e.greet()是可以调用的,因为编译器会隐式地将e.greet()转换为(*e).greet(), 即由值类型的接收者为指针类型接收者创建了一个方法。相反如果e是一个值类型,那么e.farewell()就会报错,因为编译器不会隐式地将e.farewell()转换为(&e).farewell()

  2. 接口的实现:

    • 接口的实现是隐式的,只要实现了接口的方法,就实现了该接口。
    • 接口的实现是非侵入式的,不需要在实现类中显式地声明实现了哪些接口。
  3. iface vs eface

    • iface:定义了接口的类型,包含两个指针,一个指向类型的方法表,一个指向实际的数据。只有当接口存储的类型和对象都为nil时,接口才为nil。
    • eface:空接口,包含两个指针,一个指向类型的类型信息,一个指向实际的数据。
  4. 检查T类型是否实现了某个接口:

     var _ interface{} = (*T)(nil)
     var _ interface{} = T{}
    
  5. 断言

     var i interface{} = "hello"
     if s, ok := i.(string); ok {
         fmt.Println(s)
     }
    
  6. 使用接口实现多态

     type animal interface {
         call()
         grow()
     }
    
     type cat struct {
         age int
     }
    
     func (c cat) call() {
         println("miao")
     }
    
     func (c *cat) grow() {
         c.age++
     }
    
     type dog struct {
         age int
     }
    
     func (d dog) call() {
         println("wang")
     }
    
     func (d *dog) grow() {
         d.age += 2
     }
    
     func howl(a animal) {
         a.call()
     }
    
     func grow(a animal) {
         a.grow()
     }
    
     func main() {
         c := cat{age: 1}
         howl(&c)
         grow(&c)
         println(c.age)
         d := dog{age: 2}
         howl(&d)
         grow(&d)
         println(d.age)
     }
    

    内置函数

  7. newmake的区别:

    • new用于值类型和用户定义的类型,如自定义结构体。
    • make用于内置引用类型,如mapslicechannel
  8. 常用的内置函数:

    • len:返回长度。
    • cap:返回容量。
    • append:追加元素。
    • copy:复制切片。
    • close:关闭通道。
    • delete:删除map中的元素。
    • new:分配内存,返回指针。
    • make:分配内存,返回引用类型。
    • panic:停止常规的goroutine。
    • recover:允许程序恢复goroutine。
    • reflect.TypeOf:返回变量的实际类型。
    • reflect.ValueOf:返回变量的实际值。
    • unsafe.Sizeof:返回变量的字节大小。

Reference