Golang之protobuf、WaitGroup、benchmark


Protobuf基础使用

使用:安装protoc、protoc-gen-go

定义消息类型

syntax = "proto3";
package main;

option go_package="./;main";


message Student {
  string name = 1;
  bool male = 2;
  repeated int32 scores = 3; // []int类型
}

生成对应的go代码

protoc --go_out=. student.proto

测试使用

package main

import (
    "fmt"
    "github.com/golang/protobuf/proto"
    "log"
)

func main() {
    student := &Student{
        Name:   "hqinglau",
        Male:   true,
        Scores: []int32{1, 2, 3},
    }

    data, err := proto.Marshal(student)
    if err != nil {
        log.Fatal("marshaling error: ", err)
    }

    var newStudent Student
    err = proto.Unmarshal(data, &newStudent)
    if err != nil {
        log.Fatal("unmarshaling error: ", err)
    }

    fmt.Printf("%+v", student)
    fmt.Printf("%+v", newStudent)
}

修饰符

required:必须字段,不写默认required

optional:可选字段,更新可以用这个,不影响老版本

repeated:数组概念

更新消息

message Student {
  string name = 1;
  bool male = 2;
  repeated int32 scores = 3; // []int类型
  optional string email = 4;
}

老代码仍可运行,也可写新代码:

package main

import (
    "fmt"
    "github.com/golang/protobuf/proto"
    "log"
)

func main() {
    email := "hqinglau@gmail.com"
    student := &Student{
        Name:   "hqinglau",
        Male:   true,
        Scores: []int32{1, 2, 3},
        Email:  &email,
    }

    data, err := proto.Marshal(student)
    if err != nil {
        log.Fatal("marshaling error: ", err)
    }

    var newStudent Student
    err = proto.Unmarshal(data, &newStudent)
    if err != nil {
        log.Fatal("unmarshaling error: ", err)
    }

    fmt.Printf("%+v\n", student)
    fmt.Printf("%+v\n", newStudent.GetEmail())
}

//name:"hqinglau"  male:true  scores:1  scores:2  scores:3  email:"hqinglau@gmail.com"
//hqinglau@gmail.com

WaitGroup使用陷阱

正确示例

package waitGroup

import (
    "fmt"
    "sync"
    "testing"
)

func worker(msg string)  {
    fmt.Printf("worker do %s\n", msg)
}

func TestWorker(t *testing.T)  {
    waitGroup := &sync.WaitGroup{}
    for i:=0; i<3; i++ {
        waitGroup.Add(1)
        go func(k int) {
            defer  waitGroup.Done()
            worker(fmt.Sprintf("task %d", k))
        }(i)
    }
    waitGroup.Wait()
    fmt.Println("main exit")
}

错误示例

错误代码:

var wg sync.WaitGroup

func worker(msg string) {
    wg.Add(1)
    defer wg.Done()
    fmt.Printf("worker do %s\n", msg)
}

func main() {
    go worker("task 1")
    go worker("task 2")
    go worker("task 3")
    fmt.Println("waiting")
    wg.Wait()
    fmt.Println("main exit")
}

错误原因:

WaitGroup调用Wait时,可能就没进入协程,没Add呢,直接就退出了。

Benchmark基准测试

func BenchmarkSprint(b *testing.B) {   // 测试基准的函数需以Benchmark开头, 参数testing.B
    b.ResetTimer()  // 重置计时器,防止前面有代码,造成干扰

    // b.N 从N开始,递增(越到后面增加越快),如果用例能够在1s内结束,就会增加
    for i:=0; i<b.N; i++ {
        fmt.Sprint(i)
    }
}

//goos: windows
//goarch: amd64
//cpu: Intel(R) Core(TM) i3-9100F CPU @ 3.60GHz
//BenchmarkSprint
//BenchmarkSprint-4       13386700            86.35 ns/op
//PASS

数组和切片

go 数组是值类型,复制会复制整个数组。

func TestArray(t *testing.T) {
    a := [...]int{1,2,3}
    b := a
    b[0] = 999
    fmt.Println(b)  // [999 2 3]
    fmt.Println(a)  // [1 2 3]
}

切片陷阱

例如有个很大的切片,但是现在我只需要其中的最后两位,此时如果直接用切片origin[len(origin)-2:],原切片的引用还会保留,原切片不能释放内存;此时若用复制,则可以释放原切片内存。

t.Helper() 的作用是标记一个函数为测试辅助函数,这样的话,该函数将不会在测试日志输出文件名和行号信息时出现。当 go testing 系统在查找调用栈帧的时候,通过 Helper 标记过的函数将被略过,因此这有助于找到更确切的调用者及其相关信息。

这个函数的用途在于削减日志输出中(尤其是在打印调用栈帧信息时)的杂音。

func lastTwoBySlice(origin []int) []int {
    return origin[len(origin)-2:]
}

func lastTwoByCopy(origin []int) []int {
    ret := make([]int,2)
    copy(ret, origin)
    return ret
}

func generateRandomSlice(n int) []int {
    ret := make([]int, n)
    for i:=0;i<n;i++ {
        ret[i] = rand.Int()
    }
    return ret
}

func printMem(t *testing.T)  {
    t.Helper()
    var rtm runtime.MemStats
    runtime.ReadMemStats(&rtm)
    t.Logf("%.2f MB", float64(rtm.Alloc)/1024./1024.)
}

func testLastChars(t *testing.T, f func([]int) []int) {
    t.Helper()
    ans := make([][]int, 0)
    for k := 0; k < 100; k++ {
        origin := generateRandomSlice(128 * 1024) // 1M
        ans = append(ans, f(origin))
        runtime.GC()
    }
    printMem(t)
    _ = ans
}

参考文献

你是否因使用姿势不当,而在 WaitGroup 栽了跟头?

切片(slice)性能及陷阱

Go Protobuf 简明教程