欢迎来看!

Andelu

事无繁琐,在于人

Go基础面经题

Go基础

1. 基础特性

  1. Go的优势

    • 天生支持并发,性能高。

    • 单一的标准代码格式,比其他语言更具可读性。

    • 自动垃圾收集机制比Java和Python更有效,因为它与程序同时执行。

  2. Go数据类型

    • int, string, float, bool, array, slice, map, channel, pointer, struct, interface, method
  3. go中的25个关键字

    • 程序声明2个:
      package import
    • 程序实体声明和定义8个:
      var const type func struct map chan interface
    • 程序流程控制15个:
      for range continue break select switch case default if else fallthrough defer go goto return
  4. Go程序中的包是什么?

    • 项目中包含go源文件以及其它包的目录,源文件中的函数、变量、类型都存储在该包中
    • 每个源文件都属于一个包,该包在文件顶部使用 package packageName 声明
    • 当我们在源文件中引用第三包时,需要还用 import packageName
  5. Go支持什么形式的类型转换?如何实现整数转为浮点数

    • go支持显示类型转换,即严格强制类型转换

    • a := 15
      b := float64(a)
      fmt.Println(b, reflect.TypeOf(b))  
      

2. 初级语法

  1. =:= 的区别?

    • := 声明+赋值= 仅赋值

    • var foo int
      foo = 10
      // 等价于
      foo := 10
      
  2. 指针的作用?

    • 指针用来保存变量的地址。

    • 例如:

    • var x =  5
      var p *int = &x
      fmt.Printf("x = %d",  *p) // x 可以用 *p 访问*
      
    • *运算符,也称为解引用运算符,用于访问地址中的值。

    • &运算符,也称为地址运算符,用于返回变量的地址。

  3. Go 允许多个返回值吗?

    • 允许

    • func swap(x, y string) (string, string) {
         return y, x
      }
      
      func main() {
         a, b := swap("A", "B")
         fmt.Println(a, b) // B A
      }
      
  4. Go 有异常类型吗?

    • Go 没有异常类型,只有错误类型(Error),通常使用返回值来表示异常状态。

    • f, err := os.Open("test.txt")
      if err != nil {
          log.Fatal(err)
      }
      
  5. 什么是协程(Goroutine)?

    • Goroutine 是与其他函数或方法同时运行的函数或方法。 Goroutines 可以被认为是轻量级的线程。 与线程相比,创建 Goroutine 的开销很小。 Go应用程序同时运行数千个 Goroutine 是非常常见的做法。
  6. 如何高效地拼接字符串?

    • Go 语言中,字符串是只读的,也就意味着每次修改操作都会创建一个新的字符串。如果需要拼接多次,应使用 strings.Builder,最小化内存拷贝次数。

    • var str strings.Builder
      for i := 0; i < 1000; i++ {
          str.WriteString("a")
      }
      fmt.Println(str.String())
      
  7. 什么是 rune 类型?

    • ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。

    • Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。例如下面的例子中 语 和 言 使用 UTF-8 编码后各占 3 个 byte,因此 len("Go语言") 等于 8,当然我们也可以将字符串转换为 rune 序列。

    • fmt.Println(len("Go语言")) // 8
      fmt.Println(len([]rune("Go语言"))) // 4
      
  8. Go 支持默认参数或可选参数吗?

    • Go 语言不支持可选参数(python 支持),也不支持方法重载(java支持)。
  9. 如何交换 2 个变量的值?

    1. a, b := "A", "B"
      a, b = b, a
      fmt.Println(a, b) // B A
      
  10. Go 语言 tag 的用处?

    1. tag 可以理解为 struct 字段的注解,可以用来定义字段的一个或多个属性。框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。tag 丰富了代码的语义,增强了灵活性。

    2. 例如:

    3. package main
      
      import "fmt"
      import "encoding/json"
      
      type Stu struct {
      	Name string `json:"stu_name"`
      	ID   string `json:"stu_id"`
      	Age  int    `json:"-"`
      }
      
      func main() {
      	buf, _ := json.Marshal(Stu{"Tom", "t001", 18})
      	fmt.Printf("%s\n", buf)
      }
      
    4. 这个例子使用 tag 定义了结构体字段与 json 字段的转换关系,Name -> stu_name, ID -> stu_id,忽略 Age 字段。很方便地实现了 Go 结构体与不同规范的 json 文本之间的转换。

  11. 字符串打印时,%v%+v 的区别

    • %v 和 %+v 都可以用来打印 struct 的值,区别在于 %v 仅打印各个字段的值,%+v 还会打印各个字段的名称。

    • type Stu struct {
      	Name string
      }
      
      func main() {
      	fmt.Printf("%v\n", Stu{"Tom"}) // {Tom}
      	fmt.Printf("%+v\n", Stu{"Tom"}) // {Name:Tom}
      }
      
    • 但如果结构体定义了 String() 方法,%v 和 %+v 都会调用 String() 覆盖默认值。

  12. Go 语言中如何表示枚举值(enums)?

    • 通常使用常量(const) 来表示枚举值。

    • type StuType int32
      
      const (
      	Type1 StuType = iota
      	Type2
      	Type3
      	Type4
      )
      
      func main() {
      	fmt.Println(Type1, Type2, Type3, Type4) // 0, 1, 2, 3
      }
      
    • 参考 What is an idiomatic way of representing enums in Go? - StackOverflow

  13. 空 struct{} 的用途?

    • 使用空结构体 struct{} 可以节省内存,一般作为占位符使用,表明这里并不需要一个值。

      1. fmt.Println(unsafe.Sizeof(struct{}{})) // 0
        
    • 比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。

      1. type Set map[string]struct{}
        
        func main() {
        	set := make(Set)
        
        	for _, item := range []string{"A", "A", "B", "C"} {
        		set[item] = struct{}{}
        	}
        	fmt.Println(len(set)) // 3
        	if _, ok := set["A"]; ok {
        		fmt.Println("A exists") // A exists
        	}
        }
        
    • 再比如,使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{} 代替。

      1. func main() {
        	ch := make(chan struct{}, 1)
        	go func() {
        		<-ch
        		// do something
        	}()
        	ch <- struct{}{}
        	// ...
        }
        
    • 再比如,声明只包含方法的结构体。

      1. type Lamp struct{}
        
        func (l Lamp) On() {
                println("On")
        
        }
        func (l Lamp) Off() {
                println("Off")
        }
        
  14. go中的cap函数可以作用于哪些内容?

    • 可作用于的类型有:

      • 数组(array)
      • 切片(slice)
      • 通道(channel)
    • 查看他们的容量大小,而不是装的数据大小

  15. go语言中new的作用是什么?

    • 使用new函数来分配内存空间
    • 传递给new函数的是一个类型,而不是一个值
    • 返回值是指向这个新分配的地址的指针
  16. go语言中的make作用是什么?

    • 分配内存空间并进行初始化, 返回值是该类型的实例而不是指针
    • make只能接收三种类型当做参数:slice、map、channel
  17. 总结make和new的区别?

    1. new可以接收任意内置类型当做参数,返回的是对应类型的指针
    2. make只能接收slice、map、channel当做参数,返回值是对应类型的实例
  18. 如何在运行时检查变量类型?

    • 类型开关(Type Switch)是在运行时检查变量类型的最佳方式。
    • 类型开关按类型而不是值来评估变量。每个 Switch 至少包含一个 case 用作条件语句

    • 如果没有一个 case 为真,则执行 default

  19. switch case fallthrough default使用场景

    • func main() {
          var a int
          for i := 0; i < 10; i++{
              a = rand.Intn(100)
              switch {
                  case a >= 80:
                  fmt.Println("优秀", a)
                  fallthrough // 强制执行下一个case
                  case a >= 60:
                  fmt.Println("及格", a)
                  fallthrough
                  default:
                  fmt.Println("不及格", a)
              }
          }
      }
      
  20. fmt包中Printf、Sprintf、Fprintf都是格式化输出,有什么不同?

    • 虽然这三个函数都是格式化输出,但是输出的目标不一样

      • Printf输出到控制台
      • Sprintf结果赋值给返回值
      • FprintF输出到指定的io.Writer接口中
    • 例如:

      • func main() {
            var a int = 15
            file, _ := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND, 0644)
            // 格式化字符串并输出到文件
            n, _ := fmt.Fprintf(file, "%T:%v:%p", a, a, &a)
            fmt.Println(n)
        }
        
  21. go语言中的数组和切片的区别是什么?

    • 数组:
      1. 数组固定长度,数组长度是数组类型的一部分,所以[3]int和[4]int是两种不同的数组类型
      2. 数组类型需要指定大小,不指定也会根据初始化,自动推算出大小,大小不可改变,数组是通过值传递的
    • 切片:
      1. 切片的长度可改变,切片是轻量级的数据结构,三个属性:指针、长度、容量
      2. 不要指定切片的大小,切片也是值传递只不过切片的一个属性指针指向的数据不变,所以看起来像引用传递
      3. 切片可以通过数组来初始化也可以通过make函数来初始化初始化时的len和cap相等,然后进行扩容
      4. 切片扩容的时候会导致底层的数组复制,也就是切片中的指针属性会发生变化
      5. 切片也是拷贝,在不发生扩容时,底层使用的是同一个数组,当对其中一个切片append的时候, 该切片长度会增加
        但是不会影响另外一个切片的长度
      6. copy函数将原切片拷贝到目标切片,会导致底层数组复制,因为目标切片需要通过make函数来声明初始化内存,然后
        将原切片指向的数组元素拷贝到新切片指向的数组元素
    • 重点:数组保存真正的数据切片值保存数组的指针和该切片的长度和容量
    • append函数如果切片容量足够的话,只会影响当前切片的长度,数组底层不会复制,不会影响与数组关联的其它切片的长度
    • copy直接会导致数组底层复制。
  22. go语言中值传递和地址传递(引用传递)如何运行?有什么区别?举例说明

    • 值传递会把参数的值复制一份放到对应的函数里,两个变量的地址不同,不可互相修改
    • 地址传递会把参数的地址复制一份放到对应的函数里,两个变量的地址相同,可以互相修改
    • 例如:数组传递就是值传递,而切片传递就是数组的地址传递(本质上切片值传递,只不过是保存的数据地址相同)
  23. go中的参数传递、引用传递

    • go语言中的所有的传参都是值传递(传值),都是一个副本,一个拷贝

    • 因为拷贝的内容有时候是非引用类型(int, string, struct)等,这样在函数中就无法修改原内容数据

    • 有的是引用类型(指针、slice、map、chan),这样就可以修改原内容数据

    • go中的引用类型包含slice、map、chan,它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性

    • 内置函数new计算类型大小,为其分配零值内存,返回指针。

    • 而make会被编译器翻译成具体的创建函数,由其分配内存并初始化成员结构,返回对象而非指针

  24. go中数组和切片在传递时有什么区别?

    • 数组是值传递
    • 切片地址传递(引用传递)
  25. go中slice的底层实现

    • 切片是基于数组实现的,它的底层是数组,它本身非常小,它可以理解为对底层数组的抽闲
    • 因为基于数组实现,所以它的底层内存是连续分配的,效率非常高,还可以通过索引获取数据
    • 切片本身并不是动态数组或数组指针,它内部实现的数据结构体通过指针引用底层数组
    • 设定相关属性将读写操作限定在指定的区域内,切片本身是一个只读对象,其工作机制类似于数组指针的一种封装
    • 切片对象非常小,因为它只有三个字段的数据结构:指向底层数组的指针、切片的长度、切片的容量
  26. go中slice的扩容机制,有什么注意点?

    • 首先判断,如果新申请的容量大于2倍的旧容量,最终容量就是新申请的容量
    • 否则判断,如果旧切片的长度小于1024,最终容量就是旧容量的两倍
    • 否则判断,如果旧切片的长度大于等于1024,则最终容量从旧容量开始循环增加原来的1/4,直到最终容量大于新申请的容量
    • 如果最终容量计算值溢出,则最终容量就是新申请的容量
  27. go中是如何实现切片扩容的?[答案有误,需要重新确定]

    • 当容量小于1024时,每次扩容容量翻倍,当容量大于1024时,每次扩容加25%.

    • func main() {
          s1 := make([]int, 0)
          for i := 0; i < 3000; i++{
              fmt.Println("len =", len(s1), "cap = ", cap(s1))
              s1 = append(s1, i)
          }
      }
      
  28. 扩容前后的slice是否相同?

    • 情况一:
      1. 原来数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容之后的切片还是指向原来的数组
      2. 对一个切片的操作可能影响多个指针指向相同地址的切片
    • 情况二:
      1. 原来数组的容量已经达到了最大值,在扩容,go默认会先开辟一块内存区域,把原来的值拷贝过来
      2. 然后再执行append操作,这种情况丝毫不影响原数组
    • 注意:要复制一个slice最好使用copy函数
  29. 如何判断 2 个字符串切片(slice) 是相等的?

    • go 语言中可以使用反射 reflect.DeepEqual(a, b) 判断 a、b 两个切片是否相等,但是通常不推荐这么做,使用反射非常影响性能。

    • 通常采用的方式如下,遍历比较切片中的每一个元素(注意处理越界的情况)。

      1. func StringSliceEqualBCE(a, b []string) bool {
            if len(a) != len(b) {
                return false
            }
        
            if (a == nil) != (b == nil) {
                return false
            }
        
            b = b[:len(a)]
            for i, v := range a {
                if v != b[i] {
                    return false
                }
            }
        
            return true
        }
        
  30. 看下面代码defer的执行顺序是什么?defer的作用和特点是什么?

    • 在普通函数或方法前加上defer关键字,就完成了defer所需要的语法,当defer语句被执行时,跟在defer语句后的函数会被延迟执行

    • 知道包含该defer语句的函数执行完毕,defer语句后的函数才会执行,无论包含defer语句的函数是通过return正常结束,还是通过panic导致的异常结束

    • 可以在一个函数中执行多条defer语句,由于在栈中存储,所以它的执行顺序和声明顺序相反

    • 多个 defer 语句,遵从后进先出(Last In First Out,LIFO)的原则,最后声明的 defer 语句,最先得到执行。defer 在 return 语句之后执行,但在函数退出之前,defer 可以修改返回值。

    • 例子:

    • func test() int {
      	i := 0
      	defer func() {
      		fmt.Println("defer1")
      	}()
      	defer func() {
      		i += 1
      		fmt.Println("defer2")
      	}()
      	return i
      }
      
      func main() {
      	fmt.Println("return", test())
      }
      // defer2
      // defer1
      // return 0
      
    • 这个例子中,可以看到 defer 的执行顺序:后进先出。但是返回值并没有被修改,这是由于 Go 的返回机制决定的,执行 return 语句后,Go 会创建一个临时变量保存返回值,因此,defer 语句修改了局部变量 i,并没有修改返回值。那如果是有名的返回值呢?

    • func test() (i int) {
      	i = 0
      	defer func() {
      		i += 1
      		fmt.Println("defer2")
      	}()
      	return i
      }
      
      func main() {
      	fmt.Println("return", test())
      }
      // defer2
      // return 1
      
    • 这个例子中,返回值被修改了。对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响。

  31. defer的常用场景

    • defer语句经常被用于处理成对的操作打开/关闭,链接/断开连接,加锁/释放锁
    • 通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放
    • 释放资源的defer语句应该直接跟在请求资源处理错误之后
    • 注意:defer一定要放在请求资源处理错误之后
  32. defer语句中通过recover捕获panic例子

    • 注意要在defer后函数里的recover()

    • func main() {
          defer func() {
              err := recover()
              fmt.Println(err)
          }()
          defer fmt.Println("first defer")
          defer fmt.Println("second defer")
          defer fmt.Println("third defer")
          fmt.Println("哈哈哈哈")
          panic("abc is an error")
      }
      
  33. 哈希概念讲解

    • 哈希表又称为散列表,由一个直接寻址表和一个哈希函数组成

    • 由于哈希表的大小是有限的而要存储的数值是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到相同位置的情况,这种情况叫做哈希冲突

    • 通过拉链法解决哈希冲突

      • 哈希表每个位置都连接一个链表,当冲突发生是,冲突的元素将会被加到该位置链表的最后
    • 哈希表的查找速度起决定性作用的就是哈希函数: 除法哈希发、乘法哈希法、全域哈希法

    • 哈希表的应用?

      • 字典与集合都是通过哈希表来实现的
      • md5曾经是密码学中常用的哈希函数,可以把任意长度的数据映射为128位的哈希值
  34. go中的map底层实现

    • go中map的底层实现就是一个散列表,因此实现map的过程实际上就是实现散列表的过程
    • 在这个散列表中,主要出现的结构体由两个,一个是hmap、一个是bmap
    • go中也有一个哈希函数,用来对map中的键生成哈希值
    • hash结果的低位用于把k/v放到bmap数组中的哪个bmap中
    • 高位用于key的快速预览,快速试错
  35. go中的map如何扩容

    • 翻倍扩容:如果map中的键值对个数/桶的个数>6.5,就会引发翻倍扩容
    • 等量扩容:当B<=15时,如果溢出桶的个数>=2的B次方就会引发等量扩容
    • 当B>15时,如果溢出桶的个数>=2的15次方时就会引发等量扩容
  36. go中map的查找

    • go中的map采用的是哈希查找表,由哈希函数通过key和哈希因此计算出哈希值,
    • 根据hamp中的B来确定放到哪个桶中,如果B=5,那么就根据哈希值的后5位确定放到哪个桶中
    • 在用哈希值的高8位确定桶中的位置,如果当前的bmap中未找到,则去对应的overflow bucket中查找
    • 如果当前map处于数据搬迁状态,则优先从oldbuckets中查找
  37. 如何判断 map 中是否包含某个 key ?

    • if val, ok := dict["foo"]; ok {
          //do something here
      }
      
    • dict["foo"] 有 2 个返回值,val 和 ok,如果 ok 等于 true,则说明 dict 包含 key "foo",val 将被赋予 "foo" 对应的值。

2. 代码输出

2.1 常量与变量

  1. 下面代码的输出是:

    func main() {
    	const (
    		a, b = "golang", 100
    		d, e
    		f bool = true
    		g
    	)
    	fmt.Println(d, e, g)
    }
    

    答案:

    golang 100 true
    

    在同一个 const group 中,如果常量定义与前一行的定义一致,则可以省略类型和值。编译时,会按照前一行的定义自动补全。即等价于

    gofunc main() {	
        const (		
            a, b = "golang", 100	
            d, e = "golang", 100	
            f bool = true	
            g bool = true	
        )
        fmt.Println(d, e, g)
    }
    
  2. 下面代码输出是:

    func main() {
    	const N = 100
    	var x int = N
    
    	const M int32 = 100
    	var y int = M
    	fmt.Println(x, y)
    }
    

    答案:

    编译失败:cannot use M (type int32) as type int in assignment
    Go 语言中,常量分为无类型常量和有类型常量两种,const N = 100,属于无类型常量,赋值给其他变量时,如果字面量能够转换为对应类型的变量,则赋值成功,例如,var x int = N。但是对于有类型的常量 const M int32 = 100,赋值给其他变量时,需要类型匹配才能成功,所以显示地类型转换:
    var y int = int(M)
    
  3. 下面代码的输出是:

    func main() {
    	var a int8 = -1
    	var b int8 = -128 / a
    	fmt.Println(b)
    }
    

    答案:

    -128
    int8 能表示的数字的范围是 [-2^7, 2^7-1],即 [-128, 127]。-128 是无类型常量,转换为 int8,再除以变量 -1,结果为 128,常量除以变量,结果是一个变量。变量转换时允许溢出,符号位变为1,转为补码后恰好等于 -128。
    对于有符号整型,最高位是是符号位,计算机用补码表示负数。补码 = 原码取反加一。
    例如:-1 :  11111111
    00000001(原码)    11111110(取反)    11111111(加一)
    -128:    
    10000000(原码)    01111111(取反)    10000000(加一)
    -1 + 1 = 011111111 + 00000001 = 00000000(最高位溢出省略)
    -128 + 127 = -110000000 + 01111111 = 11111111
    
  4. 下面代码输出是:

    func main() {
    	const a int8 = -1
    	var b int8 = -128 / a
    	fmt.Println(b)
    }
    

    答案:

    编译失败:constant 128 overflows int8
    -128 和 a 都是常量,在编译时求值,-128 / a = 128,两个常量相除,结果也是一个常量,常量类型转换时不允许溢出,因而编译失败。
    

2.2 作用域

  1. 下列代码输出是:

    func main() {
    	var err error
    	if err == nil {
    		err := fmt.Errorf("err")
    		fmt.Println(1, err)
    	}
    	if err != nil {
    		fmt.Println(2, err)
    	}
    }
    

    答案:

    1 err
    := 表示声明并赋值,= 表示仅赋值。
    变量的作用域是大括号,因此在第一个 if 语句 if err == nil 内部重新声明且赋值了与外部变量同名的局部变量 err。对该局部变量的赋值不会影响到外部的 err。因此第二个 if 语句 if err != nil 不成立。所以只打印了 1 err。
    

2.3 defer 延迟调用

  1. 下列代码输出:

    type T struct{}
    
    func (t T) f(n int) T {
    	fmt.Print(n)
    	return t
    }
    
    func main() {
    	var t T
    	defer t.f(1).f(2)
    	fmt.Print(3)
    }
    

    答案:

    132
    defer 延迟调用时,需要保存函数指针和参数,因此链式调用的情况下,除了最后一个函数/方法外的函数/方法都会在调用时直接执行。也就是说 t.f(1) 直接执行,然后执行 fmt.Print(3),最后函数返回时再执行 .f(2),因此输出是 132。
    
  2. func f(n int) {
    	defer fmt.Println(n)
    	n += 100
    }
    
    func main() {
    	f(1)
    }
    

    答案:

    1
    打印 1 而不是 101。defer 语句执行时,会将需要延迟调用的函数和参数保存起来,也就是说,执行到 defer 时,参数 n(此时等于1) 已经被保存了。因此后面对 n 的改动并不会影响延迟函数调用的结果。
    
  3. func main() {
    	n := 1
    	defer func() {
    		fmt.Println(n)
    	}()
    	n += 100
    }
    

    答案:

    101
    匿名函数没有通过传参的方式将 n 传入,因此匿名函数内的 n 和函数外部的 n 是同一个,延迟执行时,已经被改变为 101。
    
  4. func main() {
    	n := 1
    	if n == 1 {
    		defer fmt.Println(n)
    		n += 100
    	}
    	fmt.Println(n)
    }
    

    答案:

    101
    1
    先打印 101,再打印 1。defer 的作用域是函数,而不是代码块,因此 if 语句退出时,defer 不会执行,而是等 101 打印后,整个函数返回时,才会执行。
    

3. 中级语法

  1. go两个接口之间可以存在什么关系?

    • 如果两个接口有相同的方法列表,那么他俩就是等价的,可以相互赋值
    • 接口A可以嵌套到接口B里面,那么接口B就有了自己的方法列表+接口A的方法列表
  2. 什么是 goroutine,你如何停止它?

    • goroutine是协程/轻量级线程/用户态线程,不同于传统的内核态线程

    • 占用资源特别少,创建和销毁只在用户态执行不会到内核态,节省时间

    • 创建goroutine需要使用go关键字

    • 可以向goroutine发送一个信号通道来停止它,goroutine内部需要检查信号通道

    • 例子:

    • func main() {
          var wg sync.WaitGroup // 等待组进行多个任务的同步,可以保证并发环境中完成指定数量的任务,每个sync.WaitGroup值在内部维护着一个计数,此计数的初始默认值为0
          var exit = make(chan bool)
          wg.Add(1) // 等待组的计数器+1
          go func() {
              for {
                  select {
                  case <-exit:  // 接收到信号后return退出当前goroutine
                      fmt.Println("goroutine接收到信号退出了!")
                      wg.Done() // 等待组的计数器-1
                      return
                  default:
                      fmt.Println("还没有接收到信号")
                  }
              }
          }()
          exit <- true
          wg.Wait() // 当等待组计数器不等于0时阻塞,直到变为0
      }
      
  3. go中同步锁(也叫互斥锁)有什么特点,作用是什么?何时使用互斥锁,何时使用读写锁?

    • 当一个goroutine获得了Mutex(互斥锁)后,其它goroutine就只能乖乖等待,除非该goroutine释放Mutex

    • RWMutext(读写互斥锁)在读锁占用的情况下会阻止写,但不会阻止读,在写锁占用的情况下,会阻止任何其它goroutine进来

    • 无论是读还是写,整个锁相当于由该goroutine独占

    • 作用:保证资源在使用时的独有性,不会因为并发导致数据错乱,保证系统稳定性

    • 案例:

      • package main
        import (
            "fmt"
            "sync"
            "time"
        )
        var (
            num = 0
            lock = sync.RWMutex{}  // 耗时:100+毫秒
            //lock = sync.Mutex{}  // 耗时:50+毫秒
        )
        func main() {
            start := time.Now()
            go func() {
                for i := 0; i < 100000; i++{
                    lock.Lock()
                    //fmt.Println(num)
                    num++
                    lock.Unlock()
                }
            }()
            for i := 0; i < 100000; i++{
                lock.Lock()
                //fmt.Println(num)
                num++
                lock.Unlock()
            }
            fmt.Println(num)
            fmt.Println(time.Now().Sub(start))
        }
        
    • 总结:

      • 如果对数据写的比较多,使用Mutex同步锁/互斥锁性能更高
      • 如果对数据读的比较多,使用RWMutex读写锁性能更高
  4. goroutine案例(两个goroutine,一个负责输出数字,另一个负责输出26个英文字母,格式如下:12ab34cd56ef78gh ... yz)

    • package main
      import (
      	"fmt"
      	"sync"
      	"unicode/utf8"
      )
      // 案例:两个goroutine,一个负责输出数字,另一个负责输出26个英文字母,格式如下:12ab34cd56ef78gh ... yz
      var (
      	wg = sync.WaitGroup{}  // 和第五题很相关。申明等待组
      	chNum = make(chan bool)
      	chAlpha = make(chan bool)
      )
      func main() {
      	go func() {
      		i := 1
      		for {
      			<-chNum // 接到信号,运行该goroutine
      			fmt.Printf("%v%v", i, i + 1)
      			i += 2
      			chAlpha <- true // 发送信号
      		}
      	}()
      	wg.Add(1) // 等待组的计数器+1
      	go func() {
      		str := "abcdefghigklmnopqrstuvwxyz"
      		i := 0
      		for {
      			<-chAlpha // 接到信号,运行该goroutine
      			fmt.Printf("%v", str[i:i+2])
      			i += 2
      			if i >= utf8.RuneCountInString(str){
      				wg.Done() // 等待组的计数器-1
      				return
      			}
      			chNum <- true // 发送信号
      		}
      	}()
      	chNum <- true // 发送信号
      	wg.Wait() // 等待组的计数器不为0时,阻塞main进程,直到等待组的计数器为0
      }
      
  5. 介绍一下channel

    • go中不要通过共享内存来通信,而要通过通信实现共享内存
    • go中的csp并发模型,中文名通信顺序进程,就是通过goroutine和channel实现的
    • channel收发遵循先进先出,分为有缓冲通道(异步通道),无缓冲通道(同步通道)
  6. go中channel的特性

    • 给一个nil的channel发送数据,会造成永久阻塞
    • 从一个nil的channel接收数据,会造成永久阻塞
    • 给一个已经关闭的channel发送数据,会造成panic
    • 从一个已经关闭的channel接收数据,如果缓冲区为空,会返回零值
    • 无缓冲的channel是同步的,有缓冲的channel是异步的
    • 关闭一个nil channel会造成panic
  7. channel中ring buffer的实现

    1. channel中使用了ring buffer(环形缓冲区)来缓存写入数据,
    2. ring buffer有很多好处,而且非常适合实现FiFo的固定长度队列
    3. channel中包含buffer、sendx、recvx
    4. recvx指向最早被读取的位置,sendx指向再次写入时插入的位置
  8. go语言中,channel通道有什么特点,需要注意什么?

    • 总结:

      • 给一个nil channel发送数据时会一直堵塞
      • 从一个nil channel接收数据时会一直阻塞
      • 给一个已关闭的channel发送数据时会panic
      • 从一个已关闭的channel中读取数据时,如果channel为空,则返回通道中类型的零值
    • 案例:

      • package main
        import (
        	"fmt"
        	"sync"
        )
        func main() {
        	var wg sync.WaitGroup // 等待组
        	var ch chan int // nil channel
        	var ch1 = make(chan int) // 创建channel
        	fmt.Println(ch, ch1)  // <nil> 0xc000086060
        	wg.Add(1) // 等待组的计数器+1
        	go func() {
        		//ch <- 15  // 如果给一个nil的channel发送数据会造成永久阻塞
        		//<-ch  // 如果从一个nil的channel中接收数据也会造成永久阻塞
        		ret := <-ch1
        		fmt.Println(ret)
        		ret = <-ch1  // 从一个已关闭的通道中接收数据,如果缓冲区中为空,则返回该类型的零值
        		fmt.Println(ret)
        		wg.Done() // 等待组的计数器-1
        	}()
        	go func() {
        		//close(ch1)
        		ch1 <- 15  // 给一个已关闭通道发送数据就会包panic错误
        		close(ch1)
        	}()
        	wg.Wait() // 等待组的计数器不为0时阻塞
        }
        
  9. go中channel缓冲有什么特点?

    • 无缓冲的通道是同步的,有缓冲的通道是异步的
  10. 写一个定时任务,每秒执行一次

    • func main() {
          t1 := time.NewTicker(time.Second * 1) // 创建一个周期定时器
          var i = 1
          for {
              if i == 10{
                  break
              }
              select {
                  case <-t1.C:  // 一秒执行一次的定时任务
                  task1(i)
                  i++
              }
          }
      }
      func task1(i int) {
          fmt.Println("task1执行了---", i)
      }
      

4. 基础应用

  1. 如何关闭 HTTP 的响应体的?

    • 直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体;

    • 手动调用 defer 来关闭响应体。

    • 正确示例:

      • func main() {
            resp, err := http.Get("http://www.baidu.com") // 发出请求并返回请求结果
        
            // 关闭 resp.Body 的正确姿势
            if resp != nil {
              	defer resp.Body.Close()
            }
        
            checkError(err) // 检查错误,省略写法
            defer resp.Body.Close()	// 手动调用defer来关闭响应体
        
            body, err := ioutil.ReadAll(resp.Body) // 一次性读写文件的全部数据
            checkError(err)
        
            fmt.Println(string(body))
        }
        
  2. 是否主动关闭过http连接,为啥要这样做?

    • 有关闭,不关闭会程序可能会消耗完 socket 描述符。有如下2种关闭方式:

      • 直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。

      • 设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接

      • // 主动关闭连接
        func main() {
            req, err := http.NewRequest("GET", "http://golang.org", nil)
            checkError(err)
        
            req.Close = true // 直接设置请求变量的Close字段值为true,每次请求结束后主动关闭连接
            //req.Header.Add("Connection", "close") // 等效的关闭方式
        
            resp, err := http.DefaultClient.Do(req)
            if resp != nil {
                defer resp.Body.Close()
            }
            checkError(err)
        
            body, err := ioutil.ReadAll(resp.Body)
            checkError(err)
        
            fmt.Println(string(body))
        }
        
    • 你可以创建一个自定义配置的 HTTP transport(传输) 客户端,用来取消 HTTP 全局的复用连接

      • func main() {
            tr := http.Transport{DisableKeepAlives: true} // 自定义配置传输客户端,用来取消HTTP全部的复用连接。
            client := http.Client{Transport: &tr}
        
            resp, err := client.Get("https://golang.google.cn/")
            if resp != nil {
                defer resp.Body.Close()
            }
            checkError(err)
        
            fmt.Println(resp.StatusCode) // 200
        
            body, err := ioutil.ReadAll(resp.Body)
            checkError(err)
        
            fmt.Println(len(string(body)))
        }
        
  3. 解析 JSON 数据时,默认将数值当做哪种类型?

    • 在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理

    • func main() {
          var data = []byte(`{"status": 200}`)
          var result map[string]interface{}
      
          if err := json.Unmarshal(data, &result); err != nil {
            	log.Fatalln(err)
          }
      }
      

      解析出来的 200 是 float 类型。

  4. JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?

    • 首先 JSON 标准库对 nil slice 和 空 slice 的处理是不一致。

    • 通常错误的用法,会报数组越界的错误,因为只是声明了slice,却没有给实例化的对象。

      var slice []int // nil slice
      slice[1] = 0
      

      此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。

      empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下:

      slice := make([]int,0)// 空slice,没有值,空间也是空的
      slice := []int{}
      

      当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。总之,nil slice 和 empty slice是不同的东西,需要我们加以区分的。

5. 扩展了解

  1. go convey是什么,一般用来做什么?

    • go convey是一个支持golang的单元测试框架
    • 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面
    • 提供了丰富的断言简化测试用例的编写
  2. 说说go语言的beego框架

    • beego 是一个 golang 实现的轻量级HTTP框架
    • beego 可以通过注释路由、正则路由等多种方式完成 url 路由注入
    • 可以使用 bee new 工具生成空工程,然后使用 bee run 命令自动热编译
  3. GoStub的作用是什么?

    • GoStub也是一种测试框架:

      • GoStub 可以对全局变量打桩

      • GoStub 可以对函数打桩

      • GoStub 不可以对类的成员方法打桩

      • GoStub 可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为

6.参考

  1. Go 语言笔试面试题汇总 | 极客面试 | 极客兔兔 (geektutu.com)
  2. 极客时间-轻松学习,高效学习-极客邦 (geekbang.org)

整理不易,给个赞吧!~

posted @ 2023-11-20 21:59  轻荇  阅读(33)  评论(0编辑  收藏  举报