读书笔记——Go语言核心编程

李文塔著
描述

1. 基本数据类型

str

var a = "hello, word"    
b := a[0]    
c := a[1:]    
d := []byte(a)  // 字节数组    
e := []rune(a)  // unicode的字符数组    
for i, v := range a{    
    fmt.Println(i, v)    
}    

字符串是常量, 可以通过索引访问某个单元字节, 但不能修改。
底层是一个二元的数据结构, 一个是长度, 一个是指向字节数组的指针。

int

总共12种
byte、int、int8、int16、int32、 int64, uint、uint8、uint16、uint32、uint64、 uintptr
不同整形类型之间必须进行强制类型转换

rune

rune在GO内部是int32的别名(4个字节), byte是uint8的别名(一个字节)。 两者都是字符类型, 赋值时用单引号

float

包括float32和float64, 两个浮点数之间不应该用==或者!=进行比较, 应该用math库。

bool

var ok bool // ok is false(默认值)

复数

两种:complex64, complex128
var a complex64 = 3.1 + 6i
var b = complex(2.1, 3)
a := real(b)
c := image(b)

2. 复合数据类型

指针

*T 出现在=的左边标识指针声明, 出现在=右边表示取指针的值
Go 不支持指针的运算

var a = 11    
p := &a  // *p 和 a 的值都是11    

p ++ // error, 不允许    

数组

数组创建完长度就固定了, 不可以再追加元素
数组是值类型的, 数组赋值或作为函数参数都是值拷贝
长度也是数组类型的组成部分, [10]int 和 [20]int 表示不同的类型

a := [...]int{1, 2, 3}    
var b [2]int    
for i, v := range a {    
}    

切片

数据结构中有指向数组的指针, 所以是一种引用类型
切片三元素: 底层数组的指针、元素数量、底层数组的容量

var array = [...]int{1, 2, 3, 4, 5, 6}    
s1 := array[0:4]    
s2 := array[2:]    
a := make([]int, 10, 15)    
d := make([]int, 2, 2)    
copy(d, a) // copy只会复制d和a中长度最小的    
a = append(a, 1)    

str := "hello, 世界"    
a := []byte(str)    
b := []rune(str)    
fmt.Println(a)    
fmt.Println(b)    

map

map也是引用类型, 可以使用range 迭代map,但不保证顺序。
go内置的map不是并发安全的, 并发安全的map可以使用标准包sync中的map
不要直接修改map value内的某个元素的值, 需要将value整体替换

mp := map[string]int{"a": 1, "b":2}    
mp2 := make(map[int]string)    
mp2[1] = "tom"    
mp2[2] = "jerry"    
delete(mp, 2)    

for k, v := range(mp){    
    fmt.Println("key=", k, "value=", v)    
}    

struct

struc的存储空间是连续的, 其字段按照声明时的顺序存放。

type Person struct {    
    Name string    
    Age int    
}    

type Student struct {    
    *Person,    
    Number int    
}    

channel

interface

3. 控制结构

if

if 后面可以带一个简单的初始化语句, 并以分号分割, 该简单语句声明的变量的作用域是整个if语句块, 包括后面的else if 和 else 分支。

if x := f(); x < y {    
    return x    
} else if x > z {    
    return z    
} else {     
    return y    
}    

for

for init; condition; post { }    
for condition {}  // 等价于while 语句    
for {} // while true    

//访问map, 设计不好, 到底是返回一个参数, 还是两个?    
for k, v := range map {}    
for k := range map {}    

//访问数组、切片    
for index, value := range array{}    
for index := rnage array{}    
for _, value := range array{}    

// 访问通道    
for value := range channel{}    

switch

通过fallthough语句来强制执行下一个case子句(不必判断条件是否满足)

switch i := "y"; i { // 后面跟一个简单的初始化语句    
case "y", "Y":    
    fmt.Println("yes")    
    fallthrough    

case "n", "N":    
    fmt.Println("no")    
}    

var score int    
var grade string    
switch {  // 替换 else if    
case score >= 90:    
    grade = "a"    
case score >= 80:    
    grade = "b"    
default:    
    grade = "F"    
}    

goto & label

goto 语句只能在函数内跳转, 而且不能跳过内部变量声明语句。
goto 语句只能跳转到同级作用域或者上层作用域内, 不能跳转到内部作用域
continue、break可以配合label使用, 但label和continue、break在一个函数内。

4. 函数

定义 & 函数签名

首字母的大小决定了该函数在其他包的可见性。
函数的参数和返回值都需要使用()来包裹, 如果只有一个返回值, 而且非命名, 返回参数的()可以省略。

fun funcName(param-list) (result-list) {    
    funcition-body    
}    

func A() { // 没有参数, 没有返回值, 默认返回0    
}    

func add(a, b int) int { // a int, b int的简写    
    return a + b    
}    

func add(a, b int) (sum int) {    
    sum = a + b //    
    retunr // 如果是sum := a+b, 这里要改成return sum, 覆盖返回变量sum    
}    

如果返回多个结果, 错误类型一般放最后一个
Go函数实参到形参的传递永远是值拷贝

不定参数类型必须相同, 而且必须是函数最后一个参数, 参数名相当于切片

func sum(arr ...int) (sum int) {    
    for _, v := range arr {    
    sum += v    
    }    
    return    
}    

slice := []int{1, 2, 3, 4}    
array := [...]int{1, 2, 3, 4}    
sum(slice...) //数组不可以作为实参传递给不定参数, 必须是slice    

一个函数的类型就是函数定义首行去掉函数名、参数名

fmt.Printf("%T/n", add)  // func(int, int) int    

type Op func(int, int) int  // 定义一个函数类型    
func do(f Op, a, b int) int {    
    return f(a, b)    
}    
a := do(add, 1, 2) // 函数名add可以当作相同函数类型传参, 不需要强制类型转换    

匿名函数

var sum = func(a, b int) int {    
    return a + b    
} // 匿名函数直接复制给函数变量    

defer

defer 后面必须是函数或方法的调用, 不能是语句
defer的实参在注册时通过值拷贝传递进去
主动调用os.Exit(int)退出进程时, defer将不再被执行
defer相对于普通的函数调用需要间接的数据结构的支持, 相对于普通函数有一定的性能损耗

闭包

闭包 = 函数 + 引用环境
闭包对闭包外的环境引入是直接引用, 编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
对象是附有行为的数据, 而闭包是附有数据的行为
同一个函数返回的多个闭包共享该函数的局部变量

func fa(a int) func(i int) int {    
    return func(i int) int {    
        println(&a, a)    
        a += i    
        return a        
    }    
}    

f := fa(1)    
g := fa(1)  // g引用的闭包环境中的a和f的a不是同一个    

println(f(1))  // 2    
println(f(1))  // 3    
println(g(1))  // 2    
println(g(1))  // 3    

错误处理

panic用来主动抛出错误, recover用来补获panic抛出的错误
recover() 只有在defer后面的函数体内被直接调用才能补获panic终止异常, 否则返回nil

defer recover()    
defer fmt.Println(recover())  // 补获失败    

func except(){    
    println("defer inner")    
    if err := recover(); err != nil {    
        fmt.Println(err)    
    }    
}    

func test() {    
    defer except() // 补获成功    
    panic("test panic")    
}    

函数并不能补获内部新启动的goroutine所抛出的panic
Go提供了两种错误处理的方式, 一种是借助panic和recover的抛出补获机制, 另一种是使用error错误类型。使用规则为: a 程序发生错误导致程序不能容错继续进行, 此时应该调用panic。 b 程序虽然发生错误,但是程序能够容错继续进行, 此时应该返回error

// error 定义    
type error interface {    
        Error() string    
}    

func Sqrt(f float64) (float64, error) {    
    if f < 0 {    
        return 0, errors.New("math: square root of negative number")    
    }    
    // 实现    
}    

5. 类型系统

命名类型与非命名类型

命名类型包括20个预声明的简单类型与用户自定义类型(type )
未命名类型又称为类型字面量, Go中的复核类型都是类型字面量: array、slice、map、channel、pointer、function、struct、interface

底层类型

预声明类型和字面量类型的底层类型都是他们本身
自定义类型的底层类型是逐层递归向下查找的

类型方法

// 类型方法接收者是值类型    
fun (t TypeName) MethodName (ParamList) (ReturnList) {    
// body    
}    

// 类型方法接收者是指针类型    
fun (t *TypeName) MethodName (ParamList) (ReturnList) {    
// body    
}    

非命名类型不能自定义方法
方法的定义必须和类型的定义在同一个包中
新类型不能调用原有类型的方法, 但是底层类型支持的运算可以被新类型继承

值调用和表达式调用:

type T struct {    
    a int    
}     

func (t T) Get() int {    
    return t.a    
}    

func (t *T) Set (i int) {    
    t.a = i    
}    

var t = &T{}    
t.Set(2)  // 方法值调用    
t.Get()  // 方法值调用    
(T).Get()  // 表达式调用    

使用值调用方式调用时, 编译器会进行自动转换。
使用表达式调用时, 编译器不会进行转换。
T 类型的方法集是S
T类型的方法集是 S 和 S

组合

在简写模式下, Go编译器优先从外向内逐层查找方法, 同名方法中外层的方法能够覆盖内层的方法。
1. 若类型S包含匿名字段T, 则S的方法集包含T的方法集
2. 若类型S包含匿名字段T,则S的方法集包含T和T的方法集
3. 不管类型S中嵌入的匿名字段是T还是T, S的方法集总是包含 T 和 *T的方法集

函数类型

分为函数字面量类型和命名类型 type ADD func (int, int) int
字面量类型又包含有名和匿名, 有名即func add (int, int) int

6. 接口

一个具体类型实现接口不需要在语法上显示的声明,只要具体类型的方法集是接口方法集的超集, 就代表该类型实现了接口。

接口初始化

如果具体类型实例的方法是某个接口的方法集的超集, 则称该具体类型实现了接口, 可以将该具体类型的实例直接赋值给接口类型的变量。
接口被初始化后, 调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。

package main    

type Printer interface {    
    Print()    
}    

type S struct {}    

func (s S) Print(){    
    println("Print")    
}    

var i Printer    
// i.Print()  会报panic, 因为还没有初始化    
i = S{}    
i.Print()    

接口方法调用不是一种直接调用, 有一定的运行时开销

接口的动态类型和静态类型

接口绑定的具体实例的类型称为接口的动态类型
静态类型本质就是接口的方法签名集合。 两个接口如果方法签名集合相同(顺序可以不同),则这两个接口在语义上完全等价, 它们之间不需要强制类型转换就可以相互赋值

接口断言

形式:

o := i.(TypeName)    

// 一般用法如下    
if o, ok := i.(TypeName); ok {    
// body    
}    
  1. 如果TypeName是一个具体类型名, 则类型断言用于判断接口变量i绑定的实例类型是否就是TypeName。如果是的话, o的类型就是TypeName, 值是接口绑定的实例值的副本。 否则panic
  2. 如果TypeName是一个接口类型名,则类型断言用于判断接口变量i绑定的实例类型是否同时实现了TypeName接口。 如果是,则o的类型是接口类型TypeName, o底层绑定的具体实例是i绑定的实例的副本。

空接口

空接口 interface {}, 由于空接口的方法集为空, 所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型的实例。

7. 并发

并发和并行

  1. 并行意味着程序在任意时刻都是同时运行的 (时间点)
  2. 并发意味着程序在单位时间内是同时运行的 (时间段)

goroutine

  1. go 后面跟一个函数, 来启动goroutine
  2. go后面函数的返回值会被忽略
  3. 调度器不能保证多个goroutine的执行次序
  4. 没有父子goroutine的概念, 所有goroutine都是平等的被调度和执行的
  5. runtime.GOMAXPROCS(n) n 大于1表示设置GOMAXPROCS的值, 否则表示查询当前值

channel

// 创建无缓冲通道    
c := make(chan datatype)    

// 创建有缓冲的通道    
c := make(chan datatype, 10)    

// 写通道    
c <- struct{}{}    

// 读通道    
<- c    

// 关闭通道    
close(c)    
  1. 通道分为无缓冲的通道和有缓冲的通道。无缓冲的通道既可以用于通信, 也可以用于两个goroutine同步, 有缓冲的通道主要用于通信。
  2. goroutine 运行结束后, 写到缓冲通道中的数据不会消失
  3. 向已经关闭的通道写数据会导致panic
  4. 重复关闭通道会panic
  5. 读取已关闭的通道不阻塞不panic, 直接返回通道元素的零值。 可以使用comm, ok 语法判断通道是否已关闭
  6. 向未初始化的通道写数据或读数据都会导致当前goroutine永久阻塞
  7. 向缓冲区已满的通道写入数据会导致goroutine阻塞
  8. 通道中没有数据, 读取该通道会导致goroutine阻塞

WaitGroup

package main    
import (    
    "sync"    
)    

var wg sync.WaitGroup    

wg.Add(1)    
wg.Done()  // 和wg.Add(-1)等价    
wg.Wait() // 等待goroutine结束    

select

如果监听的通道有多个可读或可写的状态, 则select随机选取一个来处理

package main    

func main() {    
    ch := make(chan int, 1)    
    go func(chan int) {    
        for {    
            select {    
                case ch <- 0:    
                case ch <- 1:        
            }    
        }    
    }(ch)    
    for i := 0; i< 10; i++ {    
        println(<-ch)    
    }    
}    

通知退出机制

读取已经关闭的通道不会阻塞, 也不会panic, 而是立即返回该通道存储类型的零值。

并发范式

  1. 生成器
  2. 管道
  3. 固定worker池
  4. future

并发模型

  1. CSP最基本的思想是:将并发系统抽象为Channel 和 Process 两部分,Channel用来传递消息, Process用于执行, Channel和Process之间相互独立, 没有从属关系, 消息的发送和接受有严格的时序限制
  2. Go语言主要借鉴了Channel和Process的概念, 在Go中Channel就是通道, Process就是goroutine
  3. 多进程下, 每个进程都有自己独立的内存空间, 隔离性好、健壮性搞。进程间的通信需要多次在内核区和用户区之间复制数据
  4. 多线程是指启动多个内核线程进行处理, 线程的有点是通过共享内存进行通信更快捷, 切换代价小;缺点是多个线程共享内存空间,极易导致数据访问混乱, 健壮性不高
  5. 协程是一种用户态的轻量级线程, 协程的调度完全由用户态程序控制。
  6. 协程控制了线程数, 保证每个线程的运行时间片充足(减少切换次数)
  7. Go的并发执行模型就是一种变种的协程模型

8. 反射

反射是指计算机程序在运行时可以访问、检测和修改本身状态或行为的一种能力

Go的类型分类

最佳实践

  1. 在库或框架内部使用反射, 而不是把反射接口暴露给调用者, 复杂性留在内部, 简单性放在接口
  2. 框架代码才考虑使用反射, 一般业务代码没有必要抽象到反射的层次
  3. 除非没有其它办法, 否则不要使用反射技术。