Go基础面经大全(持续补充中)
Go基础
1. 基础特性
-
Go的优势
-
天生支持并发,性能高。
-
单一的标准代码格式,比其他语言更具可读性。
-
自动垃圾收集机制比Java和Python更有效,因为它与程序同时执行。
-
-
Go数据类型
- int, string, float, bool, array, slice, map, channel, pointer, struct, interface, method
-
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
- 程序声明2个:
-
Go程序中的包是什么?
- 项目中包含go源文件以及其它包的目录,源文件中的函数、变量、类型都存储在该包中
- 每个源文件都属于一个包,该包在文件顶部使用
package packageName
声明 - 当我们在源文件中引用第三包时,需要还用
import packageName
-
Go支持什么形式的类型转换?如何实现整数转为浮点数
-
go支持显示类型转换,即严格强制类型转换
-
a := 15 b := float64(a) fmt.Println(b, reflect.TypeOf(b))
-
2. 初级语法
-
=
和:=
的区别?-
:= 声明+赋值= 仅赋值
-
var foo int foo = 10 // 等价于 foo := 10
-
-
指针的作用?
-
指针用来保存变量的地址。
-
例如:
-
var x = 5 var p *int = &x fmt.Printf("x = %d", *p) // x 可以用 *p 访问*
-
*运算符,也称为解引用运算符,用于访问地址中的值。
-
&运算符,也称为地址运算符,用于返回变量的地址。
-
-
Go 允许多个返回值吗?
-
允许
-
func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("A", "B") fmt.Println(a, b) // B A }
-
-
Go 有异常类型吗?
-
Go 没有异常类型,只有错误类型(Error),通常使用返回值来表示异常状态。
-
f, err := os.Open("test.txt") if err != nil { log.Fatal(err) }
-
-
什么是协程(Goroutine)?
- Goroutine 是与其他函数或方法同时运行的函数或方法。 Goroutines 可以被认为是轻量级的线程。 与线程相比,创建 Goroutine 的开销很小。 Go应用程序同时运行数千个 Goroutine 是非常常见的做法。
-
如何高效地拼接字符串?
-
Go 语言中,字符串是只读的,也就意味着每次修改操作都会创建一个新的字符串。如果需要拼接多次,应使用 strings.Builder,最小化内存拷贝次数。
-
var str strings.Builder for i := 0; i < 1000; i++ { str.WriteString("a") } fmt.Println(str.String())
-
-
什么是 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
-
-
Go 支持默认参数或可选参数吗?
- Go 语言不支持可选参数(python 支持),也不支持方法重载(java支持)。
-
如何交换 2 个变量的值?
-
a, b := "A", "B" a, b = b, a fmt.Println(a, b) // B A
-
-
Go 语言 tag 的用处?
-
tag 可以理解为 struct 字段的注解,可以用来定义字段的一个或多个属性。框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。tag 丰富了代码的语义,增强了灵活性。
-
例如:
-
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) }
-
这个例子使用 tag 定义了结构体字段与 json 字段的转换关系,Name -> stu_name, ID -> stu_id,忽略 Age 字段。很方便地实现了 Go 结构体与不同规范的 json 文本之间的转换。
-
-
字符串打印时,
%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() 覆盖默认值。
-
-
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
-
-
空 struct{} 的用途?
-
使用空结构体 struct{} 可以节省内存,一般作为占位符使用,表明这里并不需要一个值。
-
fmt.Println(unsafe.Sizeof(struct{}{})) // 0
-
-
比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。
-
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{} 代替。
-
func main() { ch := make(chan struct{}, 1) go func() { <-ch // do something }() ch <- struct{}{} // ... }
-
-
再比如,声明只包含方法的结构体。
-
type Lamp struct{} func (l Lamp) On() { println("On") } func (l Lamp) Off() { println("Off") }
-
-
-
go中的cap函数可以作用于哪些内容?
-
可作用于的类型有:
- 数组(array)
- 切片(slice)
- 通道(channel)
-
查看他们的容量大小,而不是装的数据大小
-
-
go语言中new的作用是什么?
- 使用new函数来分配内存空间
- 传递给new函数的是一个类型,而不是一个值
- 返回值是指向这个新分配的地址的指针
-
go语言中的make作用是什么?
- 分配内存空间并进行初始化, 返回值是该类型的实例而不是指针
- make只能接收三种类型当做参数:slice、map、channel
-
总结make和new的区别?
- new可以接收任意内置类型当做参数,返回的是对应类型的指针
- make只能接收slice、map、channel当做参数,返回值是对应类型的实例
-
如何在运行时检查变量类型?
- 类型开关(
Type Switch
)是在运行时检查变量类型的最佳方式。
-
类型开关按类型而不是值来评估变量。每个
Switch
至少包含一个case
用作条件语句 -
如果没有一个
case
为真,则执行default
。
- 类型开关(
-
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) } } }
-
-
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) }
-
-
-
go语言中的数组和切片的区别是什么?
- 数组:
- 数组固定长度,数组长度是数组类型的一部分,所以[3]int和[4]int是两种不同的数组类型
- 数组类型需要指定大小,不指定也会根据初始化,自动推算出大小,大小不可改变,数组是通过值传递的
- 切片:
- 切片的长度可改变,切片是轻量级的数据结构,三个属性:指针、长度、容量
- 不要指定切片的大小,切片也是值传递只不过切片的一个属性指针指向的数据不变,所以看起来像引用传递
- 切片可以通过数组来初始化也可以通过make函数来初始化,初始化时的len和cap相等,然后进行扩容
- 切片扩容的时候会导致底层的数组复制,也就是切片中的指针属性会发生变化
- 切片也是拷贝,在不发生扩容时,底层使用的是同一个数组,当对其中一个切片append的时候, 该切片长度会增加
但是不会影响另外一个切片的长度 - copy函数将原切片拷贝到目标切片,会导致底层数组复制,因为目标切片需要通过make函数来声明初始化内存,然后
将原切片指向的数组元素拷贝到新切片指向的数组元素
- 重点:数组保存真正的数据,切片值保存数组的指针和该切片的长度和容量
- append函数如果切片容量足够的话,只会影响当前切片的长度,数组底层不会复制,不会影响与数组关联的其它切片的长度
- copy直接会导致数组底层复制。
- 数组:
-
go语言中值传递和地址传递(引用传递)如何运行?有什么区别?举例说明
- 值传递会把参数的值复制一份放到对应的函数里,两个变量的地址不同,不可互相修改
- 地址传递会把参数的地址复制一份放到对应的函数里,两个变量的地址相同,可以互相修改
- 例如:数组传递就是值传递,而切片传递就是数组的地址传递(本质上切片值传递,只不过是保存的数据地址相同)
-
go中的参数传递、引用传递
-
go语言中的所有的传参都是值传递(传值),都是一个副本,一个拷贝,
-
因为拷贝的内容有时候是非引用类型(int, string, struct)等,这样在函数中就无法修改原内容数据
-
有的是引用类型(指针、slice、map、chan),这样就可以修改原内容数据
-
go中的引用类型包含slice、map、chan,它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性
-
内置函数new计算类型大小,为其分配零值内存,返回指针。
-
而make会被编译器翻译成具体的创建函数,由其分配内存并初始化成员结构,返回对象而非指针
-
-
go中数组和切片在传递时有什么区别?
- 数组是值传递
- 切片地址传递(引用传递)
-
go中slice的底层实现
- 切片是基于数组实现的,它的底层是数组,它本身非常小,它可以理解为对底层数组的抽闲
- 因为基于数组实现,所以它的底层内存是连续分配的,效率非常高,还可以通过索引获取数据
- 切片本身并不是动态数组或数组指针,它内部实现的数据结构体通过指针引用底层数组
- 设定相关属性将读写操作限定在指定的区域内,切片本身是一个只读对象,其工作机制类似于数组指针的一种封装
- 切片对象非常小,因为它只有三个字段的数据结构:指向底层数组的指针、切片的长度、切片的容量
-
go中slice的扩容机制,有什么注意点?
- 首先判断,如果新申请的容量大于2倍的旧容量,最终容量就是新申请的容量
- 否则判断,如果旧切片的长度小于1024,最终容量就是旧容量的两倍
- 否则判断,如果旧切片的长度大于等于1024,则最终容量从旧容量开始循环增加原来的1/4,直到最终容量大于新申请的容量
- 如果最终容量计算值溢出,则最终容量就是新申请的容量
-
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) } }
-
-
扩容前后的slice是否相同?
- 情况一:
- 原来数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容之后的切片还是指向原来的数组
- 对一个切片的操作可能影响多个指针指向相同地址的切片
- 情况二:
- 原来数组的容量已经达到了最大值,在扩容,go默认会先开辟一块内存区域,把原来的值拷贝过来
- 然后再执行append操作,这种情况丝毫不影响原数组
- 注意:要复制一个slice最好使用copy函数
- 情况一:
-
如何判断 2 个字符串切片(slice) 是相等的?
-
go 语言中可以使用反射 reflect.DeepEqual(a, b) 判断 a、b 两个切片是否相等,但是通常不推荐这么做,使用反射非常影响性能。
-
通常采用的方式如下,遍历比较切片中的每一个元素(注意处理越界的情况)。
-
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 }
-
-
-
看下面代码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,即对返回值产生了影响。
-
-
defer的常用场景
- defer语句经常被用于处理成对的操作打开/关闭,链接/断开连接,加锁/释放锁
- 通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放
- 释放资源的defer语句应该直接跟在请求资源处理错误之后
- 注意:defer一定要放在请求资源处理错误之后
-
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") }
-
-
哈希概念讲解
-
哈希表又称为散列表,由一个直接寻址表和一个哈希函数组成
-
由于哈希表的大小是有限的而要存储的数值是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到相同位置的情况,这种情况叫做哈希冲突
-
通过拉链法解决哈希冲突:
- 哈希表每个位置都连接一个链表,当冲突发生是,冲突的元素将会被加到该位置链表的最后
-
哈希表的查找速度起决定性作用的就是哈希函数: 除法哈希发、乘法哈希法、全域哈希法
-
哈希表的应用?
- 字典与集合都是通过哈希表来实现的
- md5曾经是密码学中常用的哈希函数,可以把任意长度的数据映射为128位的哈希值
-
-
go中的map底层实现
- go中map的底层实现就是一个散列表,因此实现map的过程实际上就是实现散列表的过程
- 在这个散列表中,主要出现的结构体由两个,一个是hmap、一个是bmap
- go中也有一个哈希函数,用来对map中的键生成哈希值
- hash结果的低位用于把k/v放到bmap数组中的哪个bmap中
- 高位用于key的快速预览,快速试错
-
go中的map如何扩容
- 翻倍扩容:如果map中的键值对个数/桶的个数>6.5,就会引发翻倍扩容
- 等量扩容:当B<=15时,如果溢出桶的个数>=2的B次方就会引发等量扩容
- 当B>15时,如果溢出桶的个数>=2的15次方时就会引发等量扩容
-
go中map的查找
- go中的map采用的是哈希查找表,由哈希函数通过key和哈希因此计算出哈希值,
- 根据hamp中的B来确定放到哪个桶中,如果B=5,那么就根据哈希值的后5位确定放到哪个桶中
- 在用哈希值的高8位确定桶中的位置,如果当前的bmap中未找到,则去对应的overflow bucket中查找
- 如果当前map处于数据搬迁状态,则优先从oldbuckets中查找
-
如何判断 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 常量与变量
-
下面代码的输出是:
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) }
-
下面代码输出是:
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)
-
下面代码的输出是:
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
-
下面代码输出是:
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 作用域
-
下列代码输出是:
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 延迟调用
-
下列代码输出:
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。
-
func f(n int) { defer fmt.Println(n) n += 100 } func main() { f(1) }
答案:
1 打印 1 而不是 101。defer 语句执行时,会将需要延迟调用的函数和参数保存起来,也就是说,执行到 defer 时,参数 n(此时等于1) 已经被保存了。因此后面对 n 的改动并不会影响延迟函数调用的结果。
-
func main() { n := 1 defer func() { fmt.Println(n) }() n += 100 }
答案:
101 匿名函数没有通过传参的方式将 n 传入,因此匿名函数内的 n 和函数外部的 n 是同一个,延迟执行时,已经被改变为 101。
-
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. 中级语法
-
go两个接口之间可以存在什么关系?
- 如果两个接口有相同的方法列表,那么他俩就是等价的,可以相互赋值
- 接口A可以嵌套到接口B里面,那么接口B就有了自己的方法列表+接口A的方法列表
-
什么是 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 }
-
-
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读写锁性能更高
-
-
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 }
-
-
介绍一下channel
- go中不要通过共享内存来通信,而要通过通信实现共享内存
- go中的csp并发模型,中文名通信顺序进程,就是通过goroutine和channel实现的
- channel收发遵循先进先出,分为有缓冲通道(异步通道),无缓冲通道(同步通道)
-
go中channel的特性
- 给一个nil的channel发送数据,会造成永久阻塞
- 从一个nil的channel接收数据,会造成永久阻塞
- 给一个已经关闭的channel发送数据,会造成panic
- 从一个已经关闭的channel接收数据,如果缓冲区为空,会返回零值
- 无缓冲的channel是同步的,有缓冲的channel是异步的
- 关闭一个nil channel会造成panic
-
channel中ring buffer的实现
- channel中使用了ring buffer(环形缓冲区)来缓存写入数据,
- ring buffer有很多好处,而且非常适合实现FiFo的固定长度队列
- channel中包含buffer、sendx、recvx
- recvx指向最早被读取的位置,sendx指向再次写入时插入的位置
-
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时阻塞 }
-
-
-
go中channel缓冲有什么特点?
- 无缓冲的通道是同步的,有缓冲的通道是异步的
-
写一个定时任务,每秒执行一次
-
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. 基础应用
-
如何关闭 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)) }
-
-
-
是否主动关闭过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))) }
-
-
-
解析 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 类型。
-
-
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. 扩展了解
-
go convey是什么,一般用来做什么?
- go convey是一个支持golang的单元测试框架
- 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面
- 提供了丰富的断言简化测试用例的编写
-
说说go语言的beego框架
- beego 是一个 golang 实现的轻量级HTTP框架
- beego 可以通过注释路由、正则路由等多种方式完成 url 路由注入
- 可以使用 bee new 工具生成空工程,然后使用 bee run 命令自动热编译
-
GoStub的作用是什么?
-
GoStub也是一种测试框架:
-
GoStub 可以对全局变量打桩
-
GoStub 可以对函数打桩
-
GoStub 不可以对类的成员方法打桩
-
GoStub 可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为
-
-
6.参考
- Go 语言笔试面试题汇总 | 极客面试 | 极客兔兔 (geektutu.com)
- 极客时间-轻松学习,高效学习-极客邦 (geekbang.org)
整理不易,给个赞吧!~~~