【笔记】Go Coding In Go Way

opensource.googleblog.com/2020/08/new...

2020年google工程师,分享了团队或组织如何使用Go的故事。从2009年11月首次向公众发布Go,到现在go在Google内部和外部都得到了广泛使用,其网络并发和软件工程方法对其他语言及其工具产生了显着影响。

在Google内部,Go用于生产用途最早出现在2011年,那一年我们在App Engine上发布了Go,并开始通过Vitess为YouTube数据库提供流量代理服务。当时,Vitess的作者告诉我们,Go正是他们所需要的那种语言- 简单网络编程,高效执行和快速开发的结合 ,并且如果不使用Go,他们可能根本无法构建这个系统。第二年,Go取代Sawzall被用作Google的搜索质量分析。用Go语言编写的服务器每天可为数百万用户提供更快的页面加载速度和更低的数据使用率。当然,Go还推动了Google在2014年开发和推出Kubernetes

所以少年,现在来加入Gopher吧!

1. 包设计指南

Go包是一个逻辑上独立的单元,是Go的基本功能单元,用来做功能边界的划分。这些基本功能单元的累加就构成了Go应用,因此Go应用的本质就是一组Go包的集合。

Go包还是编译时的最小单位。也就是说,Go编译器编译代码时会以包为单位进行编译,而不是以文件为单位。这意味着一个包中的所有源文件都将被编译成一个单独的目标文件,而不是多个目标文件。

Go包之间不能存在循环依赖,由于无环,包可以被单独编译,也可以并行编译;已编译的Go包的目标文件中记录了其所依赖包的导出符号信息。Go编译器在读取该目标文件时不需进一步读取其依赖包的目标文件。

1.1. Go包设计思路

1.1.1. 自然内聚

比如下面有几个功能函数:Add、Subtract、Multiply、Divide、Sum、Average、Histogram,我们如何为这几个函数选桶呢?

一种不那么内聚的作法是将上述所有函数都放入math包;而更自然内聚一些的作法则是将Add、Subtract、Multiply、Divide放入math包,而将Sum、Average、Histogram等放入stats包(statistics,统计学)。

在功能选桶过程中,越符合常人思维,就越自然,可读性和可理解性大概率就越好。

1.1.2. 单一职责原则(SRP)

go 复制代码
graph/
    - graph.go // 定义Graphics接口
    - rectangle/
        - rectangle.go // 定义Rectangle类型和其Draw方法
    - circle/
        - circle.go // 定义Circle类型和其Draw方法
    - triangle/
        - triangle.go // 定义Triangle类型和其Draw方法

1.1.3. 开放-关闭原则(OCP)

扩展:可以在graph包下面添加一个square包

组合:像io包中的Reader、Writer接口被组合到ReaderCloser、ReadWriteCloser

1.1.4. 接口隔离原则(ISP)

函数不应该被迫依赖于它们不需要的参数,不多不少,刚刚好

1.1.5. 依赖倒置原则(DIP)

1.2. 单包设计

1.2.1. 包名

给Go包起名字首先要注意简单达意,比如标准库的fmt、io、os等,并且包名按惯例应该与其目录名一致;

其次,同一工程内部包名最好是唯一的,避免工程内部出现名字碰撞

最后,如果为了简洁而失去了包名的内聚性的内涵(功能和作用),比如utils、common这些名字基本无法表达包究竟担负的职责,那么莫不如将包名加长一些,点缀上能达意的单词,比如printutil而不仅仅是util。

1.2.2. 最小暴露面积

在设计包时,应该使用接口来建立抽象,而不是直接暴露实现。这样可以将实现细节隐藏起来,只暴露接口,从而提高代码的安全性和稳定性。

在设计包时,也应该尽量减少暴露给外部的接口数量。

在设计包时,应该使用非导出方法和变量来封装包内部的实现细节。

1.2.3. 警惕全局变量导出

1.2.4. main包简洁

它应该只负责装配其他包,调用其他函数或模块,不应该包含过多的代码逻辑。这样可以提高代码的可读性和可维护性。如果要对main函数进行单测的话,那么可以将main函数的逻辑放置到另外一个函数中,比如run,然后对run函数进行详尽的测试。

1.2.5. 接口类型定义应放在与使用者更近的地方

将接口类型定义放在与使用者更近的地方,可以使代码更加清晰和易于理解。使用者可以直接看到接口类型定义,了解接口类型的作用和使用方法。但注意:这并非是绝对的规则。

2. go channel

2.1. CSP模型

Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。

Golang,其实只用到了 CSP 的很小一部分,即理论中的 Process/Channel(对应到语言中的 goroutine/channel) :这两个并发原语之间没有从属关系, Process 可以订阅任意个 Channel,Channel 也并不关心是哪个 Process 在利用它进行通信;Process 围绕 Channel 进行读写,形成一套有序阻塞和可预测的并发模型。

Channel的内部实现使用了互斥锁,用于在Goroutine间传递数据,示意图如下:

仅在一个单独的Goroutine中使用channel毫无意义,并且容易引发错误。

nil channel是channel的默认零值,向nil channel写数据,或从nil channel中读数据,会永久阻塞(block),但不会panic。关闭(close)一个nil channel会触发pannic。

2.2. 基本语法

go 复制代码
c := make(chan bool) //创建一个无缓冲的bool型Channel

c <- x           //向一个Channel发送一个值
<- c          //从一个Channel中接收一个值
x = <- c      //从Channel c接收一个值并将其存储到x中
x, ok = <- c  //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

2.3. 场景用法

2.3.1. 等待一个事件

go 复制代码
func TestNextTokenCase2(t *testing.T) {
	fmt.Println("Begin doing something!")
	c := make(chan bool)
	go func() {
		fmt.Println("Doing something...")
        
		close(c) 
        // 关闭会导致N个goroutine都会收到关闭的信号
        // 也可以是c<-true
	}()
	<-c
	fmt.Println("Done!")
}

2.3.2. Select终结workers

我们在使用select时很少只是对其进行一次evaluation,我们常常将其与for {}结合在一起使用,并选择适当时机从for{}中退出。

go 复制代码
func worker(die chan bool, index int) {
	fmt.Println("Begin: This is Worker:", index)
	for {
		select {
		//case xx:
		//做事的分支
		case <-die:
			fmt.Println("Done: This is Worker:", index)
			return
		}
	}
}

func TestNextTokenCase2(t *testing.T) {
	die := make(chan bool)

	for i := 1; i <= 100; i++ {
		go worker(die, i)
	}

	time.Sleep(time.Second * 5)
	close(die)
	select {} //deadlock we expected
}
  • 已经close的unbuffered channel上执行读操作,回返回channel对应类型的零值,比如bool型channel返回false,int型channel返回0。但向close的channel写则会触发panic。不过无论读写都不会导致阻塞。
  • 带缓冲的channel略有不同。尽管已经close了,但我们依旧可以从中读出关闭前写入的3个值。第四次读取时,则会返回该channel类型的零值。向这类channel写入操作也会触发panic。

2.3.3. range channel

go 复制代码
func generator(strings chan string) {
    time.Sleep(2 * time.Second)
    strings <- "Five hour's New York jet lag"
    strings <- "and Cayce Pollard wakes in Camden Town"
    strings <- "to the dire and ever-decreasing circles"
    strings <- "of disrupted circadian rhythm."
    //close(strings) close会赋值为nil
}

func TestNextTokenCase2(t *testing.T) {
    strings := make(chan string)
    go generator(strings)
    for s := range strings { // 一直读取,除非channel关闭
        fmt.Printf("%s\n", s)
    }
    fmt.Printf("\n")
}

2.3.4. Timers

go 复制代码
1、超时机制Timeout

带超时机制的select是常规的tip,下面是示例代码,实现30s的超时select:

func worker(start chan bool) {
        timeout := time.After(30 * time.Second)
        for {
                select {
                     // ... do some stuff
                case <- timeout:
                    return
                }
        }
}

2、心跳HeartBeart

与timeout实现类似,下面是一个简单的心跳select实现:

func worker(start chan bool) {
        heartbeat := time.Tick(30 * time.Second)
        for {
                select {
                     // ... do some stuff
                case <- heartbeat:
                    //... do heartbeat stuff
                }
        }
}

2.3.5. 等待所有协程退出

go 复制代码
func TestNextTokenCase2(t *testing.T) {
	var ch chan int = make(chan int, 10)
	var wg sync.WaitGroup
	var balance = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	j := 0

	for m := 0; m < len(balance); m++ {
		ch <- balance[m]
		wg.Add(1)
	}
	for n := 0; n < len(balance); n++ {
		go func() {
			s := <-ch
			fmt.Println("s ", s)
			if s == 4 || s == 7 {
				j++
			}
			defer wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println("j ", j)
	fmt.Println("执行完毕")
}

3. signal处理,优雅退出服务

更优雅的重启看这里

github.com/facebookarc...

Golang 的系统信号处理主要涉及os包、os.signal包以及syscall包。其中最主要的函数是signal包中的Notify函数:

func Notify(c chan<- os.Signal, sig ...os.Signal)

该函数会将进程收到的系统Signal转发给channel c。转发哪些信号由该函数的可变参数决定,如果你没有传入sig参数,那么Notify会将系统收到的所有信号转发给c。如果你像下面这样调用Notify:

signal.Notify(c, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2)

则Go只会关注你传入的Signal类型,其他Signal将会按照默认方式处理,大多都是进程退出。因此你需要在Notify中传入你要关注和处理的Signal类型,也就是拦截它们,提供自定义处理函数来改变它们的行为。

go 复制代码
// +build go1.8

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        c.String(http.StatusOK, "Welcome Gin Server")
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: router.Handler(),
    }

    go func() {
        // service connections
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    // Wait for interrupt signal to gracefully shutdown the server with
    // a timeout of 5 seconds.
    quit := make(chan os.Signal, 1)
    // kill (no param) default send syscall.SIGTERM
    // kill -2 is syscall.SIGINT
    // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Shutdown Server ...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:", err)
    }
    // catching ctx.Done(). timeout of 5 seconds.
    select {
        case <-ctx.Done():
        log.Println("timeout of 5 seconds.")
    }
    log.Println("Server exiting")
}

4. Go与C语言的互操作

Go有强烈的C背景,除了语法具有继承性外,其设计者以及其设计目标都与C语言有着千丝万缕的联系。在Go与C语言互操作(Interoperability)方面,Go更是提供了强大的支持。尤其是在Go中使用C,你甚至可以直接在Go源文件中编写C代码。

在如下一些场景中,可能会涉及到Go与C的互操作:

1、提升局部代码性能时,用C替换一些Go代码。C之于Go,好比汇编之于C。

2、嫌Go内存GC性能不足,自己手动管理应用内存。

3、实现一些库的Go Wrapper。比如Oracle提供的C版本OCI,但Oracle并未提供Go版本的以及连接DB的协议细节,因此只能通过包装C OCI版本的方式以提供Go开发者使用。

4、Go导出函数供C开发者使用(目前这种需求应该很少见)。

github.com/andreiavram...

go 复制代码
func main() {
    // Numbers
    fmt.Println("\nNumbers")

    a := 1
    b := 2
    sum := int(C.sum(C.int(a), C.int(b)))
    fmt.Print(sum, "\n\n")

    // Get string
    fmt.Println("Get string")
    getString := C.GoString(C.get_string())
    fmt.Println(getString)

    //C.GoBytes()
    //这是 Go 语言提供的一个函数,用于将 C 语言中的数据转换为 Go 语言的字节数组([]byte)。
    //unsafe.Pointer(C.get_string())
    //C.get_string() 是一个 C 语言函数,它返回一个 C 语言字符串指针。
    //unsafe.Pointer() 是 Go 语言提供的一个可以将任意指针转换为 unsafe.Pointer 类型的函数。这种转换可以让 Go 语言代码访问 C 语言代码中的指针。
    //24
    //这是一个数字,表示要从 C 语言字符串中获取的字节长度。w
    stringBytes := C.GoBytes(unsafe.Pointer(C.get_string()), 24)
    fmt.Println(stringBytes[0:bytes.Index(stringBytes, []byte{0})])
    fmt.Println()

    // Send string
    fmt.Println("Send string")
    str := "lorem ipsum"
    cStr := C.CString(str)
    C.print_string(cStr)
    C.free(unsafe.Pointer(cStr))
    fmt.Println()

    // Send bytes
    fmt.Println("Send byte array")
    data := []byte{1, 4, 2}
    cBytes := (*C.uchar)(unsafe.Pointer(&data[0]))
    cBytesLength := C.size_t(len(data))
    fmt.Print("bytes: ")
    C.print_buffer(cBytes, cBytesLength)
    fmt.Println()

    // Struct
    fmt.Println("Get and pass struct")
    point := C.struct_point{}
    point.x = 0
    point.y = 2
    fmt.Println(point)
    fmt.Print(C.point_diff(point), "\n\n")

    // Arbitrary data: unsafe.Pointer to void pointer
    fmt.Println("Pass void pointer")
    C.pass_void_pointer(unsafe.Pointer(&point.y))
    fmt.Println()

    // Enum
    fmt.Println("Access enum")
    fmt.Print(C.enum_status(Pending) == C.PENDING, C.PENDING, C.DONE, "\n\n")

    // Callback
    fmt.Println("Pass callback")
    c := registerCallback(evenNumberCallback, nil)
    C.generate_numbers(5, c)
    unregisterCallback(c)

    // Callback with params
    user := User{
        Username: "johndoe",
    }
    cWithParams := registerCallback(userCallback, unsafe.Pointer(&user))
    C.user_action(cWithParams)
    unregisterCallback(cWithParams)
    fmt.Println(user)
}
.PHONY: all c go run env

TESTLIBPATH="./ctestlib"

all: c go run

env:
	docker build --tag cgo .
	docker run --rm -ti -v $(shell pwd):/src cgo

c:
    // 打开告警,示告警为错误,编译为动态链接库
	gcc -std=c99 -c -Wall -Werror -fpic -o ${TESTLIBPATH}/test.o ${TESTLIBPATH}/test.c
	gcc -std=c99 -shared -o ${TESTLIBPATH}/libtest.so ${TESTLIBPATH}/test.o

go:
	go build -o app *.go

run:
	./app


//任何出现${SRCDIR}字符串的地方,都会被替换为包含源文件的目录的绝对路径
//dlopen函数按指定模式打开指定的动态链接库文件。它有一个加载顺序:(1)RPATH,(2) LD_LIBRARY_PATH,(3)/etc/ld.so.cache 维护的so 列表,(4)/lib 和/usr/lib。
// -Wl: 将参数传递给链接器(Linker)

/*
#cgo CFLAGS: -I${SRCDIR}/ctestlib
#cgo LDFLAGS: -Wl,-rpath,${SRCDIR}/ctestlib
#cgo LDFLAGS: -L${SRCDIR}/ctestlib
#cgo LDFLAGS: -ltest

#include <test.h>
*/
import "C"
[root@ipsec-server cgo-examples]# make
gcc -std=c99 -c -Wall -Werror -fpic -o "./ctestlib"/test.o "./ctestlib"/test.c
gcc -std=c99 -shared -o "./ctestlib"/libtest.so "./ctestlib"/test.o
go build -o app *.go
./app

Numbers
3

Get string
string sent from C
[115 116 114 105 110 103 32 115 101 110 116 32 102 114 111 109 32 67]

Send string
string sent from Go: lorem ipsum

Send byte array
bytes: 142

Get and pass struct
{0 2}
-2

Pass void pointer
2

Access enum
true 0 1

Pass callback
odd number:  0
odd number:  2
odd number:  4
{johndoe 5}

5. 多平台交叉编译

Windows下编译Mac平台64位可执行程序:

ini 复制代码
SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build

Mac 下编译 Linux 和 Windows平台 64位 可执行程序:

ini 复制代码
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

Linux 下编译 Mac 和 Windows 平台64位可执行程序:

ini 复制代码
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
#!/usr/bin/bash 
archs=(amd64 arm64) 
for arch in ${archs[@]} 
do         
    env GOOS=linux GOARCH=${arch} go build -o prepnode_${arch} 
done

6. go测试技术

6.1. 表驱动测试

Golang的struct字面值(struct literals)语法让我们可以轻松写出表驱动测试。

go 复制代码
package strings_test
import (
    "strings"
    "testing"
)
func TestIndex(t *testing.T) {
    var tests = []struct {
        s   string
        sep string
        out int
    }{
        {"", "", 0},
        {"", "a", -1},
        {"fo", "foo", -1},
        {"foo", "foo", 0},
        {"oofofoofooo", "f", 2},
        // etc
    }
    for _, test := range tests {
        actual := strings.Index(test.s, test.sep)
        if actual != test.out {
            t.Errorf("Index(%q,%q) = %v; want %v",
         test.s, test.sep, actual, test.out)
        }
    }
}

$go test -v strings_test.go

=== RUN TestIndex

--- PASS: TestIndex (0.00 seconds)

PASS

ok command-line-arguments 0.007s

6.2. T结构

*testing.T参数用于错误报告:

t.Errorf("got bar = %v, want %v", got, want)

t.Fatalf("Frobnicate(%v) returned error: %v", arg, err)

t.Logf("iteration %v", i)

也可以用于enable并行测试(parallet test):
t.Parallel()

控制一个测试是否运行:

if runtime.GOARCH == "arm" {
t.Skip("this doesn't work on ARM")

}

6.3. http服务测试

github.com/spkane/outy...

scss 复制代码
// Exported variables for monitoring the server.
// These are exported via HTTP as a JSON object at /debug/vars.
var (
	hitCount       = expvar.NewInt("hitCount")
	pollCount      = expvar.NewInt("pollCount")
	pollError      = expvar.NewString("pollError")
	pollErrorCount = expvar.NewInt("pollErrorCount")
)

pollError.Set(err.Error())
pollErrorCount.Add(1)


func TestIsTagged(t *testing.T) {
	// Set up a fake "Google Code" web server reporting 404 not found.
	status := statusHandler(http.StatusNotFound)
	s := httptest.NewServer(&status)
	defer s.Close()

	if isTagged(s.URL) {
		t.Fatal("isTagged == true, want false")
	}

	// Change fake server status to 200 OK and try again.
	status = http.StatusOK

	if !isTagged(s.URL) {
		t.Fatal("isTagged == false, want true")
	}
}

func TestIntegration(t *testing.T) {
    ... ...

    status := statusHandler(http.StatusNotFound)
	ts := httptest.NewServer(&status)
	defer ts.Close()

    s := NewServer("1.x", ts.URL, 1*time.Millisecond)
    
    // Make first request to the server.
	r, _ := http.NewRequest("GET", "/", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, r)
	if b := w.Body.String(); !strings.Contains(b, "No.") {
		t.Fatalf("body = %s, want no", b)
	}

    ... ...
}

6.4. Mocks和fakes

通过在代码中使用interface,Go可以避免使用mock和fake测试机制。

例如,如果你正在编写一个文件格式解析器,不要这样设计函数:

func Parser(f *os.File) error

作为替代,你可以编写一个接受interface类型的函数:

func Parser(r io.Reader) error

和bytes.Buffer、strings.Reader一样,*os.File也实现了io.Reader接口。

6.5. 子进程测试

scss 复制代码
func Crasher() {
    fmt.Println("Going down in flames!")
    os.Exit(1)
}

为了测试上面的代码,我们将测试程序本身作为一个子进程进行测试:

func TestCrasher(t *testing.T) {
    if os.Getenv("BE_CRASHER") == "1" {
        Crasher()
        return
    }
    cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
    cmd.Env = append(os.Environ(), "BE_CRASHER=1")
    err := cmd.Run()
    if e, ok := err.(*exec.ExitError); ok && !e.Success() {
        return
    }
    t.Fatalf("process ran with err %v, want exit status 1", err)
}

7. sync.Map

原生map无法进行并发写操作,否则就fatal error: concurrent map writes。于是gopher们便寻求其他路径。一种路径无非是基于原生map 包装出一个支持并发读写的自定义map类型,比如,最简单的方式就是用一把互斥锁(sync.Mutex) 同步各个goroutine对map内数据的访问;如果读多写少,还可以利用读写锁(sync.RWMutex) 来保护map内数据,减少锁竞争,提高并发读的性能。很多第三方map的实现原理也大体如此。

另外一种路径就是使用sync.Map

sync.Map 在读和删除两项性能基准测试上的数据都大幅领先使用sync.Mutex或RWMutex包装的原生map,仅在写入一项上存在一倍的差距。

为啥这么厉害,让我们看看它的实现流程:

go 复制代码
// sync.Map的核心数据结构
type Map struct {
    mu Mutex                        // 对 dirty 加锁保护,线程安全
    read atomic.Value                 // read 只读的 map,充当缓存层
    dirty map[interface{}]*entry     // 负责写操作的 map,当misses = len(dirty)时,将其赋值给read
    misses int                        // 未命中 read 时的累加计数,每次+1
}
// 上面read字段的数据结构
type readOnly struct {
    m  map[interface{}]*entry // 
    amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true
}

// 上面m字段中的entry类型
type entry struct {
    // value是个指针类型
    p unsafe.Pointer // *interface{}
}

7.1. 读取操作

read 中存在:直接返回

read 中不存在:

  • amended =false,则返回 false
  • amended = true,表示 dirty 中存在 read 没有的数据,加锁,从 dirty 中读取;misses++,当 misses== len(dirty):将 dirty 数据赋值给 read ,并且 read 内的 amended=false,清空 dirty

7.2. 写入操作

read 中存在 key,并且不是删除状态,则直接原子操作更新read即可

read 中存在 key,但read map中 entry 为 expunged 的状态,说明 dirty map 中不存在对应的 entry,因此dirty新增entry并将read的p设置为 nil 状态

read 中不存在 key 或 该 key 不可直接更新:

  • 上锁
  • read 中 存在 key:则同时更新 read 和 dirty
  • read 中 不存在 key,但是存在 dirty:则更新 dirty
  • 全新的 key,不存在 read 和 dirty:

首先判断如果 read.amended == false,则改为 true,并且判断 dirty==nil 时,将 read 加载到 dirty,最 后更新 dirty

  • 解锁

7.3. 删除操作

  • 存在 read 中,但entry 为 nil 或 expunged 状态,说明已经被删除,无需操作
  • 存在 read 中,则直接将元素置为 nil
  • 不存在 read 但是存在 dirty 中(read.amended == true),则直接delete删除
  • 两个 map 都不存在,直接返回

总结:

sync.Map 适合读多写少的场景,大量的读操作可以通过只读取read map 拥有极好的性能。

而如果写操作增加,首先会造成read map中读取miss增加,会回源到dirty中读取,且dirty可能会频繁整体更新为read,回源读取,整体更新的步骤都是阻塞上锁的。

其次,写操作也会带来dirty和 read中数据频繁的不一致,导致read中的数据需要同步到dirty中,这个过程在键值对比较多时,性能损耗较大且整个过程是阻塞的。

所以sync.Map 并不适合大量写操作。

8. 函数调用链跟踪

scss 复制代码
func trace() func() {
    pc, _, _, ok := runtime.Caller(1)
    if !ok {
        panic("not found caller")
    }

    fn := runtime.FuncForPC(pc)
    name := fn.Name()

    fmt.Printf("enter: %s\n", name)
    return func() { fmt.Printf("exit: %s\n", name) }
}

func C1() {
    defer trace()()
    D()
}

func D() {
    defer trace()()
}

原理:当执行流来带defer语句时,首先会对defer后面的表达式进行求值。trace函数会执行,输出函数入口信息,并返回一个"打印出口信息"的匿名函数。该函数在此并不会执行,而是被注册到函数A1的defer函数栈中,待A1函数执行结束后才会被弹出执行。也就是在A1结束后,会有一条函数的出口信息被输出。

defer官方介绍

  1. 参数预计算:每次defer语句执行时,会先计算出函数值和入参并保持起来;即,在执行defer语句时,延迟函数的入参已经确定,并保存了副本。
  2. 延迟调用时机:defer语句没有真正的被调用延迟函数延迟函数真正被调用是在主调函数返回前
    1. 如果主调函数有明确的return语句,则延迟函数将在所有返回值被设置(即return语句被执行)之后,主调函数返回之前被执行;
    2. 如果延迟函数nil,在延迟函数被调用时,而非执行defer语句时触发panic
  3. 执行顺序:按照defer语句的逆序执行。

9. 一些小Tips

  1. 当有疑问时,请遵循最小惊喜原则。争取做到一目了然。要直截了当,要简单,要显式,要无聊。
  2. 在Go中,一个常见的错误是先写了一些函数(比如:GetDataFromAPI),然后在考虑如何测试它时不知所措,如何写这样一个测试呢?它必须为函数的调用提供一个本地的TLS测试服务器,所以你需要一种方法来注入这种依赖。它要写数据到io.Writer,你同样需要为此注入一个模拟外部世界的本地依赖,比如:bytes.Buffer。 详见《go - 依赖注入(dependency injection)最通俗的讲解 - 个人文章 - SegmentFault 思否
  3. 你的库没有权利终止用户的程序。不要在你的包中调用像os.Exit、log.Fatal、panic这样的函数,这不是你能决定的
  4. 通过强迫用户传递给你一个文件,你限制了他们对你的库的使用。通过接受一个接口(如 io.Writer)来代替,你将打开新的可能性,包括尚未被创造的类型,后续它们仍然可以满足(接口) ,可以与你的代码io.Writer一起工作。为什么要 "返回结构体"?好吧,假设你返回一些接口类型。这极大地限制了用户对该值的操作(他们能做的就是调用其上的方法)。即使他们事实上可以用底层的具体类型做他们需要做的事情,他们也必须先用类型断言来解包它。换句话说,这就是额外的文书工作(应该避免)。另一种避免限制用户选择的方法是不要使用只有当前Go版本才有的功能。相反,考虑至少支持最近两个主要的Go版本:有些人不能立即升级。
  5. 行良好边界的方法是始终检查错误。如果你不这样做,无效的数据可能会泄露进来。
  6. 你不要盲目地遵从诫命,而要自己思考

10. grpc 测试

grpc-go项目在test下提供了bufconn包,可以帮助我们像httptest那样建立用于测试的"虚拟gRPC服务器"

go 复制代码
// grpc-test-examples/grpctest/server_with_buffconn_test.go

package main

import (
    "context"
    "log"
    "net"
    "testing"

    pb "demo/mygrpc"

    "google.golang.org/grpc"
    "google.golang.org/grpc/test/bufconn"
)

func newGRPCServer(t *testing.T) (pb.MyServiceClient, func()) {
    // 创建 bufconn.Listener 作为服务器的监听器
    listener := bufconn.Listen(1024 * 1024)

    // 创建 gRPC 服务器
    srv := grpc.NewServer()

    // 注册服务处理程序
    pb.RegisterMyServiceServer(srv, &server{})

    // 在监听器上启动服务器
    go func() {
        if err := srv.Serve(listener); err != nil {
            t.Fatalf("Server failed to start: %v", err)
        }
    }()

    // 创建 bufconn.Dialer 作为客户端连接
    dialer := func(context.Context, string) (net.Conn, error) {
        return listener.Dial()
    }

    // 使用 DialContext 和 bufconn.Dialer 创建客户端连接
    conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(dialer), grpc.WithInsecure())
    if err != nil {
        t.Fatalf("Failed to dial server: %v", err)
    }

    // 创建客户端实例
    client := pb.NewMyServiceClient(conn)
    return client, func() {
        err := listener.Close()
        if err != nil {
            log.Printf("error closing listener: %v", err)
        }
        srv.Stop()
    }
}

// grpc-test-examples/grpctest/server_with_buffconn_test.go

func TestServerUnaryRPCWithBufConn(t *testing.T) {
    client, shutdown := newGRPCServer(t)
    defer shutdown()

    tests := []struct {
        name           string
        requestMessage *pb.RequestMessage
        expectedResp   *pb.ResponseMessage
    }{
        {
            name: "Test Case 1",
            requestMessage: &pb.RequestMessage{
                Message: "Test message",
            },
            expectedResp: &pb.ResponseMessage{
                Message: "Unary RPC response",
            },
        },
        // Add more test cases as needed
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            resp, err := client.UnaryRPC(context.Background(), tt.requestMessage)
            if err != nil {
                t.Fatalf("UnaryRPC failed: %v", err)
            }

            if resp.Message != tt.expectedResp.Message {
                t.Errorf("Unexpected response. Got: %s, Want: %s", resp.Message, tt.expectedResp.Message)
            }
        })
    }
}

11. golang国密

tjfoc/gmsm: GM SM2/3/4 library based on Golang (基于Go语言的国密SM2/SM3/SM4算法库)

12. 测试框架

GoConvey是另一个流行的Golang自动化测试工具,支持TDD(Test Driven Development)和BDD风格的测试。它提供了一个易于使用的Web界面,可以实时显示测试结果,并且能够自动监视代码更改并重新运行测试。

Go单测从零到溜系列---5.使用goconvey - 知乎

13. 守护进程

守护进程是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或循环等待处理某些事件的发生;它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机才随之一起停止运行;

守护进程一般都以root用户权限运行,因为要使用某些特殊的端口(1-1024)或者资源;

守护进程的父进程一般都是init进程,因为它真正的父进程在fork出守护进程后就直接退出了,所以守护进程都是孤儿进程,由init接管;

守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等

创建守护进程的过程:

1)fork()创建子进程,父进程exit()退出

这是创建守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。

2)在子进程中调用 setsid() 函数创建新的会话

在调用了fork()函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来。

3)再次 fork() 一个孙进程并让子进程退出

为什么要再次fork呢,假定有这样一种情况,之前的父进程fork出子进程以后还有别的事情要做,在做事情的过程中因为某种原因阻塞了,而此时的子进程因为某些非正常原因要退出的话,就会形成僵尸进程,所以由子进程fork出一个孙进程以后立即退出,孙进程作为守护进程会被init接管,此时无论父进程想做什么都随它了。

4)在孙进程中调用 chdir() 函数,让根目录 "/" 成为孙进程的工作目录

这一步也是必要的步骤,使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如"/mnt/usb")是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp,改变工作目录的常见函数是chdir。

5)在孙进程中调用 umask() 函数,设置进程的文件权限掩码为0

文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。

6)在孙进程中关闭任何不需要的文件描述符

同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

在上面的第2)步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。

7)守护进程退出处理

当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。

【Linux】守护进程( Daemon)的定义,作用,创建流程_守护进程:运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务。-CSDN博客

go还是别搞了,直接用systemd就行

14. 依赖中间件的单测写法

我们第一站就来到了testcontainers-go。testcontainers-go是一个Go语言开源项目,专门用于简化创建和清理基于容器的依赖项,常用于Go项目的单元测试、自动化集成或冒烟测试中。通过testcontainers-go提供的易于使用的API,开发人员能够以编程方式定义作为测试的一部分而运行的容器,并在测试完成时清理这些资源。

go 复制代码
// testcontainers/kafka_setup/kafka_test.go

package main

import (
    "context"
    "fmt"
    "testing"

    "github.com/testcontainers/testcontainers-go/modules/kafka"
)

func TestKafkaSetup(t *testing.T) {
    ctx := context.Background()

    kafkaContainer, err := kafka.RunContainer(ctx, kafka.WithClusterID("test-cluster"))
    if err != nil {
        panic(err)
    }

    // Clean up the container
    defer func() {
        if err := kafkaContainer.Terminate(ctx); err != nil {
            panic(err)
        }
    }()

    state, err := kafkaContainer.State(ctx)
    if err != nil {
        panic(err)
    }

    if kafkaContainer.ClusterID != "test-cluster" {
        t.Errorf("want test-cluster, actual %s", kafkaContainer.ClusterID)
    }
    if state.Running != true {
        t.Errorf("want true, actual %t", state.Running)
    }
    brokers, _ := kafkaContainer.Brokers(ctx)
    fmt.Printf("%q\n", brokers)
}

15. Uber Go语言编码规范

tonybai.com/2019/10/12/...

Uber是世界领先的生活出行服务提供商,也是Go语言的早期adopter,根据Uber工程博客的内容,大致可以判断出Go语言在Uber内部扮演了十分重要的角色。Uber内部的Go语言工程实践也是硕果累累,有大量Go实现的内部工具被Uber开源到github上,诸如被Gopher圈熟知的zapjaeger等。2018年年末Uber将内部的Go风格规范开源到github,经过一年的积累和更新,该规范已经初具规模,并受到广大Gopher的关注。本文是该规范的中文版本,并"夹带"了部分笔者的点评,希望对国内Gopher有所帮助。

  • 运行golint和go vet检查源码
  • go.dev/doc/effecti...
  • 当map或slice作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。
scss 复制代码
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// 这里我们修改trips[0],但不会影响到d1.trips
trips[0] = ...
  • 返回slices或maps,请注意用户对暴露内部状态的map或slice的修改。
go 复制代码
type Stats struct {
  sync.Mutex

  counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
  s.Lock()
  defer s.Unlock()

  result := make(map[string]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// snapshot现在是一个拷贝
snapshot := stats.Snapshot()
  • 使用defer做清理
  • 自定义err
go 复制代码
// package foo

type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func IsNotFoundError(err error) bool {
  _, ok := err.(errNotFound)
  return ok
}

func Open(file string) error {
  return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
  if foo.IsNotFoundError(err) {
    // handle
  } else {
    panic("unknown error")
  }
}
  • 建议在可能的地方添加上下文,以使您获得诸如"调用服务foo:连接被拒绝"之类的更有用的错误,而不是诸如"连接被拒绝"之类的模糊错误。在将上下文添加到返回的错误时,请避免使用" failed to"之类的短语来保持上下文简洁,这些短语会陈述明显的内容,并随着错误在堆栈中的渗透而逐渐堆积:
  • 原子操作
go 复制代码
type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // already running...
     return
  }
  // start the Foo
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}
  • 将原语转换为字符串或从字符串转换时,strconv速度比fmt快。
  • 不要反复从固定字符串创建字节slice。相反,请执行一次转换并捕获结果。
css 复制代码
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}

BenchmarkGood-4  500000000   3.25 ns/op
  • 减少嵌套
go 复制代码
for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}
  • 对于未导出的顶层常量和变量,使用_作为前缀
ini 复制代码
// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)
  • 缩小变量作用域
go 复制代码
if err := ioutil.WriteFile(name, data, 0644); err != nil {
    return err
}
  • 函数调用中的裸参数可能会损害可读性。当参数名称的含义不明显时,请为参数添加C样式注释(/* ... */)。
arduino 复制代码
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)
  • 如果你为Printf-style函数声明格式字符串,请将格式化字符串放在外面,并将其设置为const常量。这有助于go vet对格式字符串执行静态分析。
  • 核心测试逻辑重复时,将表驱动测试与子测试一起使用,以避免重复代码。
go 复制代码
tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  // ...
}
  • 功能可选
go 复制代码
type options struct {
  timeout time.Duration
  caching bool
}

// Option overrides behavior of Connect.
type Option interface {
  apply(*options)
}

type optionFunc func(*options)

func (f optionFunc) apply(o *options) {
  f(o)
}

func WithTimeout(t time.Duration) Option {
  return optionFunc(func(o *options) {
    o.timeout = t
  })
}

func WithCaching(cache bool) Option {
  return optionFunc(func(o *options) {
    o.caching = cache
  })
}

// Connect creates a connection.
func Connect(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    timeout: defaultTimeout,
    caching: defaultCaching,
  }

  for _, o := range opts {
    o.apply(&options)
  }

  // ...
}

// Options must be provided only if needed.

db.Connect(addr)
db.Connect(addr, db.WithTimeout(newTimeout))
db.Connect(addr, db.WithCaching(false))
db.Connect(
  addr,
  db.WithCaching(false),
  db.WithTimeout(newTimeout),
)

16. go内存分配

tonybai.com/2020/03/10/...

17. Go运行时调度器

tonybai.com/2020/03/21/...

18. 会导致数据竞争问题的一些代码编程示例

tonybai.com/2022/06/21/...

我们知道:并发程序不好开发,更难于调试。并发是问题的滋生地,即便Go内置并发并提供了基于CSP并发模型的并发原语(goroutine、channel和select),实际证明,现实世界中,Go程序带来的并发问题并没有因此减少(手动允悲)。 "没有银弹"再一次应验

常见的数据竞争模式都有哪些?

  1. 闭包问题

Go闭包对其包裹函数中的变量的捕捉方式都是通过引用的方式.

这第一个例子中,每次循环都基于一个闭包函数创建一个新的goroutine,这些goroutine都捕捉了外面的循环变量job,这就在多个goroutine之间建立起对job的竞争态势。

例子2中闭包与变量声明作用域的结合共同造就了新goroutine中的err变量就是外部Foo函数的返回值err。这就会造成err值成为两个goroutine竞争的"焦点"。

2. 切片的"锅"

3. map并发

并发读写map会panic

4. 误传值惹的祸

Go推荐使用传值语义,因为它简化了逃逸分析,并使变量有更好的机会被分配到栈中,从而减少GC的压力。但有些类型是不能通过传值方式传递的,比如下面例子中的sync.Mutex:

5. 误用消息传递(channel)与共享内存

Go采用CSP的并发模型,而channel类型充当goroutine间的通信机制。虽然相对于共享内存,CSP并发模型更为高级,但从实际来看,在对CSP模型理解不到位的情况下,使用channel时也十分易错

这个例子中的问题在于Start函数启动的goroutine可能阻塞在f.ch的send操作上。因为,一旦ctx cancel了,Wait就会退出,此时没有goroutine再在f.ch上阻塞读,这将导致Start函数启动的新goroutine可能阻塞在"f.ch <- 1"这一行上。

6. sync.WaitGroup误用导致data race问题

我们看到例子中的代码将wg.Add(1)放在了goroutine执行的函数中了,而没有像正确方法那样,将Add(1)放在goroutine创建启动之前,这就导致了对WaitGroup内部计数器形成了数据竞争,很可能因goroutine调度问题,是的Add(1)在未来得及调用,从而导致Wait提前返回。

  1. Go内置单测框架,并支持并行测试(testing.T.Parallel())。但如若使用并行测试,则极其容易导致数据竞争问题

Reference

tonybai.com/articles/

www.cnblogs.com/zpcdbky/p/1...

interview.wzcu.com/Golang/CSP....

chenlujjj.github.io/go/go-mem-m...

blog.kennycoder.io/2021/01/17/...

ttps://www.zhihu.com/question/566542925/answer/3297306918

www.zhihu.com/question/56...

相关推荐
程序员清风7 分钟前
浅析Web实时通信技术!
java·后端·面试
java知路43 分钟前
springboot 基于google 缓存,实现防重复提交
spring boot·后端·缓存
A-bodgie1 小时前
Spring 中的 Environment 对象
java·后端·spring·servlet·springboot
桐桐桐1 小时前
Spring Security @PreAuthorize @PostAuthorize 权限控制
java·后端·spring
齐 飞2 小时前
MongoDB笔记02-MongoDB基本常用命令
前端·数据库·笔记·后端·mongodb
毕小宝2 小时前
若依管理系统使用已有 Nacos 部署流程整理
后端
2401_857636392 小时前
Spring Boot环境下的知识分类与检索
java·spring boot·后端
qq_172805592 小时前
GIN 反向代理功能
后端·golang·go
2401_857026232 小时前
Spring Boot框架下的知识管理与多维分类
spring boot·后端·oracle