GO语言基础与实战 | 青训营笔记

88 阅读12分钟

什么是Go语言

优点:

1、高性能、高并发

2、语法简单、学习曲线平缓

3、丰富的标准库

4、完善的工具链

5、静态链接

6、快速编译

7、跨平台

8、垃圾回收

应用场景:

1、C++不太适合在线Web业务

2、性能比较好,部署简单,学习成本低

3、内部RPC和HTTP框架的推广

变量

常见类型:

字符串(在go是内置类型)、整数、浮点数、布尔型、数组、切片、map

数组 vs 切片

数组是一个具有编号且长度固定的元素序列。

切片不同于数组,可以任意改变长度。可以用make来创建切片。

声明方式:

1、 var name string(可以不写,会自动去推导变量类型) = ""

2、变量 := 值

3、【数组】var a [5]int 或 b := [5]int{1, 2, 3, 4, 5}

​ 或 var twoD [2][3]int

4、【切片】s := make([]string, 3)

5、【map,完全无序】m := make(map[string]int) 或 m := map[string]int{"1": 1, "2": 2}

基本语法:

1、range

对于一个slice或者一个map的话,可以用range来快速遍历。range遍历的时候,对于数组会返回两个值:索引和值。如果我们不需要索引的话,可以用下划线来忽略

package main
​
import "fmt"func main() {
​
    m := map[string]string{"a": "A", "b": "B"}
    for k, v := range m {
        fmt.Println(k, v)
    }
​
    // 只获取key
    for k := range m {
        fmt.Println(k)
    }
​
    // 忽略key,只获取value
    for _, v := range m {
        fmt.Println(v)
    }
}

2、函数

变量类型后置。Golang里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真的返回结果,第二个是错误信息。


func add(a int, b int) int {
    return a + b
}

3、指针

go里的指针的一个主要用途就是对于传入参数进行修改。

入参类型要写成指针类型。调用的时候要加个&符号。


package main
​
import "fmt"func add(n *int) {
    *n += 2
}
​
func main() {
    n := 5
    add(&n)
    fmt.Println(n) // 7
}

3、结构体

结构体也支持指针,能实现结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销


package main
import "fmt"type user struct {
    name     string
    password string
}
​
func main() {
    a := user{name: "wang", password: "1024"}
    a.name = "jan"
    fmt.Println(a)
}

4、结构体方法

在普通函数的基础上,把第一个参数加上括号写到函数名称签名。

这个参数带不带指针决定修不修改结构体本身


package main
​
import "fmt"type user struct {
    name     string
    password string
}
​
func (u user) check(password string) bool {
    return u.password == password
}
​
func (u *user) reset(password string) {
    u.password = password
}
​
func main() {
    a := user{name: "wang", password: "1024"}
    fmt.Println(a.check("1024")) // true
​
    a.reset("2048")
    fmt.Println(a) // {wang 2048}
​
}

5、错误处理

在函数里面,可以在函数的返回值类型里面,后面加一个error,就代表这个函数可能会返回错误。

return的时候返回两个值,如果出现错误,那么返回 nil 和 error。如果没有的话,返回原本结果和nil。


package main
​
import (
    "errors"
    "fmt"
)
​
type user struct {
    name     string
    password string
}
​
func findUser(users []user, name string) (v *user, err error) {
    for _, u := range users {
        if u.name == name {
            return &u, nil
        }
    }
    return nil, errors.New("not found")
}
​
func main() {
    u, err := findUser([]user{{"wang", "1024"}}, "wang")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(u.name) // wang
}

6、字符串操作:

strings.Contains(dst, src)

strings.Count

strings.HasPrefix

strings.Index

strings.Join

strings.Repeat

strings.Replace

strings.Split

stings.ToUpper

...

7、字符串格式化打印

使用%v 打印任意类型的变量,而不需要区分数字字符串

使用 %+v 来打印详细信息,%#v则更详细


p := "hello"
fmt.Printf("s=%v\n", s) // s=hello

8、json处理

对于一个已有 的结构体,只要保证每个字段的第一个字母是大写,也就是公开字段,那么这个结构体就能用 JSON.marshaler 去序列化,变成一个 JSON 的字符串。

序列化后的字符串也能够用 JSON.unmarshaler 去反序列化到一个空的变量里面。


package main
​
import (
    "encoding/json"
    "fmt"
)
​
type userInfo struct {
    Name  string
    Age   int `json:"age"`
    Hobby []string
}
​
func main() {
    a := userInfo{"wang", 18, []string{"Golang", "TypeSrcipt"}}
    buf, err := json.Marshal(a)
    if err != nil {
        panic(err)
    }
    fmt.Println(buf) // [123 34 78 97 ... ]
    fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeSrcipt"]} 
​
    buf, err = json.MarshalIndent(a, "", "\t")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf))
    /*
        {                                                                         
            "Name": "wang",                                                   
            "age": 18,                                                        
            "Hobby": [                                                        
                    "Golang",                                                 
                    "TypeSrcipt"                                              
            ]                                                                 
        }  
    */
​
    var b userInfo
    err = json.Unmarshal(buf, &b)
    if err != nil {
        panic(err)
    }
​
    fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeSrcipt"}}
​
}
​

9、时间处理


package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    t := time.Date(2023, 5, 16, 1, 38, 0, 0, time.UTC)
    fmt.Println(t) // 2023-05-16 01:38:00 +0000 UTC                        
​
    format := t.Format("2006-01-02 15:04:05")
    fmt.Println(format) // 2023-05-16 01:38:00
​
    t2, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
    fmt.Println(t2) // 2022-03-27 01:25:36 +0000 UTC 
}

10、数字解析

可以用parseInt、parseFloat来解析一个字符串

可以用Atoi把一个十进制字符串转成数字。可以用itoA把数字转成字符串。

如果输入不合法,那么这些函数都会返回error

11、进程信息

可以用 os.argv 得到程序执行的时候指定的命令行参数。

可以用 os.getenv 来读取环境变量

示例工程

1、猜谜游戏


package main
​
import (
    "bufio"
    "fmt"
    "math/rand"
    "os"
    "strconv"
    "strings"
    "time"
)
​
func main() {
    maxNum := 100
    rand.Seed(time.Now().UnixNano())
    secretNum := rand.Intn(maxNum)
    fmt.Println("秘密数字是", secretNum)
​
    fmt.Println("请输入你的猜测数字")
    reader := bufio.NewReader(os.Stdin)
    for {
​
        // 读取一行输入
        input, err := reader.ReadString('\n')
        if err != nil {
            fmt.Println("输入时发生错误...", err)
            continue
        }
        // 去掉换行符
        input = strings.TrimSuffix(input, "\r\n")
​
        guess, err := strconv.Atoi(input)
        if err != nil {
            fmt.Println("输入不合法", err)
            continue
        }
        fmt.Println("你的猜测是 ", guess)
​
        if guess > secretNum {
            fmt.Println("你猜的数大了,再试一次")
        } else if guess < secretNum {
            fmt.Println("你猜的数小了,再试一次")
        } else {
            fmt.Println("猜测正确!牛逼")
            break
        }
    }
}

2、在线词典——抓包

两个工具网站:

curl命令转化为go代码

curlconverter.com/go/

json 转换为结构体:

oktools.net/json2go

完整代码:


package main
​
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
)
​
// 请求结构体
type DictRequest struct {
    TransType string `json:"trans_type"`
    Source    string `json:"source"`
    UserId    string `json:"user_id"`
}
​
// 返回结果结构体
type DictResponse struct {
    Rc   int `json:"rc"`
    Wiki struct {
    } `json:"wiki"`
    Dictionary struct {
        Prons struct {
            EnUs string `json:"en-us"`
            En   string `json:"en"`
        } `json:"prons"`
        Explanations []string      `json:"explanations"`
        Synonym      []string      `json:"synonym"`
        Antonym      []string      `json:"antonym"`
        WqxExample   [][]string    `json:"wqx_example"`
        Entry        string        `json:"entry"`
        Type         string        `json:"type"`
        Related      []interface{} `json:"related"`
        Source       string        `json:"source"`
    } `json:"dictionary"`
}
​
func query(word string) {
    client := &http.Client{}
    // var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
    request := DictRequest{TransType: "en2zh", Source: word}
    buf, err := json.Marshal(request) // buf是字节数组
    if err != nil {
        log.Fatal(err)
    }
​
    data := bytes.NewReader(buf)
    // 设置请求
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
    if err != nil {
        log.Fatal(err)
    }
​
    // 设置请求头
    req.Header.Set("authority", "api.interpreter.caiyunai.com")
    req.Header.Set("accept", "application/json, text/plain, */*")
    req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
    req.Header.Set("app-name", "xy")
    req.Header.Set("content-type", "application/json;charset=UTF-8")
    req.Header.Set("device-id", "7eb20fa47949e9e7bb8f98b5b8b15f2f")
    req.Header.Set("origin", "https://fanyi.caiyunapp.com")
    req.Header.Set("os-type", "web")
    req.Header.Set("os-version", "")
    req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
    req.Header.Set("sec-ch-ua", `"Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"`)
    req.Header.Set("sec-ch-ua-mobile", "?0")
    req.Header.Set("sec-ch-ua-platform", `"Windows"`)
    req.Header.Set("sec-fetch-dest", "empty")
    req.Header.Set("sec-fetch-mode", "cors")
    req.Header.Set("sec-fetch-site", "cross-site")
    req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
    req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
​
    // 发起请求
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
​
    // 读取响应
    bodyText, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    //fmt.Printf("%s\n", bodyText)
    if resp.StatusCode != 200 {
        log.Fatal("bad StatusCode", resp.StatusCode, "body", string(bodyText))
    }
    var dictResponse DictResponse
    err = json.Unmarshal(bodyText, &dictResponse)
    if err != nil {
        log.Fatal(err)
    }
    //fmt.Printf("%#v\n", dictResponse)
    // 打印结果
    fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
    for _, item := range dictResponse.Dictionary.Explanations {
        fmt.Println(item)
    }
}
​
func main() {
    if len(os.Args) != 2 {
        fmt.Println(os.Stderr, "输入参数个数不对")
        os.Exit(1)
    }
    word := os.Args[1]
    query(word)
}

3、Socks5 代理

Socks5 是个代理协议,但不能用来翻墙,且是明文传输。这个协议的用途是,比如某些企业的内网为了确保安全性,有严格的防火墙策略,但带来副作用是访问某些资源会很麻烦。Socks5相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多翻墙软件,最终暴露的也是一个Socks5协议的端口。


package main
​
import (
    "bufio"
    "context"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
)
​
const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04func main() {
    server, err := net.Listen("tcp", "127.0.0.1:1080")
    if err != nil {
        panic(err)
    }
    for {
        client, err := server.Accept()
        if err != nil {
            log.Printf("Accept failed %v", err)
            continue
        }
        go process(client)
    }
}
​
func process(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    err := auth(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    //log.Println("auth success")
    err = connect(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
}
​
// 协议第二步:认证
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
    // +----+----------+----------+
    // |VER | NMETHODS | METHODS  |
    // +----+----------+----------+
    // | 1  |    1     | 1 to 255 |
    // +----+----------+----------+
    // VER: 协议版本,socks5为0x05
    // NMETHODS: 支持认证的方法数量
    // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED
    // X’02’ USERNAME/PASSWORD// 先读版本号
    ver, err := reader.ReadByte()
    if err != nil {
        return fmt.Errorf("read ver failed:%w", err)
    }
    if ver != socks5Ver {
        return fmt.Errorf("not supported ver:%v", ver)
    }
    // 再读认证的方法数目
    methodSize, err := reader.ReadByte()
    if err != nil {
        return fmt.Errorf("read methodSize failed:%w", err)
    }
​
    //生成一个相同长度的slice,用 ReadFull 把它全部填充进去
    method := make([]byte, methodSize)
    _, err = io.ReadFull(reader, method)
    if err != nil {
        return fmt.Errorf("read method failed:%w", err)
    }
    log.Println("ver", ver, "method", method)
    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    // 返回一个 response,包含两个字段,一个 version 一个是 method
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}
​
// 第三步,实现请求阶段
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
    // +----+-----+-------+------+----------+----------+
    // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER 版本号,socks5的值为0x05
    // CMD 0x01表示CONNECT请求
    // RSV 保留字段,值为0x00
    // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
    //   0x01表示IPv4地址,DST.ADDR为4个字节
    //   0x03表示域名,DST.ADDR是一个可变长度的域名
    // DST.ADDR 一个可变长度的值
    // DST.PORT 目标端口,固定2个字节
​
    buf := make([]byte, 4)
    _, err = io.ReadFull(reader, buf)
    if err != nil {
        return fmt.Errorf("read header failed:%w", err)
    }
    ver, cmd, atyp := buf[0], buf[1], buf[3]
    if ver != socks5Ver {
        return fmt.Errorf("not supported ver:%v", ver)
    }
    if cmd != cmdBind {
        return fmt.Errorf("not supported cmd:%v", cmd)
    }
    addr := ""
    switch atyp {
    case atypeIPV4:
        _, err = io.ReadFull(reader, buf)
        if err != nil {
            return fmt.Errorf("read atyp failed:%w", err)
        }
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
    case atypeHOST:
        hostSize, err := reader.ReadByte()
        if err != nil {
            return fmt.Errorf("read hostSize failed:%w", err)
        }
        host := make([]byte, hostSize)
        _, err = io.ReadFull(reader, host)
        if err != nil {
            return fmt.Errorf("read host failed:%w", err)
        }
        addr = string(host)
    case atypeIPV6:
        return errors.New("IPv6: no supported yet")
    default:
        return errors.New("invalid atyp")
    }
    _, err = io.ReadFull(reader, buf[:2])
    if err != nil {
        return fmt.Errorf("read port failed:%w", err)
    }
    port := binary.BigEndian.Uint16(buf[:2])
​
    // 第四阶段:relay
    // 通过 net.Dial 建立一个 TCP 连接
    dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
    if err != nil {
        return fmt.Errorf("dial dst failed:%w", err)
    }
    defer dest.Close()
    log.Println("dial", addr, port)
​
    // +----+-----+-------+------+----------+----------+
    // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER socks版本,这里为0x05
    // REP Relay field,内容取值如下 X’00’ succeeded
    // RSV 保留字段
    // ATYPE 地址类型
    // BND.ADDR 服务绑定的地址
    // BND.PORT 服务绑定的端口DST.PORT
    _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    if err != nil {
        return fmt.Errorf("write failed: %w", err)
    }
​
    // 建立 浏览器和下游服务器的双向数据转发
    // 标准库的 io.copy 可以实现一个单向数据转发。双向转发的话,要启动两个 goroutine。
    // 通过 context机制,可以使得等待任意一个方向copy出错的时候再返回 connect函数
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
​
    go func() {
        _, _ = io.Copy(dest, reader)
        cancel()
    }()
    go func() {
        _, _ = io.Copy(conn, dest)
        cancel()
    }()
​
    // 最后等待 ctx.Done(),只要cancel 被调用,ctx.Done()就会立刻返回。
    // 在上面两个goroutine 里面调用一次 cancel 即可。
    <-ctx.Done()
    return nil
}

Socks5协议工作原理

浏览器访问网站时的工作流程:

首先是浏览器和 Socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP连接。这里可以分为四个阶段,握手阶段、认证阶段、请求阶段、relay阶段。

握手阶段:浏览器向 socks5 代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类,socks5 服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的话就代表不需要认证,返回其他类型的话就会开始认证流程。

认证阶段:略

请求阶段:认证通过后,浏览器会向 socks5 服务器发起请求。主要信息包括 版本号,请求的类型,一般主要是 connection请求,就代表服务器要和某个域名或者某个ip地址某个端口建立 tcp 连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。

relay阶段:此时浏览器会发送 正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上 代理服务器并不关心流量的细节,可以是 HTTP流量,也可以是其他 TCP 流量。

Go并发

1、并发 vs 并行

并发:多线程程序在一个核的cpu上运行

并行:多线程程序在多个核的cpu上运行

Go可以充分发挥多核优势,高效运行

2、Goroutine

协程 vs 线程

协程:用户态,轻量级线程,栈 KB 级别

线程:内核态,线程跑多个协程,栈 MB 级别

3、CSP(Communicating Sequential Processes)

提倡通过通信共享内存而不是通过共享内存而实现通信

4、Channel

make(chan 元素类型,[缓冲大小])

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int, 2)


package main
​
import (
    "fmt"
    "time"
)
​
func CalSquare() {
    // 生产者
    src := make(chan int)
    // 消费者,有缓冲。因为消费者的消费速度会慢一些,通过设置一定大小的缓冲,可以解决生产和消费速度带来的执行效率问题。
    dest := make(chan int, 3)
    go func() {
        defer close(src)
        for i := 0; i < 10; i++ {
            // 发送值i到Channel src中
            src <- i
        }
    }()
​
    go func() {
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()
​
    for i := range dest {
        //
        println(i)
    }
​
}
func main() {
    CalSquare()
}
​
/*
结果:
0
1 
4 
9 
16
25
36
49
64
81
*/

5、并发安全 Lock


var (
    // 共享内存
    x    int64
    lock sync.Mutex
)
​
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}
​
func addWithoutLock() {
    for i := 0; i < 2000; i++ {
        x += 1
    }
}
​
func main() {
    x = 0
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
    time.Sleep(time.Second)
    println("不加锁", x)
​
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    println("加锁", x)
}
​
/*
结果:
不加锁 8958
加锁 10000
*/

6、WaitGroup

Add(delta int)

Done()

Wait()

开启协程+1;执行结束-1;主协程阻塞直到计数器为0。


// 快速打印 hello goroutine 0 ~ hello goroutine 4
func ManyGoWait() {
    var wg sync.WaitGroup
    wg.Add(5)
​
    for i := 0; i < 5; i++ {
        go func(j int) {
            defer wg.Done()
            println("hello goroutine" + fmt.Sprint(j))
        }(i)
    }
    wg.Wait()
}

资料:

线程同步 WaitGroup

Go Module

Go依赖管理

1、Go 依赖管理演进

GOPATH - Go Vendor - Go Module

2、环境变量 $GOPATH

bin 项目编译的二进制文件

pkg 项目编译的中间产物,加速编译

src 项目源码

GOPATH弊端:

场景:项目A 和 项目B 依赖于某一package的不同版本

问题:无法实现package多版本控制

3、Go Vendor

  • 项目目录下增加 vendor文件,所有依赖包副本形式放在 $ProjectRoot/vendor
  • 依赖寻址方式:vendor => GOPATH

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题【GOPATH的问题】。

Go Vendor弊端:

场景:vendor依旧不能很清晰的标识依赖的版本概念。

问题:

  • 无法控制依赖的版本
  • 更新项目又可能出现依赖冲突,导致编译出错

4、Go Module

  • 通过go.mod 文件管理依赖包版本
  • 通过 go get/go mod指令工具管理依赖包

终极目标:定义版本规则和管理项目依赖关系

5、依赖管理三要素

1、配置文件,描述依赖 go.mod

2、中心仓库管理依赖库 Proxy

3、本地工具 go get/mod

go mod init

作用:生成 go.mod 文件,此命令会在当前目录中初始化和创建一个新的go.mod文件,手动创建go.mod文件再包含一些module声明也等同该命令,而go mod init命令便是帮我们简便操作,可以帮助我们自动创建。

go mod tidy

作用:是把项目所需要的依赖添加到go.mod,并删除go.mod中,没有被项目使用的依赖

Go测试

测试分类:

回归测试:QA同事手动通过终端回归一些固定的主流程场景

集成测试:对系统功能维度做测试验证

单元测试:开发者对单独的函数、模块做功能验证

单元测试规则:

  • 所有测试文件以 _test.go结尾

  • func TestXxx(*testing.T)

    
    func TestPublishPost(t *testing.T) {
        ...
    }
    
  • 初始化逻辑放到 TestMain中

    
    func TestMain(m *testing.M) {
        // 测试前:数据装载、配置初始化等前置工作
        code := m.Run()
        // 测试后:释放资源等收尾工作
        os.Exit(code)
    }
    

Tips:

  • 一般覆盖率:50%~60%,较高覆盖率80%
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责

单元测试-Mock

快速为一个函数、方法打桩,不需再依赖本地文件


monkey.Patch(被打桩的方法, func() string {
    // 打桩。。。
})
defer monkey.Unpatch(被打桩的方法) // 卸载mock

基准测试

一般指测试一段程序的运行性能及耗费CPU的程度。

基本原理

让用户实现一个循环来调用被测算法,外层的函数传入要循环的次数,通过不断的尝试,使得 总耗时/循环次数 趋于稳定后就可以认为这个值就是在这个环境下被测算法的耗时,当然也可以通过指定循环次数等使得提前结束。

而外层函数就是通过 b.N把循环次数传递给我们的,所以在基准测试内应该要有个像这样的循环:

实例:随机选择执行服务器的基准测试


// 文件名:select_test.go
package Test
​
import (
    "math/rand"
    "testing"
)
​
var ServerIndex [10]int
​
func InitServerIndex() {
    for i := 0; i < 10; i++ {
        ServerIndex[i] = i + 100
    }
}
​
// 随机选择执行服务器
func Select() int {
    return ServerIndex[rand.Intn(10)]
}
​
func BenchmarkSelect(b *testing.B) {
    InitServerIndex()
    // 重新开始计时
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Select()
    }
}
​
// 个人意见只写并行测试就ok了,如果想非并行可以指定cpu数量为1,例如go test -bench . -cpu 1
func BenchmarkSelectParallel(b *testing.B) {
    InitServerIndex()
    b.ResetTimer()
    // RunParallel 多协程并发测试
    /*
        RunParallel will create GOMAXPROCS goroutines and distribute work among them.
    */
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // // The loop body is executed b.N times total across all goroutines.
            Select()
        }
    })
}