2019-12-22-Go语言入门笔记

周末两天拜读了飞雪无情大佬的博客,学习了Go语言,记录了一些笔记,方便之后查看。

笔记记录的内容没有按照书中的顺序,从博客里看到哪篇感兴趣就点开看看,需要系统学习Go语言的朋友可以买本书。

1. golang必备技巧:接口型函数

原始接口实现

// 定义接口
type Handler interface {
    Do(k, v interface{})
}

func Each(m map[interface{}]interface{}, h Handler) {
    if m != nil && len(m) > 0 {
        for k,v := range m{
            h.Do(k, v)
        }
    }
}

// 实现接口
type welcome string
func (w welcome) Do(k, v interface{}) {
    fmt.Printf("%s, 我叫%s, 今年%d岁\n", w, k, v)
}

func main(){
    persons := make(map[interface{}]interface{})
    persons["张三"] = 20
    persons["李四"] = 23
    persons["王五"] = 26

    var w welcome = "大家好"
    Each(persons, w)
}

使用接口型函数可以用一个更有意义的名字代替Handler接口中的Do函数

// 新的接口定义
type Handler func(k, v interface{})

func (f HandlerFunc) Do(k, v interface{}) {
    f(k, v)
}

// 实现接口可以用更加灵活的函数名
type welcome string
func (w welcome) selfInfo(k, v interface{}) {
    fmt.Printf("%s, 我叫%s, 今年%d岁\n", w, k, v)
}
func main(){
    // 其余部分不变
    Each(persons, HandlerFunc(w.selfInfo)) # selfInfo强制类型转换成HandlerFunc,两者是同一种类型
}

为了不让用户自己做强制转型,可以定义一个帮助函数

func EachFunc(m map[interface{}]interface{}, f func[k, v interface{}]) {
    Each(n, HandlerFunc(f))
}

EachFunc接受的是func(k,v interface{})类型的函数,没有必要再实现Handler接口

func selfInfo(k,v interface{}){
    fmt.Printf("大家好,我叫%s,今年%d岁\n", k, v)
}
func main(){
    persons := make(map[interface{}]interface{})
    persons["张三"] = 20
    persons["李四"] = 23
    persons["王五"] = 26

    EachFunc(persons, selfInfo)
}

到此,完成函数型接口的设计与使用

2. 标识符可见性

go语言中函数或者类型的可见性由大小写进行区分的,函数或者类型的名字以大写字母开头,其他包可以访问,否则只能在包内访问

:=操作符可以推断变量的类型

一个导出的类型,包含未导出的字段,则对应的字段无法在外部访问,也即不能初始化,该类型中的函数同理

3. 接口

接口是一个抽象类型,并且是一种引用类型
把用户定义的类型称为实体类型,和接口类型区分开
接口值内部布局,包含两个字长的数据接口
第一个字是指向内部表接口的指针,内部表中存储实体类型的信息及相关联的方法集
第二个字是指向存储实体类型的指针

实体类型以值接收者实现接口的时候,实体类型的值和指针都实现了该接口
实体类型以指针实现接口的时候,只有实体类型的指针实现了该接口

4. 调试

打印输出
日志输出
GDB
go build "-N -l" main.go
"-N -l"表示忽略编译器优化
Delve,专为Go程序而生的调试工具,尤其在在调试多goroutine的时候

5. Writer和Reader

Writer和Reader是两个高度抽象的接口,都是只有一个方法
接口设计遵循Unix的输入和输出
io包的大部分操作和类型都是基于这两个接口

type Writer interface{
    Write(p []byte)(n int, err error)
}
type Reader interface {
    Read(p []byte)(n int, err error)
}

6. 读写锁

sync.RWMutex

7. 通道

在多个goroutine并发中,不仅可以通过原子函数和互斥锁来保证对共享资源的安全访问,还可以通过使用通道,在多个goroutine发送和接收共享数据,达到数据同步的目的
通道,一个goroutine可以往里写数据,另一个goroutine可以从中读数据
通道用于goroutine之间通信,发送和接收的操作符都是<-

ch := make(chan int)
ch <- 2 // 往通道发送数据
x := <-ch // 从通道读数据,赋值给x
<-ch // 从通道读数据,然后忽略
close(ch)

通道分为有缓冲通道和无缓冲通道,默认通道是无缓冲的
单向通道

var send chan<- int     // 只能发送
var receive <-chan int // 只能接收

8. 反射

https://www.flysnow.org/2017/06/13/go-in-action-go-reflect.html
反射,程序在运行时可以访问、检测和修改它本身状态或行为的一种能力

任何接口都由两部分组成,一个是接口的具体类型,一个是具体类型的值。interface{}可以表示任何类型,所以var i int = 3,变量i可以当成一个接口。
标注库提供reflect.Value, reflect.Type两种类型来获取任意对象的Value和Type

9. Struct Tag

先看一个json字符串和struct类型互相转换的一个例子

type User struct{
    Name string `name`
    Age int `age`
}
func main(){
    var u User
    h := `{"name":"张三", "age":15}`
    err := json.Unmarshal([]byte[h], &u)
    if err == nil {
        fmt.Println(u)
    }
}

json解析的原理就是通过反射获得每个字段的tag,然后把解析的json对应的值赋值给它们。

10. 上下文(context)

结束一个后台goroutine,可以使用chan+select的方式
如果goroutine的关系很复杂,通过chan+select的方式显得有些笨拙

使用context可以实现控制多个goroutine的功能

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go func(ctx context.Context) {
        for {
            select {
                case <-ctx.Done():
                    fmt.Println("停止")
                    return 
                default:
                    fmt.Println("监控中")
                    time.Sleep(2*time.Second)
            }
        }
    }(ctx)

    time.Sleep(10*time.Second)
    fmt.Println("通知监控停止")
    cancel()
    time.Sleep(5*time.Second)
}

context接口定义

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chain struct{}
    Err() error
    Value(key interface{}) interface{}
}

11. 并发示例pool

本节通过使用有缓冲的通道实现一个资源池,资源池适用于在多个goroutine之间共享资源,每个goroutine可以从资源池里申请资源,使用完之后放回资源池,其它goroutine可以复用。

首先定义一个结构体

type Pool struct {
    m sync.Mutex
    res chan io.Closer // 所有实现了io.Closer接口的类型都可以作为资源,交给资源池管理
    factory func() (io.Closer, error) // 生成新资源的函数
    closed bool
}

定义一个资源池已经关闭的错误
var ErrPoolClosed = errors.New("资源池已经关闭")
创建资源池的工厂函数

func New(fn func() (io.Closer, error), size uint) (*Pool, error) {
    if size <= 0 {
        return nil, errors.New("size值太小")
    }
    return &Pool{
        factor: fn,
        res make(chan io.Closer, size),
    }, nil
}

从资源池获取一个资源

func (p *Pool) Acquire() (io.Closer, error) {
    select {
        case r, ok := <-p.res:
        log.Println("Acquire:共享资源")
        if !ok {
            return nil, ErrPoolClosed
        }
        return r,nil
    }
    default:
        log.Println("Acquire:新生成资源")
        return p.factory()
}

关闭资源池

func (p *Pool) Close() {
    p.m.Lock()
    defer p.m.Unlock()

    if p.closed() {
        return 
    }
    p.closed = true
    close(p.res)
    for r := range p.res {
        r.Close()
    }
}

资源释放

func (p *Pool) Release(r io.Closer) {
    p.m.Lock()
    defer p.m.Unlock()

    if p.closed {
        r.Close()
        return
    }

    select {
        case p.res <- r:
            log.Println("资源释放到池子")
        default:
            log.Println("资源池满了,释放资源")
            r.close()
    }
}

使用资源池示例

package main
import (
    "io"
    "log"
    "math/rand"
    "sync/atomic"
    "time"
)
const (
    maxGoroutine    = 5
    poolRes         = 2
)

func main() {
    var wg sync.WaitGroup
    wg.Add(maxGoroutine)

    p, err := common.New(createConnection, poolRes)
    if err != nil {
        log.Println(err)
        return
    }

    for query := 0; query < maxGoroutine; query++ {
        go func(q int) {
            dbQuery(q, p)
            wg.Done()
        }(query)
    }

    wg.Wait()
    log.Println("关闭资源池")
    p.close()
}

func dbQuery(query int, pool *common.Pool) {
    coon, err := pool.Acquire()
    if err != nil {
        log.Println(err)
        return 
    }
    defer pool.Release(coon)

    time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
    log.Printf("第%d个查询,使用的是ID为%d的数据库连接", query, conn.(*dbConnection).ID)
}

type dbConnection struct {
    ID int32 
}

func (db *dbConnection) Close() error {
    log.Println("关闭连接", db.ID)
    return nil
}

var idCounter int32 
func createConnection() (io.Closer, error) {
    id := atomic.AddInt32(&idCounter, 1)
    return &dbConnection{id}, nils
}

go语言提供了原生的资源管理池sync.Pool,防止用户重复造轮子
使用sync.Pool实现上面的示例

package main
import (
    "log"
    "math/rand"
    "sync"
    "sync/atomic"
    "time"
)
const (
    maxGoroutine = 5
)
func main() {
    var wg sync.WaitGroup
    wg.Add(maxGoroutine)

    p := &sync.Pool{
        New:createConnection,
    }

    for query := 0; query < maxGoroutine; query++{
        go func(q int) {
            dbQuery(q, p)
            wg.Done()
        }(query)
    }
    wg.Wait()
}

func dbQuery(query int, pool *sync.Pool) {
    conn := pool.Get().(*dbConnection)
    defer pool.Put(conn)

    // 模拟查询
    time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
    log.Printf("第%d个查询,使用的是ID为%d的数据库连接", query, conn.ID)
}
//数据库连接
type dbConnection struct {
    ID int32//连接的标志
}

//实现io.Closer接口
func (db *dbConnection) Close() error {
    log.Println("关闭连接", db.ID)
    return nil
}

var idCounter int32

//生成数据库连接的方法,以供资源池使用
func createConnection() interface{} {
    //并发安全,给数据库连接生成唯一标志
    id := atomic.AddInt32(&idCounter, 1)
    return &dbConnection{ID:id}
}

12. 并发示例Runner

Runner通过使用通道来监控程序的执行时间,生命周期,甚至终止程序
Runner可以在后台执行任何任务,并且受我们的控制。
首先我们来自己构建一个Runner

type Runner struct {
    tasks []func(int) // 执行的任务
    complete chan error // 用于通知任务完成
    timeout <-chan time.Time // 任务多久内完成
    interrupt chan os.Signal // 强制终止信号
}

定义一个工厂函数

func New(tm time.Duration) *Runner {
    return &Runner{
        complete:make(chan error),
        timeout:time.After(tm),
        interrupt:make(chan os.Signal, 1)
    }
}

给Runner添加任务

func (r *Runner) Add(tasks ...func(int)) {
    r.tasks = append(r.tasks, tasks...)
}

定义两个错误变量

var ErrTimeOut = errors.New("执行超时)
var ErrInterrupt = errors.New("被中断")

执行任何

// 执行任务过程中收到中断信号,返回中断错误
// 任务全部执行完,返回nil
func (r *Runner) run() error {
    for id, task := range r.tasks {
        if r.isInterrupt() {
            return ErrInterrupt
        }
        task(id)
    }
    return nil
}
// 检查是否收到了中断信号
func (r *Runner) isInterrrupt() bool {
    select {
        case <-r.interrupt:
            signal.Stop(r.interrupt)
            return true
        default:
            return false
    }
}

注意,select如果没有default的话,会阻塞直到r.interrupt可以接收值为止
接下来,开始执行惹怒我,并监视任务的完成

func (r *Runner) Start() error {
    // 希望接收哪些系统信号
    signal.Notify(r.interrupt, os.Interrupt)

    go func() {
        r.complete <- r.run()
    }()
    select {
        case err := <- r.complete:
            return err
        case <-r.timeout:
            return ErrTimeOut
    }
}

测试Runner

func main() {
    log.Println("开始执行任务")
    timeout := 3*time.Second
    r := common.New(timeout)

    r.Add(createTask(),createTask(),createTask())

    if err := r.Start; err != nil {
        switch err {
        case common.ErrTimeOut:
            log.Println(err)
            os.Exit(1)
        case common.ErrInterrupt:
            log.Println(err)
            os.Exit(2)
        }
    }
    log.Println("任务执行结束")
}

13. 日志

log包提供的选项常量

const (
    Ldata       = 1 << iota 
    Ltime
    Lmicroseconds
    Llongfile
    Lshortfile
    LUTC                    // 日期时间转为0时区
    LstdFlags   = Ldata | Ltime  // Go提供的标准日志抬头
)

14. goroutine

go语言中的并发指的是让某个函数独立于其它函数运行的能力,一个goroutine就是一个独立的工作单元,runtime会在逻辑处理器上调度这些goroutine来运行,一个逻辑处理器绑定一个操作系统线程,因此goroutine不是线程,它是一个协程,由go语言runtime本身的算法实现的。

默认情况,go给每个可用的物理处理器都分配一个逻辑处理器。
设置逻辑处理器的个数

runtime.GOMAXPROCS(runtime.NumCPU())

15. 并发资源竞争

go语言提供了atomic和sync包用来给共享资源同步加锁
atomic提供了很多原子化的函数可以保证并发下资源的同步访问修改问题。但是支持的数据类型有限
sync中的互斥锁可以保护临界区

16. 数组,切片,map

数组
数组是长度固定的数据类型,必须存储一段相同类型的元素,而且这些元素是连续的。固定长度,是数组和切片最明显的区别
声明数组

var array [5]int  // 声明
array = [5]int{1,2,3,4,5} // 初始化

创建数组的时候进行初始化

array := [5]int{1,2,3,4,5}

不指定长度的初始化

array := [...]int{1,2,3,4,5}

只给特定索引的元素赋初始值

array := [5]int{1:1, 3:4}

相同类型的数组可以互相赋值,否则会编译错误
长度相同,每个元素的类型也相同,称为相同类型的数组

array := [5]int{1:1, 3:4}
var array1 [5]int = array   //success
var array2 [4]int = array   // error

指针数组的元素类型是指针

// 为索引1和3创建了内存空间
// 其它索引的指针是零值nil
array := [5]*int{1: new(int), 3:new(int)}
*array[1] = 1   // ok
array[0] = 0    // panic 索引0没有分配内存
array[0] = new(int) 
*array[0] = 2   // ok

在函数间传递数组指针

func modify(a *[5]int) {
    a[1] = 3
}
func main() {
    array := [5]int{1:2, 3:4}
    modify(&array)
}

切片
切片可以对应C++语言里的vector,是对原生数组的一层封装和抽象
切片有三个字段,指向底层数组的指针,切片长度,容量

创建切片

slice := make([]int, 5) // 长度5,容量默认为5
slice := make([]int, 5, 10) // 长度5,容量10
slice := []int{1,2,3,4,5} // 不指定[]中的值,容量=长度
slice := []int{4:1}

切片和数组的微小差别

// 数组
array := [5]int{4:1}
// 切片
slice := []int{4:1}

区分nil切片和空切片
nil切片表示指向底层数组的指针为nil,表示一个不存在的切片
空切片对应的指针是个地址,表示一个空集合

基于现有数组或切片创建新的切片

slice := []int{1,2,3,4,5}
slice1 := slice[:]
slice2 := slice[0:]
slice3 := slice[:5]

新切片和原切片或原数组底层共用同一个数组
计算切片的大小和容量

slice := []int{1,2,3}
len(slice)
cap(slice)

append会增加底层数组的容量,容量小于1000时,总是成倍增长,超过1000,增长因子为1.25

range返回的是切片元素的赋值,而不是元素的引用

切片在函数间传递时,切片是按值传递,但是底层的数组是共享的

map
map基于哈希表实现无序的键值对集合
创建nil map

var dict map[string]int

此时还不能使用,必须初始化,用make函数为其开辟内存空间

dict = make(map[string]int)

使用大括号进行声明并初始化

dict := map[string]int{}    // 空的map
dict := map[string]int{"张三":43} 

map的键可以任何可以用==运算符进行比较的值,切片、函数以及含有切片的结构类型不能用于map的键,因为具有引用语义,不可比较。
map的值可以是任何值,没有限制

检测键值对是否存在

age, exists := dict["李四"]

删除某个键值对
也可删除不存在的键值对,没有任何作用

delete(dict, "张三")

遍历

for key, value := range dict{

}

函数间传递map默认是引用

17. 包管理

命名导入

import myfmt "mylib/fmt"

每个包可以有多个init函数,会在main函数执行之前执行,用来初始化变量、设置包或者执行引导工作

使用空标识符导入一个包,会执行包里的init函数

18. doc, 开发工具

go doc 在终端查看文档
lib godoc 生成网页版文档

19. 类型

静态类型编程语言,编译是需要知道每个值的类型

基本类型
也即原始类型,对基本类型进行操作,一般都会返回一个新创建的值,因此在多线程下是安全的

引用类型,包括切片,map,接口,函数,chan
引用类型一般都是包含一个指向底层数据结构的指针
引用类型被复制时,指针的值被复制,仍然指向原来的底层数据

结构类型
通过在函数中传入结构体的指针,可以修改原有结构体的内容

自定义类型
结构类型也是自定义类型,也可以基于一个已有类型,创造一个新类型,类似C语言中的typedef

type Duration int64

但是Duration和int64本质上不是同一种类型不能互相赋值,不会做隐式类型转换

20. 函数方法

方法指的是有接收者的函数,指针作为接收者可以改变接收者的状态,值类型作为接收者不会改变接收者的状态

方法的调用既可以使用值接收者,也可以使用指针接收者,编译器会自动解引用以满足接收者的要求

对类型进行操作,弄清楚是要改变当前值,还是创建一个新值返回,然后决定用值传递还是指针传递

多值返回,不用简化了try catch操作

可变参数,本质上是一个数组,可以使用for range循环

21. 单元测试

go语言提供了测试框架
含有单元测试代码的go文件必须以_test.go结尾
单元测试的函数名必须以Test开头,且是可导出公开的函数
接收一个指向testing.T类型的指针,没有返回值

模拟调用
测试覆盖率,被测试到的代码行数占所有代码行数的比例

22. 基准测试

通过测试CPU和内存的效率问题,评估被测试代码的性能

测试函数以Benchmark开头
接收一个指向Benchmark类型的指针作为唯一参数
没有返回值

你可能感兴趣的:(2019-12-22-Go语言入门笔记)