原站地址:Go语言核心36讲_Golang_Go语言-极客时间
一、接口类型的合理运用
1. 接口类型只包含方法,不包含字段。 方法集合就是它的全部特征。
任何数据类型,只要实现了接口的方法集合全部,那么它就是这个接口的实现类型
2. 怎么判定该数据类型的方法,是实现了接口的方法?
签名一致(参数和返回), 函数名一致。
3. 数据类型的指针类型实现了一个接口所有的办法,但不代表它的值类型实现了这个接口。
两者的方法集合是不等价的,指针类型的方法集合 包含了值类型的所有方法集合,但反过来就不是了。
4. 什么是 静态类型和动态类型,动态值 ?
比如 *Dog类型是 Pet 接口的实现类型,那么:
dog := Dog{"little pig"}
var pet Pet = &dog
Pet 是静态类型, *Dog 是就是动态类型; 赋给pet的值叫做动态值 (或者实际值)
5. 接口变量(实现接口的变量) 的赋值操作之后,也是以副本的方式进行赋值。
6. 接口变量被赋予动态值的时候,存储的是包含了这个动态值的副本的一个结构更加复杂的值。
它包含两个指针,一个是指向类型信息的指针,另一个是指向动态值的指针。
7. 用 值为nil的接口变量 给 其他接口变量 赋值时,结果仍然是带类型的nil。 做 == nil 判断时,结果是false 。比如:
var dog1 *Dog
dog2 := dog1
var pet Pet = dog2
这里 pet 的值就是带类型的nil (Go 会用一个叫iface的实例包装它)
8. 接口也可以组合使用。 如果多个接口之间存在方法重名冲突的话,会编译不过。
而且即使函数签名不一样,只是重名,也一样会编译不过。
二、关于指针的有限操作
1. 不可寻址的三种情况:不可变的值,临时结果,不安全的(操作会破坏程序的一致性,引发不可预知的错误)
2. 不可寻址的状态下,无法获取变量的指针,也就无法执行一些指针相关的操作。
因此,New("little pig").SetName("monster") 这样是会编译错误的。
同样情况,自增自减语句也要求表达式的结果值必须是可寻址的。因此,临时变量也不能自增。
3. 对于字典变量索引表达式结果值虽然不可寻址,但有三种例外的情况,不可寻址也能正确运行:
(1) 可以做自增操作
(2) 可以做赋值操作
(3) 可用用于range子句的for语句中,在range关键字左边的表达式
4. 指针的转换
dog := Dog{"little pig"}
dogP := &dog
dogPtr := uintptr(unsafe.Pointer(dogP))
一个指针值(dogP) 可以被转换为一个unsafe.Pointer类型的值,再转成 uintptr 类型的值。
只要再配合 unsafe.Offsetof(dogP.name) 方法,可以跳过各种限制直接查看和修改数据的权力。
这是个非常规操作,可以用于调试。
三、go语句及其执行规则
1. 线程分 系统级线程 和 用户级线程。
用户级线程 架设在 系统级线程 之上,由用户代码来控制创建、执行和销毁。
2. 用户级线程 优势: 创建和销毁不通过操作系统,速度更快。由用户控制执行,可以很灵活。
用户级线程 劣势: 实现复杂,用户须全权负责具体实现,以及与操作系统正确地对接。
3. Go 并发编程模型中的三个主要元素:
(1) G (goroutine):用户级线程
(2) M (machine): 系统级线程
(3) P (processor): 运行中介,使多个G和多个M可以适时地对接。又称调度器。
4. 调度器P的具体作用:
(1) 用户级线程G 因事件(比如等待 I/O 或锁) 而暂停运行的时候,调度器P 会发现并把G与 系统级线程M 分离, 释放M的资源给其他G使用
(2) 更多的G 需要运行的时候,P 会寻找 空闲的 M
(3) M 不够的时候,P 向操作系统申请新的 M。 M 不使用了,P 会及时地把M 销毁。
5. 什么是主 goroutine?
主 goroutine 是 Go 程序的运行后被自动地启用的,不需要用户做任何手动的操作。
主 goroutine 的go函数就是 作为程序入口的main函数。
6. 怎样才能让主 goroutine 等待其他 goroutine?
(1) 简单办法: 使用睡眠函数 time.Sleep (time.Millisecond * 100)
(2) 更优办法:使用通道 chan
main函数创建一个通道,长度与我启用的 goroutine 的数量一致。在每个 goroutine 运行完毕前,向该通道发送一个值。
main函数从通道接收元素值,接收的次数与 goroutine 的数量一致时,就完成了等待。
(3) 更更优的办法:使用sync.WaitGroup。 后面详述。
func worker(id int, done chan<- bool) {
for i := 0; i < 3; i++ {
fmt.Printf("Worker %d 处理任务 %d\n", id, i)
}
done <- true // 发送完成信号
}
func main() {
numWorkers := 3
done := make(chan bool, numWorkers) // 带缓冲通道,容量等于子goroutine数量
// 启动多个子goroutine
for i := 0; i < numWorkers; i++ {
go worker(i, done)
}
// 主goroutine等待所有子goroutine完成
for i := 0; i < numWorkers; i++ {
<-done // 接收完成信号,阻塞直到所有信号到达
}
}
7. 怎样让启用的多个 goroutine 按照既定的顺序运行?
(1) go函数接受一个int类型的参数,用来标明允许序号
for i := uint32(0); i < 10; i++ {
go func(i uint32) {
trigger(i)
} (i) //这里输入一个标明序号的参数
}
(2) 上面的 trigger函数内部,使用公共变量count来计数。 count 和 前面的序号相等时,就表示按照顺序,可以继续运行了。
trigger := func(i uint32, fn func()) {
for {
if n := atomic.LoadUint32(&count); //读取公共变量count,以原子方式读取
n == i { // 与序号i 相等,可以执行。
fn() //调用外部传入的函数,执行需要的逻辑
atomic.AddUint32(&count, 1) //原子方式增1,让下一个goroutine执行
break
}
time.Sleep(time.Nanosecond) //睡眠,持续循环
}
}
原子方式增1,让下一个goroutine 获得执行对应逻辑的机会。
四、if语句、for语句和switch语句
1. for语句中,只有一个迭代变量的话,该迭代变量只会代表元素的索引值。
那样,只有一个迭代变量的情况意味着什么呢?这意味着,该迭代变量只会代表当次迭代对应的元素值的索引值。
numbers1 := []int{1, 2, 3, 4, 5, 6}
for i := range numbers1 { // i表示的是索引编号,从0开始
}
2. for range语句中,range右边的的 numbers1 ,在整个循环过程中,只会执行一次求值。
如果numbers1 是数组,那求得是拷贝值,不会被修改。
如果numbers1 是切片,那求得是引用值,会被修改。
3. 声明数组和切片的方式很接近,区别是是否确定了长度。数组长度确定的,切片长度是不确定的。
声明数组: numbers2 := [...]int{1, 2, 3, 4} (...是自动推断长度的意思,也可以给具体数值)
声明切片: numbers2 := []int{1, 2, 3, 4}
4. switch case 语句中,switch表达式值类型,和各个case 表达式值类型,必须相同。
如果不同,会以 switch表达式值类型 为基准, 对 case 表达式值进行类型转换。
如果类型转换失败,会编译不过。比如 把 int8 转换为 无类型的常量,就会失败。
5. switch case 语句中,各个 case 表达式的值不能重复。但只是字面量值不能重复,用变量的话,可以跳过这个限制。
五、错误处理
1. error类型是一个接口类型,是Go 的内建类型。
error接口声明中只包含了一个方法Error。Error没有参数,但会返回一个string类型的结果。
Error方法就相当于其他类型的 String方法,可以输出字符串。
2. 最基本的生成错误值的方式:errors.New函数
err := errors.New("empty request")
(1) 传入一个由字符串的错误信息
(2) 返回包含了这个错误信息的 error类型值.
(3) 值 err 的 Error方法,会返回之前传入的错误信息,相当于String方法。
3. error类型值,遇到打印print操作时,会自动调用它的Error方法,返回字符串。
4. 从静态类型值err转换为动态类型值: err := err.(type)
5. 如何判断 err是什么类型?
转换为动态类型值,然后用switch和特定类型值做判断
switch err := err.(type) { //转换为动态类型值
case os.ErrClosed: //和特定类型值做类型判定
fmt.Printf("error(closed): %s\n", err)
case os.ErrInvalid:
fmt.Printf("error(invalid): %s\n", err)
}
6. 链式错误关联: 在错误类型中,可以安放一个可以代表潜在错误的字段(同样是error接口类型),表示这个错误的更深层次错误是什么,帮助找到错误的根源。
7. 通过errors.New生成的错误值 只能被赋给变量。(因为error是接口类型)
这些代表错误的变量,有可能会被外部越权修改。解决办法:
(1) 编写私有的错误值以及公开的获取和判等函数。
(2) 使用 syscall包的 Errno类型。它既包含error接口的实现类型,也包含uintptr的常量类型。也就是改成使用常量给外部使用。
六、panic函数、recover函数以及defer语句
1. panic 从被引发到程序终止运行的大致过程是怎样?
(1) panic 详情会被建立起来,包含错误类型值,goroutine的ID,代码行数,源码文件的路径。
(2) 此行代码所属函数的执行终止,控制权转移至再上一级的代码调用位置。
(3) 一级一级地沿着调用栈的反方向传播至外层函数 (即go函数和main函数)
2. 主动触发panic情况下,如何让panic详情包含更多的信息?
(1) panic是内建函数,接受一个空接口(interface{})类型的参数。 所以适合传入 Error 类型参数。
(2) 创建 Error时 输入的信息,会被包含进panic输出信息里。
3. 怎样对 panic 施加保护,避免程序崩溃?
(1) 联合使用内建函数recover 和 defer语句。
(2) recover函数专用于恢复 panic,无输入参数,会返回一个空接口类型的值。(主动触发panic情况下,传入的参数值)
(3) defer语句用来延迟执行代码。延迟到该语句所在的函数即将执行结束时,才执行defer内的代码。 无论结束执行的原因是什么。
func main() {
defer func(){ //用defer语句调用匿名函数
p := recover() //调用recover函数
if p != nil {
fmt.Printf("panic: %s\n", p)
}
fmt.Println("Exit defer function.")
}()
panic(errors.New("something wrong")) //触发panic。
}
4. 如果一个函数中有多条defer语句,那么几个defer函数的执行顺序是怎样的?
defer函数调用顺序,与它们的出现顺序(严谨说是执行顺序)完全相反。
如果是嵌套方式,就是从内到外。 如果是for循环方式,就是从最后那次循环的defer开始,反向往前执行,后进先出。