GO语言核心30讲 进阶技术 (接口,指针,go语句,if for switch语句,错误处理,panic recover)

 原站地址: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。 后面详述。

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开始,反向往前执行,后进先出。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值