<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; Make</title>
	<atom:link href="http://tonybai.com/tag/make/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Wed, 15 Apr 2026 23:35:12 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Go编译的几个细节，连专家也要停下来想想</title>
		<link>https://tonybai.com/2024/11/11/some-details-about-go-compilation/</link>
		<comments>https://tonybai.com/2024/11/11/some-details-about-go-compilation/#comments</comments>
		<pubDate>Sun, 10 Nov 2024 22:13:45 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[CFLAGS]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CGO_ENABLED]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[DWARF]]></category>
		<category><![CDATA[expvar]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[getaddrinfo]]></category>
		<category><![CDATA[getnameinfo]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[glibc-static]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-sqlite3]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golist]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[inittask]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[ldflags]]></category>
		<category><![CDATA[LD_LIBRARY_PATH]]></category>
		<category><![CDATA[libresolv.so]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[NameResolution]]></category>
		<category><![CDATA[nm]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[sqlite-devel]]></category>
		<category><![CDATA[sqlite3]]></category>
		<category><![CDATA[TinyGo]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[内联]]></category>
		<category><![CDATA[动态链接]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[死码消除]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[静态链接]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4383</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/11/11/some-details-about-go-compilation 在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。 注：本文示例使用的环境为Go 1.23.0、Linux Kernel 3.10.0和CentOS 7.9。 1. Go编译默认采用静态链接还是动态链接？ 我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？ 很多人脱口而出：动态链接，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？ 我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点： $go env&#124;grep CGO_ENABLED CGO_ENABLED='1' 验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可： // go-compilation/main.go package main import "fmt" func main() { fmt.Println("hello, world") } 构建该程序： $go build -o helloworld-default main.go 之后，我们查看一下生成的可执行文件helloworld-default的文件属性： $file helloworld-default helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ldd [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/some-details-about-go-compilation-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/11/11/some-details-about-go-compilation">本文永久链接</a> &#8211; https://tonybai.com/2024/11/11/some-details-about-go-compilation</p>
<p>在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。</p>
<blockquote>
<p>注：本文示例使用的环境为<a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23/">Go 1.23.0</a>、Linux Kernel 3.10.0和CentOS 7.9。</p>
</blockquote>
<h2>1. Go编译默认采用静态链接还是动态链接？</h2>
<p>我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？</p>
<p>很多人脱口而出：<a href="https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/">动态链接</a>，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？</p>
<p>我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点：</p>
<pre><code>$go env|grep CGO_ENABLED
CGO_ENABLED='1'
</code></pre>
<p>验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可：</p>
<pre><code>// go-compilation/main.go

package main

import "fmt"

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>构建该程序：</p>
<pre><code>$go build -o helloworld-default main.go
</code></pre>
<p>之后，我们查看一下生成的可执行文件helloworld-default的文件属性：</p>
<pre><code>$file helloworld-default
helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-default
   不是动态可执行文件
</code></pre>
<p>我们看到，虽然CGO_ENABLED=1，但默认情况下，Go构建出的helloworld程序是静态链接的(statically linked)。</p>
<p>那么默认情况下，Go编译器是否都会采用静态链接的方式来构建Go程序呢？我们给上面的main.go添加一行代码：</p>
<pre><code>// go-compilation/main-with-os-user.go

package main

import (
    "fmt"
    _ "os/user"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>和之前的hello, world不同的是，这段代码多了一行<strong>包的空导入</strong>，导入的是os/user这个包。</p>
<p>编译这段代码，我们得到helloworld-with-os-user可执行文件。</p>
<pre><code>$go build -o helloworld-with-os-user main-with-os-user.go
</code></pre>
<p>使用file和ldd检视文件helloworld-with-os-user：</p>
<pre><code>$file helloworld-with-os-user
helloworld-with-os-user: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-os-user
    linux-vdso.so.1 =&gt;  (0x00007ffcb8fd4000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007fb5d6fce000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007fb5d6c00000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb5d71ea000)
</code></pre>
<p>我们看到：<strong>一行新代码居然让helloworld从静态链接变为了动态链接</strong>，同时这也是如何编译出一个hello world版的动态链接Go程序的答案。</p>
<p>通过nm命令我们还可以查看Go程序依赖了哪些C库的符号：</p>
<pre><code>$nm -a helloworld-with-os-user |grep " U "
                 U abort
                 U __errno_location
                 U fprintf
                 U fputc
                 U free
                 U fwrite
                 U malloc
                 U mmap
                 U munmap
                 U nanosleep
                 U pthread_attr_destroy
                 U pthread_attr_getstack
                 U pthread_attr_getstacksize
                 U pthread_attr_init
                 U pthread_cond_broadcast
                 U pthread_cond_wait
                 U pthread_create
                 U pthread_detach
                 U pthread_getattr_np
                 U pthread_key_create
                 U pthread_mutex_lock
                 U pthread_mutex_unlock
                 U pthread_self
                 U pthread_setspecific
                 U pthread_sigmask
                 U setenv
                 U sigaction
                 U sigaddset
                 U sigemptyset
                 U sigfillset
                 U sigismember
                 U stderr
                 U strerror
                 U unsetenv
                 U vfprintf
</code></pre>
<p>由此，我们可以得到一个结论，在默认情况下(CGO_ENABLED=1)，Go会尽力使用静态链接的方式，但在某些情况下，会采用动态链接。那么究竟在哪些情况下会默认生成动态链接的程序呢？我们继续往下看。</p>
<h2>2. 在何种情况下默认会生成动态链接的Go程序？</h2>
<p>在以下几种情况下，Go编译器会默认(CGO_ENABLED=1)生成动态链接的可执行文件，我们逐一来看一下。</p>
<h3>2.1 一些使用C实现的标准库包</h3>
<p>根据上述示例，我们可以看到，在某些情况下，即使只依赖标准库，Go 仍会在CGO_ENABLED=1的情况下采用动态链接。这是因为代码依赖的标准库包使用了C版本的实现。虽然这种情况并不常见，但<a href="https://pkg.go.dev/os/user">os/user包</a>和<a href="https://pkg.go.dev/net">net包</a>是两个典型的例子。</p>
<p>os/user包的示例在前面我们已经见识过了。user包允许开发者通过名称或ID查找用户账户。对于大多数Unix系统(包括linux)，该包内部有两种版本的实现，用于解析用户和组ID到名称，并列出附加组ID。一种是用纯Go编写，解析/etc/passwd和/etc/group文件。另一种是基于cgo的，依赖于标准C库（libc）中的例程，如getpwuid_r、getgrnam_r和getgrouplist。当cgo可用(CGO_ENABLED=1)，并且特定平台的libc实现了所需的例程时，将使用基于cgo的（libc支持的）代码，即采用动态链接方式。</p>
<p>同样，net包在名称解析(Name Resolution，即域名或主机名对应IP查找)上针对大多数Unix系统也有两个版本的实现：一个是纯Go版本，另一个是基于C的版本。C版本会在cgo可用且特定平台实现了相关C函数(比如getaddrinfo和getnameinfo等)时使用。</p>
<p>下面是一个简单的使用net包并采用动态链接的示例：</p>
<pre><code>// go-compilation/main-with-net.go

package main

import (
    "fmt"
    _ "net"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>编译后，我们查看一下文件属性：</p>
<pre><code>$go build -o helloworld-with-net main-with-net.go 

$file helloworld-with-net
helloworld-with-net: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-net
    linux-vdso.so.1 =&gt;  (0x00007ffd75dfd000)
    libresolv.so.2 =&gt; /lib64/libresolv.so.2 (0x00007fdda2cf9000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007fdda2add000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007fdda270f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fdda2f13000)
</code></pre>
<p>我们看到C版本实现依赖了libresolv.so这个用于名称解析的C库。</p>
<p>由此可得，当Go在默认cgo开启时，一旦依赖了标准库中拥有C版本实现的包，比如os/user、net等，Go编译器会采用动态链接的方式编译Go可执行程序。</p>
<h3>2.2 显式使用cgo调用外部C程序</h3>
<p>如果使用cgo与外部C代码交互，那么生成的可执行文件必然会包含动态链接。下面我们来看一个调用cgo的简单示例。</p>
<p>首先，建立一个简单的C lib：</p>
<pre><code>// go-compilation/my-c-lib

$tree my-c-lib
my-c-lib
├── Makefile
├── mylib.c
└── mylib.h

// go-compilation/my-c-lib/Makefile

.PHONY:  all static

all:
        gcc -c -fPIC -o mylib.o mylib.c
        gcc -shared -o libmylib.so mylib.o
static:
        gcc -c -fPIC -o mylib.o mylib.c
        ar rcs libmylib.a mylib.o

// go-compilation/my-c-lib/mylib.h

#ifndef MYLIB_H
#define MYLIB_H

void hello();
int add(int a, int b);

#endif // MYLIB_H

// go-compilation/my-c-lib/mylib.c

#include &lt;stdio.h&gt;

void hello() {
    printf("Hello from C!\n");
}

int add(int a, int b) {
    return a + b;
}
</code></pre>
<p>执行make all构建出动态链接库libmylib.so！接下来，我们编写一个Go程序通过cgo调用libmylib.so中：</p>
<pre><code>// go-compilation/main-with-call-myclib.go 

package main

/*
#cgo CFLAGS: -I ./my-c-lib
#cgo LDFLAGS: -L ./my-c-lib -lmylib
#include "mylib.h"
*/
import "C"
import "fmt"

func main() {
    // 调用 C 函数
    C.hello()

    // 调用 C 中的加法函数
    result := C.add(3, 4)
    fmt.Printf("Result of addition: %d\n", result)
}
</code></pre>
<p>编译该源码：</p>
<pre><code>$go build -o helloworld-with-call-myclib main-with-call-myclib.go
</code></pre>
<p>通过ldd可以看到，可执行文件helloworld-with-call-myclib是动态链接的，并依赖libmylib.so：</p>
<pre><code>$ldd helloworld-with-call-myclib
    linux-vdso.so.1 =&gt;  (0x00007ffcc39d8000)
    libmylib.so =&gt; not found
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007f7166df5000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007f7166a27000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f7167011000)
</code></pre>
<p>设置LD_LIBRARY_PATH(为了让程序找到libmylib.so)并运行可执行文件helloworld-with-call-myclib：</p>
<pre><code>$ LD_LIBRARY_PATH=./my-c-lib:$LD_LIBRARY_PATH ./helloworld-with-call-myclib
Hello from C!
Result of addition: 7
</code></pre>
<h3>2.3 使用了依赖cgo的第三方包</h3>
<p>在日常开发中，我们经常依赖一些第三方包，有些时候这些第三方包依赖cgo，比如<a href="https://github.com/mattn/go-sqlite3">mattn/go-sqlite3</a>。下面就是一个依赖go-sqlite3包的示例：</p>
<pre><code>// go-compilation/go-sqlite3/main.go
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    // 打开数据库（如果不存在，则创建）
    db, err := sql.Open("sqlite3", "./test.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 创建表
    sqlStmt := `CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);`
    _, err = db.Exec(sqlStmt)
    if err != nil {
        log.Fatalf("%q: %s\n", err, sqlStmt)
    }

    // 插入数据
    _, err = db.Exec(`INSERT INTO user (name) VALUES (?)`, "Alice")
    if err != nil {
        log.Fatal(err)
    }

    // 查询数据
    rows, err := db.Query(`SELECT id, name FROM user;`)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        err = rows.Scan(&amp;id, &amp;name)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%d: %s\n", id, name)
    }

    // 检查查询中的错误
    if err = rows.Err(); err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>编译和运行该源码：</p>
<pre><code>$go build demo
$ldd demo
    linux-vdso.so.1 =&gt;  (0x00007ffe23d8e000)
    libdl.so.2 =&gt; /lib64/libdl.so.2 (0x00007faf0ddef000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007faf0dbd3000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007faf0d805000)
    /lib64/ld-linux-x86-64.so.2 (0x00007faf0dff3000)
$./demo
1: Alice
</code></pre>
<p>到这里，有些读者可能会问一个问题：如果需要在上述依赖场景中生成静态链接的Go程序，该怎么做呢？接下来，我们就来看看这个问题的解决细节。</p>
<h2>3. 如何在上述情况下实现静态链接？</h2>
<p>到这里是不是有些烧脑了啊！我们针对上一节的三种情况，分别对应来看一下静态编译的方案。</p>
<h3>3.1 仅依赖标准包</h3>
<p>在前面我们说过，之所以在使用os/user、net包时会在默认情况下采用动态链接，是因为Go使用了这两个包对应功能的C版实现，如果要做静态编译，让Go编译器选择它们的纯Go版实现即可。那我们仅需要关闭CGO即可，以依赖标准库os/user为例：</p>
<pre><code>$CGO_ENABLED=0 go build -o helloworld-with-os-user-static main-with-os-user.go
$file helloworld-with-os-user-static
helloworld-with-os-user-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-with-os-user-static
    不是动态可执行文件
</code></pre>
<h3>3.2 使用cgo调用外部c程序（静态链接）</h3>
<p>对于依赖cgo调用外部c的程序，我们要使用静态链接就必须要求外部c库提供静态库，因此，我们需要my-c-lib提供一份libmylib.a，这通过下面命令可以实现(或执行make static)：</p>
<pre><code>$gcc -c -fPIC -o mylib.o mylib.c
$ar rcs libmylib.a mylib.o
</code></pre>
<p>有了libmylib.a后，我们还要让Go程序静态链接该.a文件，于是我们需要修改一下Go源码中cgo链接的flag，加上静态链接的选项：</p>
<pre><code>// go-compilation/main-with-call-myclib-static.go
... ...
#cgo LDFLAGS: -static -L my-c-lib -lmylib
... ...
</code></pre>
<p>编译链接并查看一下文件属性：</p>
<pre><code>$go build -o helloworld-with-call-myclib-static main-with-call-myclib-static.go

$file helloworld-with-call-myclib-static
helloworld-with-call-myclib-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=b3da3ed817d0d04230460069b048cab5f5bfc3b9, not stripped
</code></pre>
<p>我们得到了预期的结果！</p>
<h3>3.3 依赖使用cgo的外部go包（静态链接）</h3>
<p>最麻烦的是这类情况，要想实现静态链接，我们需要找出外部go依赖的所有c库的.a文件(静态共享库)。以我们的go-sqlite3示例为例，go-sqlite3是sqlite库的go binding，它依赖sqlite库，同时所有第三方c库都依赖libc，我们还要准备一份libc的.a文件，下面我们就先安装这些：</p>
<pre><code>$yum install -y gcc glibc-static sqlite-devel
... ...

已安装:
  sqlite-devel.x86_64 0:3.7.17-8.el7_7.1                                                                                          

更新完毕:
  glibc-static.x86_64 0:2.17-326.el7_9.3
</code></pre>
<p>接下来，我们就来以静态链接的方式在go-compilation/go-sqlite3-static下编译一下：</p>
<pre><code>$go build -tags 'sqlite_omit_load_extension' -ldflags '-linkmode external -extldflags "-static"' demo

$file ./demo
./demo: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=c779f5c3eaa945d916de059b56d94c23974ce61c, not stripped
</code></pre>
<p>这里命令行中的-tags &#8216;sqlite_omit_load_extension&#8217;用于禁用SQLite3的动态加载功能，确保更好的静态链接兼容性。而-ldflags &#8216;-linkmode external -extldflags “-static”&#8216;的含义是使用外部链接器(比如gcc linker)，并强制静态链接所有库。</p>
<p>我们再看完略烧脑的几个细节后，再来看一个略轻松的话题。</p>
<h2>4. Go编译出的可执行文件过大，能优化吗？</h2>
<p>Go编译出的二进制文件一般较大，一个简单的“Hello World”程序通常在2MB左右：</p>
<pre><code>$ls -lh helloworld-default
-rwxr-xr-x 1 root root 2.1M 11月  3 10:39 helloworld-default
</code></pre>
<p>这一方面是因为Go将整个runtime都编译到可执行文件中了，另一方面也是因为Go静态编译所致。那么在默认情况下，Go二进制文件的大小还有优化空间么？方法不多，有两种可以尝试：</p>
<ul>
<li>去除符号表和调试信息</li>
</ul>
<p>在编译时使用-ldflags=”-s -w”标志可以去除符号表和调试符号，其中-s用于去掉符号表和调试信息，-w用于去掉DWARF调试信息，这样能显著减小文件体积。以helloworld为例，可执行文件的size减少了近四成：</p>
<pre><code>$go build -ldflags="-s -w" -o helloworld-default-nosym main.go
$ls -l
-rwxr-xr-x 1 root root 2124504 11月  3 10:39 helloworld-default
-rwxr-xr-x 1 root root 1384600 11月  3 13:34 helloworld-default-nosym
</code></pre>
<ul>
<li>使用tinygo</li>
</ul>
<p><a href="https://github.com/tinygo-org/tinygo/">TinyGo</a>是一个Go语言的编译器，它专为资源受限的环境而设计，例如微控制器、WebAssembly和其他嵌入式设备。TinyGo的目标是提供一个轻量级的、能在小型设备上运行的Go运行时，同时尽可能支持Go语言的特性。tinygo的一大优点就是生成的二进制文件通常比标准Go编译器生成的文件小得多：</p>
<pre><code>$tinygo build -o helloworld-tinygo main.go
$ls -l
总用量 2728
-rwxr-xr-x  1 root root 2128909 11月  5 05:43 helloworld-default*
-rwxr-xr-x  1 root root  647600 11月  5 05:45 helloworld-tinygo*
</code></pre>
<p>我们看到：tinygo生成的可执行文件的size仅是原来的30%。</p>
<blockquote>
<p>注：虽然TinyGo在特定场景（如IoT和嵌入式开发）中非常有用，但在常规服务器环境中，由于生态系统兼容性、性能、调试支持等方面的限制，可能并不是最佳选择。对于需要高并发、复杂功能和良好调试支持的应用，标准Go仍然是更合适的选择。</p>
<p>注：这里使用的tinygo为0.34.0版本。</p>
</blockquote>
<h2>5. 未使用的符号是否会被编译到Go二进制文件中？</h2>
<p>到这里，相信读者心中也都会萦绕一些问题：到底哪些符号被编译到最终的Go二进制文件中了呢？未使用的符号是否会被编译到Go二进制文件中吗？在这一小节中，我们就来探索一下。</p>
<p>出于对Go的了解，我们已经知道无论是GOPATH时代，还是Go module时代，Go的编译单元始终是包(package)，一个包（无论包中包含多少个Go源文件）都会作为一个编译单元被编译为一个目标文件(.a)，然后Go链接器会将多个目标文件链接在一起生成可执行文件，因此如果一个包被依赖，那么它就会进入到Go二进制文件中，它内部的符号也会进入到Go二进制文件中。</p>
<p>那么问题来了！是否被依赖包中的所有符号都会被放到最终的可执行文件中呢？我们以最简单的helloworld-default为例，它依赖fmt包，并调用了fmt包的Println函数，我们看看Println这个符号是否会出现在最终的可执行文件中：</p>
<pre><code>$nm -a helloworld-default | grep "Println"
000000000048eba0 T fmt.(*pp).doPrintln
</code></pre>
<p>居然没有！我们初步怀疑是inline优化在作祟。接下来，关闭优化再来试试：</p>
<pre><code>$go build -o helloworld-default-noinline -gcflags='-l -N' main.go

$nm -a helloworld-default-noinline | grep "Println"
000000000048ec00 T fmt.(*pp).doPrintln
0000000000489ee0 T fmt.Println
</code></pre>
<p>看来的确如此！不过当使用”fmt.”去过滤helloworld-default-noinline的所有符号时，我们发现fmt包的一些常见的符号并未包含在其中，比如Printf、Fprintf、Scanf等。</p>
<p>这是因为Go编译器的一个重要特性：死码消除(dead code elimination)，即编译器会将未使用的代码和数据从最终的二进制文件中剔除。</p>
<p>我们再来继续探讨一个衍生问题：如果Go源码使用空导入方式导入了一个包，那么这个包是否会被编译到Go二进制文件中呢？其实道理是一样的，如果用到了里面的符号，就会存在，否则不会。</p>
<p>以空导入os/user为例，即便在CGO_ENABLED=0的情况下，因为没有使用os/user中的任何符号，在最终的二进制文件中也不会包含user包：</p>
<pre><code>$CGO_ENABLED=0 go build -o helloworld-with-os-user-noinline -gcflags='-l -N' main-with-os-user.go
[root@iZ2ze18rmx2avqb5xgb4omZ helloworld]# nm -a helloworld-with-os-user-noinline |grep user
0000000000551ac0 B runtime.userArenaState
</code></pre>
<p>但是如果是带有init函数的包，且init函数中调用了同包其他符号的情况呢？我们以expvar包为例看一下：</p>
<pre><code>// go-compilation/main-with-expvar.go

package main

import (
    _ "expvar"
    "fmt"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>编译并查看一下其中的符号：</p>
<pre><code>$go build -o helloworld-with-expvar-noinline -gcflags='-l -N' main-with-expvar.go
$nm -a helloworld-with-expvar-noinline|grep expvar
0000000000556480 T expvar.appendJSONQuote
00000000005562e0 T expvar.cmdline
00000000005561c0 T expvar.expvarHandler
00000000005568e0 T expvar.(*Func).String
0000000000555ee0 T expvar.Func.String
00000000005563a0 T expvar.init.0
00000000006e0560 D expvar..inittask
0000000000704550 d expvar..interfaceSwitch.0
... ...
</code></pre>
<p>除此之外，如果一个包即便没有init函数，但有需要初始化的全局变量，比如crypto包的hashes：</p>
<pre><code>// $GOROOT/src/crypto/crypto.go
var hashes = make([]func() hash.Hash, maxHash)
</code></pre>
<p>crypto包的相关如何也会进入最终的可执行文件中，大家自己动手不妨试试。下面是我得到的一些输出：</p>
<pre><code>$go build -o helloworld-with-crypto-noinline -gcflags='-l -N' main-with-crypto.go
$nm -a helloworld-with-crypto-noinline|grep crypto
00000000005517b0 B crypto.hashes
000000000048ee60 T crypto.init
0000000000547280 D crypto..inittask
</code></pre>
<p>有人会问：os/user包也有一些全局变量啊，为什么这些符号没有被包含在可执行文件中呢？比如：</p>
<pre><code>// $GOROOT/src/os/user/user.go
var (
    userImplemented      = true
    groupImplemented     = true
    groupListImplemented = true
)
</code></pre>
<p>这就要涉及Go包初始化的逻辑了。我们看到crypto包包含在可执行文件中的符号中有crypto.init和crypto..inittask这两个符号，显然这不是crypto包代码中的符号，而是Go编译器为crypto包自动生成的init函数和inittask结构。</p>
<p>Go编译器会为每个包生成一个init函数，即使包中没有显式定义init函数，同时<a href="https://go.dev/src/cmd/compile/internal/pkginit/init.go">每个包都会有一个inittask结构</a>，用于运行时的包初始化系统。当然这么说也不足够精确，如果一个包没有init函数、需要初始化的全局变量或其他需要运行时初始化的内容，则编译器不会为其生成init函数和inittask。比如上面的os/user包。</p>
<p>os/user包确实有上述全局变量的定义，但是这些变量是在编译期就可以确定值的常量布尔值，而且未被包外引用或在包内用于影响控制流。Go编译器足够智能，能够判断出这些初始化是”无副作用的”，不需要在运行时进行初始化。只有真正需要运行时初始化的包才会生成init和inittask。这也解释了为什么空导入os/user包时没有相关的init和inittask符号，而crypto、expvar包有的init.0和inittask符号。</p>
<h2>6. 如何快速判断Go项目是否依赖cgo？</h2>
<p>在使用开源Go项目时，我们经常会遇到项目文档中没有明确说明是否依赖Cgo的情况。这种情况下，如果我们需要在特定环境（比如CGO_ENABLED=0）下使用该项目，就需要事先判断项目是否依赖Cgo，有些时候还要快速地给出判断。</p>
<p>那究竟是否可以做到这种快速判断呢？我们先来看看一些常见的作法。</p>
<p>第一类作法是源码层面的静态分析。最直接的方式是检查源码中是否存在import “C”语句，这种引入方式是CGO使用的显著标志。</p>
<pre><code>// 在项目根目录中执行
$grep -rn 'import "C"' .
</code></pre>
<p>这个命令会递归搜索当前目录下所有文件，显示包含import “C”的行号和文件路径，帮助快速定位CGO的使用位置。</p>
<p>此外，CGO项目通常包含特殊的编译指令，这些指令以注释形式出现在源码中，比如前面见识过的#cgo CFLAGS、#cgo LDFLAGS等，通过对这些编译指令的检测，同样可以来判断项目是否依赖CGO。</p>
<p>不过第一类作法并不能查找出Go项目的依赖包是否依赖cgo。而找出直接依赖或间接依赖是否依赖cgo，我们需要工具帮忙，比如使用Go工具链提供的命令分析项目依赖：</p>
<pre><code>$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
</code></pre>
<p>其中ImportPath是依赖包的导入路径，而CgoFiles则是依赖中包含import “C”的Go源文件。我们以go-sqlite3那个依赖cgo的示例来验证一下：</p>
<pre><code>// cd go-compilation/go-sqlite3

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
runtime/cgo: [cgo.go]
github.com/mattn/go-sqlite3: [backup.go callback.go error.go sqlite3.go sqlite3_context.go sqlite3_load_extension.go sqlite3_opt_serialize.go sqlite3_opt_userauth_omit.go sqlite3_other.go sqlite3_type.go]
</code></pre>
<p>用空导入os/user的示例再来看一下：</p>
<pre><code>$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}'  main-with-os-user.go | grep -v '\[\]'
runtime/cgo: [cgo.go]
os/user: [cgo_lookup_cgo.go getgrouplist_unix.go]
</code></pre>
<p>我们知道os/user有纯go和C版本两个实现，因此上述判断只能说“对了一半”，当我关闭CGO_ENABLED时，Go编译器不会使用基于cgo的C版实现。</p>
<p>那是否在禁用cgo的前提下对源码进行一次编译便能验证项目是否对cgo有依赖呢？这样做显然谈不上是一种“快速”的方法，那是否有效呢？我们来对上面的go-sqlite3项目做一个测试，我们在关闭CGO_ENABLED时，编译一下该示例：</p>
<pre><code>// cd go-compilation/go-sqlite3
$ CGO_ENABLED=0 go build demo
</code></pre>
<p>我们看到，Go编译器并未报错！似乎该项目不需要cgo!  但真的是这样吗？我们运行一下编译后的demo可执行文件：</p>
<pre><code>$ ./demo
2024/11/03 22:10:36 "Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub": CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);
</code></pre>
<p>我们看到成功编译出来的程序居然出现运行时错误，提示需要cgo！</p>
<p>到这里，没有一种方法可以快速、精确的给出项目是否依赖cgo的判断。也许判断Go项目是否依赖CGO并没有捷径，需要从源码分析、依赖检查和构建测试等多个维度进行。</p>
<h2>7. 小结</h2>
<p>在本文中，我们深入探讨了Go语言编译过程中的几个重要细节，尤其是在静态链接和动态链接的选择上。通过具体示例，我们了解到：</p>
<ul>
<li>
<p>默认链接方式：尽管CGO_ENABLED默认值为1，Go编译器在大多数情况下会采用静态链接，只有在依赖特定的C库或标准库包时，才会切换到动态链接。</p>
</li>
<li>
<p>动态链接的条件：我们讨论了几种情况下Go会默认生成动态链接的可执行文件，包括依赖使用C实现的标准库包、显式使用cgo调用外部C程序，以及使用依赖cgo的第三方包。</p>
</li>
<li>
<p>实现静态链接：对于需要动态链接的场景，我们也提供了将其转为静态链接的解决方案，包括关闭CGO、使用静态库，以及处理依赖cgo的外部包的静态链接问题。</p>
</li>
<li>
<p>二进制文件优化：我们还介绍了如何通过去除符号表和使用TinyGo等方法来优化生成的Go二进制文件的大小，以满足不同场景下的需求。</p>
</li>
<li>
<p>符号编译与死码消除：最后，我们探讨了未使用的符号是否会被编译到最终的二进制文件中，并解释了Go编译器的死码消除机制。</p>
</li>
</ul>
<p>通过这些细节探讨，我希望能够帮助大家更好地理解Go编译的复杂性，并在实际开发中做出更明智的选择，亦能在面对Go编译相关问题时，提供有效的解决方案。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go-compilation">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/11/11/some-details-about-go-compilation/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go weak包前瞻：弱指针为内存管理带来新选择</title>
		<link>https://tonybai.com/2024/09/23/go-weak-package-preview/</link>
		<comments>https://tonybai.com/2024/09/23/go-weak-package-preview/#comments</comments>
		<pubDate>Sun, 22 Sep 2024 16:05:20 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ephemerons]]></category>
		<category><![CDATA[fasthttp]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[nil]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[std]]></category>
		<category><![CDATA[StrongPointer]]></category>
		<category><![CDATA[unique]]></category>
		<category><![CDATA[value]]></category>
		<category><![CDATA[weak]]></category>
		<category><![CDATA[WeakPointer]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[弱引用]]></category>
		<category><![CDATA[弱指针]]></category>
		<category><![CDATA[强指针]]></category>
		<category><![CDATA[标准库]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4292</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/09/23/go-weak-package-preview 在介绍Go 1.23引入的unique包的《Go unique包：突破字符串局限的通用值Interning技术实现》一文中，我们知道了unique包底层是基于internal/weak包实现的，internal/weak是一个弱指针功能的Go实现。所谓弱指针(Weak Pointer，也称为弱引用)是与强指针相对而言的，强指针(Strong Pointer，也可称作强引用)就是下面代码片段中的这种常规指针： var p *T = new(T) // 假设T类型对象被分配到堆上 只要p指向堆上的T对象，那么T对象就无法被GC回收。但弱指针并非如此，它也可以指向堆上的某个内存对象(比如T类型对象)，但它无法像强指针那样阻止GC回收该对象。 Go unique包的实现者Michael Knyszek近期提议在标准库引入weak包(实际上是将internal/weak公开暴露给Go开发者)，该提议被Russ Cox代表的Go提案评审委员会所接受，最早将于Go 1.24版本落地。 在这篇短文中，我们来前瞻一下weak包的API设计、原理、应用场景以及社区对该提案一些观点。 注：weak包尚未落地，本文中的代码在Go 1.23中均无法运行，可以视作伪代码。 1. weak包的API weak包的核心是Pointer&#91;T&#93;类型，它代表了对类型T的弱指针。以下目前Michael Knyszek为weak包设计的主要API： type Pointer[T any] struct { ... } func Make[T any](ptr *T) Pointer[T] func (p Pointer[T]) Value() *T Make函数用于创建一个弱指针，而Value方法则用于获取弱指针指向的实际值。如果原始对象已被垃圾回收，Value方法将返回nil。这个设计秉承了Go一贯的简洁，允许开发者轻松创建和使用弱指针，同时保持了Go语言的类型安全特性。 2. weak包弱指针的工作原理 在开篇时，我已经对弱指针的作用做了简单说明，这里结合上述weak包的API和提案中的设计原理再扩展一下。 弱指针的核心思想是允许引用内存而不阻止垃圾回收器回收它。垃圾回收器在回收对象时，会自动将所有指向该对象的弱指针设置为nil。这确保了弱指针不会产生悬空引用(dangling pointer)。 下图是weak包弱指针的工作原理示意图，展示了weak pointer的核心工作原理，包括间接对象的使用和垃圾回收时的行为： 简单看一下这张图：程序创建一个对象并通过weak.Make创建一个weak.Pointer(弱指针)，在Go运行时内部，weak.Pointer通过8字节的间接对象引用原始对象。这个间接对象是weak.Pointer的内部字段，按当前internal/weak的实现来看，该字段是一个unsafe.Pointer。这个间接对象包含了实际的弱引用。 值得注意的是，弱指针的比较基于它们最初创建时使用的指针。即使原始对象被回收，两个由相同指针创建的弱指针仍然会被认为是相等的。这个特性使得弱指针可以安全地用作map的键。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-weak-package-preview-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/09/23/go-weak-package-preview">本文永久链接</a> &#8211; https://tonybai.com/2024/09/23/go-weak-package-preview</p>
<p>在介绍<a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23">Go 1.23</a>引入的unique包的《<a href="https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/">Go unique包：突破字符串局限的通用值Interning技术实现</a>》一文中，我们知道了unique包底层是基于internal/weak包实现的，internal/weak是一个弱指针功能的Go实现。所谓弱指针(Weak Pointer，也称为弱引用)是与强指针相对而言的，强指针(Strong Pointer，也可称作强引用)就是下面代码片段中的这种常规指针：</p>
<pre><code>var p *T = new(T) // 假设T类型对象被分配到堆上
</code></pre>
<p>只要p指向堆上的T对象，那么T对象就无法被GC回收。但弱指针并非如此，它也可以指向堆上的某个内存对象(比如T类型对象)，但它无法像强指针那样阻止GC回收该对象。</p>
<p>Go unique包的实现者<a href="https://github.com/golang/go/issues/67552">Michael Knyszek近期提议在标准库引入weak包</a>(实际上是将internal/weak公开暴露给Go开发者)，该提议被Russ Cox代表的Go提案评审委员会所接受，最早将于Go 1.24版本落地。</p>
<p>在这篇短文中，我们来前瞻一下weak包的API设计、原理、应用场景以及社区对该提案一些观点。</p>
<blockquote>
<p>注：weak包尚未落地，本文中的代码在Go 1.23中均无法运行，可以视作伪代码。</p>
</blockquote>
<h2>1. weak包的API</h2>
<p>weak包的核心是Pointer&#91;T&#93;类型，它代表了对类型T的弱指针。以下目前Michael Knyszek为weak包设计的主要API：</p>
<pre><code>type Pointer[T any] struct { ... }

func Make[T any](ptr *T) Pointer[T]

func (p Pointer[T]) Value() *T
</code></pre>
<p>Make函数用于创建一个弱指针，而Value方法则用于获取弱指针指向的实际值。如果原始对象已被垃圾回收，Value方法将返回nil。这个设计秉承了Go一贯的简洁，允许开发者轻松创建和使用弱指针，同时保持了Go语言的类型安全特性。</p>
<h2>2. weak包弱指针的工作原理</h2>
<p>在开篇时，我已经对弱指针的作用做了简单说明，这里结合上述weak包的API和提案中的设计原理再扩展一下。</p>
<p>弱指针的核心思想是允许引用内存而不阻止垃圾回收器回收它。垃圾回收器在回收对象时，会自动将所有指向该对象的弱指针设置为nil。这确保了弱指针不会产生悬空引用(dangling pointer)。</p>
<p>下图是weak包弱指针的工作原理示意图，展示了weak pointer的核心工作原理，包括间接对象的使用和垃圾回收时的行为：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-weak-package-preview-2.png" alt="" /></p>
<p>简单看一下这张图：程序创建一个对象并通过weak.Make创建一个weak.Pointer(弱指针)，在Go运行时内部，weak.Pointer通过8字节的间接对象引用原始对象。这个间接对象是weak.Pointer的内部字段，按当前internal/weak的实现来看，该字段是一个unsafe.Pointer。这个间接对象包含了实际的弱引用。</p>
<p>值得注意的是，弱指针的比较基于它们最初创建时使用的指针。即使原始对象被回收，两个由相同指针创建的弱指针仍然会被认为是相等的。这个特性使得弱指针可以安全地用作map的键。</p>
<h2>3. weak包的典型使用场景</h2>
<p>weak包的引入将为Go带来更灵活的内存管理机制，它允许开发者创建不会阻止垃圾回收的引用，从而在保持内存效率的同时，实现更复杂的数据结构和算法。特别是在处理缓存、<a href="https://wiki.c2.com/?CanonicalizedMapping">规范化映射(Canonicalization mapping)</a>等场景时。</p>
<p>以缓存为例，使用弱指针，我们可以创建不会阻止被缓存对象被垃圾回收的缓存系统，这对于管理内存敏感的大型缓存系统特别有用。下面提案中Russ Cox举的一个使用weak包实现简单缓存的示例(可理解为伪代码)：</p>
<pre><code>type Cache[K any, V any] struct {
    f func(*K) V
    m atomic.Map[uintptr, func() V]
}

func NewCache[K comparable, V any](f func(*K)V) *Cache[K, V] {
    return &amp;Cache[K, V]{f: f}
}

func (c *Cache[K, V]) Get(k *K) V {
    kw := uintptr(unsafe.Pointer((k))
    vf, ok := c.m.Load(kw)
    if ok {
        return vf()
    }
    vf = sync.OnceValue(func() V { return c.f(k) })
    vf, loaded := c.m.LoadOrStore(kw, vf) // 原issue中似乎少了第二个参数vf
    if !loaded {
        // Stored kw→vf to c.m; add the cleanup.
        runtime.AddCleanup(k, c.cleanup, kw)
    }
    return vf()
}

func (c *Cache[K, V]) cleanup(kw uintptr) {
    c.m.Delete(kw)
}

var cached = NewCache(expensiveComputation)
</code></pre>
<p>这段代码定义了一个泛型缓存结构Cache，它有两个类型参数K和V，以及两个成员字段f和m：</p>
<ul>
<li>f是一个函数，接受&#42;K类型的指针，返回V类型的值，这是用于计算缓存值的函数。</li>
<li>m是一个原子映射，键是K类型的弱指针，值是返回V的函数。</li>
</ul>
<p>NewCache是缓存的创建函数，接受一个计算函数f，返回初始化的Cache指针。</p>
<p>Cache类型的Get方法用于获取缓存的值，它首先创建键k的弱指针kw，然后以该弱指针为键尝试从缓存(atomicMap)中加载值。如果找到，直接返回缓存的值。如果未找到，使用sync.OnceValue创建一个只执行一次的函数，调用c.f(k)计算值。之后，尝试将新计算的函数存储到缓存中。 如果成功存储（即之前没有这个键），添加一个清理函数，最后返回计算后的Value值。</p>
<p>这个实现允许缓存中的键在不再被程序其他部分引用时被垃圾回收，从而避免了内存长期占用或是泄漏。</p>
<h2>4. 社区声音</h2>
<p>针对该weak包提案，Go社区的主要声音是支持的，认为weak包将为Go带来更灵活的内存管理机制，但也表示了对无法用好weak包这个低级机制的担忧，希望在正式文档或Go Tour中包含更多使用关于weak包的示例和最佳实践。</p>
<p>Go新版GC的主要设计者<a href="https://github.com/RLH">Richard L. Hudson</a>提出了对sweeping storms和清理大型缓存中过时weak条目的担忧，并提出了使用<a href="https://dl.acm.org/doi/pdf/10.1145/263698.263733">ephemerons</a>（一种更复杂的弱引用机制）的可能性，但也认识到其实现复杂度和性能开销较高。</p>
<p>也有一些Go社区开发者保持了对weak包的谨慎态度，比如<a href="https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp">fasthttp</a>的维护者、<a href="https://github.com/VictoriaMetrics/VictoriaMetrics">VictorialMetrics</a>的联创<a href="https://github.com/valyala">Aliaksandr Valialkin</a> 就建议：在决定如何在Go中实现弱指针之前，最好先分析其他编程语言中弱指针的最常见的生产用例，并首先思考一下在标准库中为这些实际用例提供更高级别的解决方案而不是暴露较低级别的弱指针的方案是否会更好。</p>
<p>也有gopher提出：能否在提案中添加2-3个没有弱指针就无法解决的实际问题的例子，但Michael Knyszek并未回应。</p>
<h2>5. 小结</h2>
<p>weak包的引入让Go的工具箱更加完整，它为开发者提供了更细粒度的内存控制，同时其核心API也保持了Go简单易用的特性。</p>
<p>对于Go开发者来说，weak包使得某些复杂的内存管理场景变得更容易处理，但也需要开发者更好地理解垃圾回收机制和弱引用的工作原理。</p>
<p>社区对weak包的引入持积极态度，但也关注其实现细节、性能影响和最佳实践，同时也意识到了使用weak指针时可能面临的挑战。</p>
<p>不过，开发者在使用weak包时还是需要谨慎，毕竟过度使用弱指针可能会使代码变得难以理解和维护，最好的方法是将它用在最适合的场景下。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/09/23/go-weak-package-preview/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Go unique包：突破字符串局限的通用值Interning技术实现</title>
		<link>https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/</link>
		<comments>https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/#comments</comments>
		<pubDate>Tue, 17 Sep 2024 22:06:19 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[go4org]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Go标准库]]></category>
		<category><![CDATA[Handle]]></category>
		<category><![CDATA[hash]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Lisp]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[netaddr]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[std]]></category>
		<category><![CDATA[strong]]></category>
		<category><![CDATA[tailscale]]></category>
		<category><![CDATA[unique]]></category>
		<category><![CDATA[value]]></category>
		<category><![CDATA[weak]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[值比较]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[内存优化]]></category>
		<category><![CDATA[内存管理]]></category>
		<category><![CDATA[哈希表]]></category>
		<category><![CDATA[字符串]]></category>
		<category><![CDATA[字符串interning]]></category>
		<category><![CDATA[并发编程]]></category>
		<category><![CDATA[弱指针]]></category>
		<category><![CDATA[强指针]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[方法]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[锁]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4283</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/09/18/understand-go-unique-package-by-example Go的1.23版本中引入了一个新的标准库包unique，为Go开发者带来了高效的值interning能力。这种能力不仅适用于字符串类型值，还可应用于任何可比较(comparable)类型的值。 本文将简要探讨interning技术及其在Go中的实现方式，通过介绍unique包的功能，帮助读者更好地理解这一技术及其实际应用。 1. 从string interning技术说起 通常提到interning技术时，指的是传统的字符串驻留（string interning）技术。它是一种优化方法，旨在减少程序中重复字符串的内存占用，并提高字符串比较操作的效率。其基本原理是将相同的字符串值在内存中只存储一次，所有对该字符串的引用都指向同一内存地址，而不是为每个相同字符串创建单独的副本。下图展示了使用和不使用string interning技术的对比: 这个图直观地展示了string interning如何通过共享相同的字符串来节省内存和提高效率。我们看到：在不使用string interning的情况下，每个字符串都有自己的内存分配，即使内容相同，比如”Hello”字符串出现两次，占用了两块不同的内存空间。而在使用string interning的情况下，相同内容的字符串只存储一次，比如：两个”Hello”字符串引用指向同一个内存位置。 string interning在多种场景下非常有用，比如在解析文本格式(如XML、JSON)时，interning能高效处理标签名称经常重复的问题；在编译器或解释器的实现时，interning能够减少符号表中的重复项等。 传统的string interning通常使用哈希表或字典来存储字符串的唯一实例。每次出现新字符串时，程序首先会检查哈希表中是否已有相同的字符串，若存在则返回其引用，若不存在则将其存储在表中。 Michael Knyszek在Go官博介绍interning技术时，也给出了一个传统实现的代码片段： var internPool map[string]string // Intern returns a string that is equal to s but that may share storage with // a string previously passed to Intern. func Intern(s string) string { pooled, ok := [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-go-unique-package-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/09/18/understand-go-unique-package-by-example">本文永久链接</a> &#8211; https://tonybai.com/2024/09/18/understand-go-unique-package-by-example</p>
<p><a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23">Go的1.23版本</a>中引入了一个<a href="https://pkg.go.dev/unique?ref=tonybai.com">新的标准库包unique</a>，为Go开发者带来了高效的值<a href="https://en.wikipedia.org/wiki/Interning_(computer_science)">interning能力</a>。这种能力不仅适用于字符串类型值，还可应用于任何可比较(comparable)类型的值。</p>
<p>本文将简要探讨interning技术及其在Go中的实现方式，通过介绍unique包的功能，帮助读者更好地理解这一技术及其实际应用。</p>
<h2>1. 从string interning技术说起</h2>
<p>通常提到interning技术时，指的是传统的字符串驻留（string interning）技术。它是一种优化方法，旨在<strong>减少程序中重复字符串的内存占用</strong>，并<strong>提高字符串比较操作的效率</strong>。其基本原理是将相同的字符串值在内存中只存储一次，所有对该字符串的引用都指向同一内存地址，而不是为每个相同字符串创建单独的副本。下图展示了使用和不使用string interning技术的对比:</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-unique-package-by-example-2.png" alt="" /></p>
<p>这个图直观地展示了string interning如何通过共享相同的字符串来节省内存和提高效率。我们看到：在不使用string interning的情况下，每个字符串都有自己的内存分配，即使内容相同，比如”Hello”字符串出现两次，占用了两块不同的内存空间。而在使用string interning的情况下，相同内容的字符串只存储一次，比如：两个”Hello”字符串引用指向同一个内存位置。</p>
<p>string interning在多种场景下非常有用，比如在解析文本格式(如XML、JSON)时，interning能高效处理标签名称经常重复的问题；在编译器或解释器的实现时，interning能够减少符号表中的重复项等。</p>
<p>传统的string interning通常使用哈希表或字典来存储字符串的唯一实例。每次出现新字符串时，程序首先会检查哈希表中是否已有相同的字符串，若存在则返回其引用，若不存在则将其存储在表中。</p>
<p>Michael Knyszek在<a href="https://go.dev/blog/unique?ref=tonybai.com">Go官博介绍interning技术</a>时，也给出了一个传统实现的代码片段：</p>
<pre><code>var internPool map[string]string

// Intern returns a string that is equal to s but that may share storage with
// a string previously passed to Intern.
func Intern(s string) string {
    pooled, ok := internPool[s]
    if !ok {
        // Clone the string in case it's part of some much bigger string.
        // This should be rare, if interning is being used well.
        pooled = strings.Clone(s)
        internPool[pooled] = pooled
    }
    return pooled
}
</code></pre>
<p>这种实现虽然简单，但Knyszek指出了其存在几个问题：</p>
<ul>
<li>一旦字符串被intern，就永远不会被释放。</li>
<li>在多goroutine环境下使用需要额外的同步机制。</li>
<li>仅限于字符串类型值，不能用于其他类型的值。</li>
</ul>
<p>Go 1.23版本引入的unique包就是string interning技术的一种Go官方实现，当然就像前面所说，unique包不仅仅支持传统的string interning，还支持任何支持比较的类型的值的interning。</p>
<p>不过，在介绍unique包之前，我们简单看看这些年来Go社区对interning技术的贡献。</p>
<h2>2. Go社区interning技术的实现简史</h2>
<p>由于其他主流语言都或多或少有了对string interning的支持，Go社区显然也需要这样的包，在Go issues列表中，我能找到的最早提出在Go中添加interning技术实现的是2013年go核心开发人员Brad Fitzpatrick提出的”<a href="https://github.com/golang/go/issues/5160?ref=tonybai.com">proposal: runtime: optionally allow callers to intern strings</a>“。</p>
<p>2019年，Josh Bleecher Snyder发表了一篇博文<a href="https://commaok.xyz/post/intern-strings?ref=tonybai.com">Interning strings in Go</a>，探讨了interning的Go实现方法，并给出一个<a href="https://github.com/josharian/intern">简单但重度使用sync.Pool的interning实现</a>，该实现支持对string和字节切片的interning。</p>
<p>2021年，tailscale为了实现<a href="https://github.com/inetaf/netaddr">可以高效表示ip地址的netaddr包</a>，构建和开源了<a href="https://github.com/go4org/intern">go4.org/intern包</a>，这是一个可用于量产级别的interning实现。</p>
<blockquote>
<p>注：go4.org中这个go4的名字很可能就是因为go4.org这个组织只有四个contributors：Brad Fitzpatrick、Josh Bleecher Snyder、Dave Anderson和Matt Layher。之前的一篇文章《<a href="https://tonybai.com/2023/04/16/understanding-unsafe-assume-no-moving-gc/">理解unsafe-assume-no-moving-gc包</a>》中的unsafe-assume-no-moving-gc包也是go4.org下面的。</p>
</blockquote>
<p>之后，Brad Fitzpatrick将inetaf/netaddr包的实现合并到了Go标准库net/netip中，而netaddr包依赖的go4.org/intern包也被移入Go项目，变为internal/intern包，并被net/netip包所使用。</p>
<p>直到2023年9月，mknyszek提出”<a href="https://github.com/golang/go/issues/62483">unique: new package with unique.Handle</a>“的proposal，给出unique包的API设计和参考实现。unique落地后，原先使用internal/intern包的net/netip也都改为使用unique包了，internal/intern在Go 1.23版本被移除。</p>
<p>接下来，我们来看看这篇文章的主角unique包。</p>
<h2>3. Go的unique包介绍</h2>
<p>相较于传统的interning实现以及Go社区之前的实现，Go 1.23引入的unique包提供了一个更加通用和高效的interning实现方案。下面我们就分别从API、unique包的优势以及实现原理等几个方面介绍一下这个包。</p>
<h3>3.1 unique包的API</h3>
<p>从用户角度看，unique包提供的核心API非常简洁：</p>
<pre><code>$go doc unique.Handle
package unique // import "unique"

type Handle[T comparable] struct {
    // Has unexported fields.
}

func Make[T comparable](value T) Handle[T]
func (h Handle[T]) Value() T
</code></pre>
<p>Make函数就是unique包的”Intern”函数，它接受一个可比较类型的值，返回一个intern后的值，不过和前面那个传统实现方式的Intern函数不同，Make函数返回的是一个Handle&#91;T&#93;类型的值。针对同一个传给Make函数的值，返回的Handle&#91;T&#93;类型的值是相同的：</p>
<pre><code>// unique-examples/string_interning.go
package main

import "unique"

func main() {
    h1 := unique.Make("hello")
    h2 := unique.Make("hello")
    h3 := unique.Make("hello")
    h4 := unique.Make("golang")
    println(h1 == h2) // true
    println(h1 == h3) // true
    println(h1 == h4) // false
    println(h2 == h4) // false
}
</code></pre>
<p>unique包的作者Knyszek认为Handle&#91;T&#93;和Lisp语言中的Symbol十分类似，Symbol在Lisp中是interned后的字符串，Lisp确保相同的字符串只存储一次，提高内存存储和使用效率。</p>
<p>不过前面说了，unique不仅支持字符串值的interning，还支持其他可比较类型的值的interning，下面是一个int interning和一个自定义可比较类型的interning的例子：</p>
<pre><code>// unique-examples/int_interning.go

package main

import "unique"

func main() {
    var a, b int = 5, 6
    h1 := unique.Make(a)
    h2 := unique.Make(a)
    h3 := unique.Make(b)
    println(h1 == h2) // true
    println(h1 == h3) // false
}

// unique-examples/user_type_interning.go

package main

import "unique"

type UserType struct {
    a int
    z float64
    s string
}

func main() {
    var u1 = UserType{
        a: 5,
        z: 3.14,
        s: "golang",
    }
    var u2 = UserType{
        a: 5,
        z: 3.15,
        s: "golang",
    }
    h1 := unique.Make(u1)
    h2 := unique.Make(u1)
    h3 := unique.Make(u2)
    println(h1 == h2) // true
    println(h1 == h3) // false
}
</code></pre>
<blockquote>
<p>注：如果要intern的类型T是包含指针的结构体，这些指针指向的值几乎总是会逃逸到堆上。</p>
</blockquote>
<p>通过Make获得的Handle&#91;T&#93;的Value方法可以获取到interning值的原始值，我们看下面示例：</p>
<pre><code>// unique-examples/value.go
package main

import (
    "fmt"
    "unique"
)

type UserType struct {
    a int
    z float64
    s string
}

func main() {
    var u1 = UserType{
        a: 5,
        z: 3.14,
        s: "golang",
    }
    h1 := unique.Make(u1)
    h2 := unique.Make("hello, golang")
    h3 := unique.Make(567890)
    v1 := h1.Value()
    v2 := h2.Value()
    v3 := h3.Value()
    fmt.Printf("%T: %v\n", v1, v1) // main.UserType: {5 3.14 golang}
    fmt.Printf("%T: %v\n", v2, v2) // string: hello, golang
    fmt.Printf("%T: %v\n", v3, v3) // int: 567890
}
</code></pre>
<blockquote>
<p>注：Value方法返回的是值的浅拷贝，对于复合类型可能存在共享底层数据的情况。</p>
</blockquote>
<h3>3.2 unique包的实现原理</h3>
<p>传统的字符串interning实现起来可能并不难，但unique包的目标是设计支持可比较类型、interning值也可被GC且支持快速interning值比较的方案，unique包的实现涉及到hashtrimap、细粒度锁以及与runtime内gc相关函数结合的技术难题，因此其门槛还是很高的，即便是Go核心团队成员Knyszek实现的unique包，在Go 1.23发布后也被发现了<a href="https://github.com/golang/go/issues/69370">较为“严重”的bug</a>，该问题将<a href="https://github.com/golang/go/issues/69383">在Go 1.23.2版本修正</a>。</p>
<p>下面是一个unique包实现原理的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-unique-package-by-example-3.png" alt="" /></p>
<p>上图展示了Make、Handle&#91;T&#93;和Value方法之间的关系，以及它们如何与内部的map(hashtrieMap)交互。</p>
<p>我们看到，图中三次调用Make(“hello”)都返回相同的Handle&#91;string&#93;{ptr1}，即无论调用多少次Make，对于相同的输入值，Make总是返回相同的Handle。</p>
<p>图中的Handle&#91;string&#93;{ptr1}是一个包含指向存储”hello”的内存位置指针的结构，所有三次Make调用返回的Handle都指向同一个内存位置。下面是Handle结构体的定义，看了你就明白了这句话的含义：</p>
<pre><code>// $GOROOT/src/unique/handle.go
type Handle[T comparable] struct {
    value *T
}
</code></pre>
<blockquote>
<p>注：这里Handle内部的指针&#42;T都是strong pointer(强指针)，以图中示例，只要有一个Handle实例(由Make返回的)存在，内存中的”hello”就不会被GC。</p>
</blockquote>
<p>Handle&#91;string&#93;{ptr1}的Value()方法返回存储的字符串值”hello”。</p>
<p>unique包有一个内部map(hashtrieMap)存储键值对，键是字符串”hello”的clone，值是一个weak.Pointer，指向存储实际字符串值的内存位置。weak.Pointer 是Go 1.23版本的内部包internal/weak中的一个类型，主要用于实现弱指针（weak pointer）的功能。weak.Pointer的主要作用是允许引用一个对象，而不会阻止该对象被垃圾收集器回收。具体来说，它允许你持有一个指向对象的指针，但当该对象的强指针消失时，垃圾收集器仍然可以回收该对象。下面是一张weak Pointer工作机制的示意图，展示了弱指针的生命周期以及对GC行为的影响：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-unique-package-by-example-4.png" alt="" /></p>
<p>初始状态下，应用创建一个对象，同时创建一个强指针和一个weak.Pointer指向该对象。GC检查对象，但因为存在强指针，所以不能回收。强指针被移除，只剩下weak.Pointer指向对象。GC检查对象，发现没有强指针，于是回收对象。内存被释放，weak.Pointer变为nil。</p>
<p>由于weak包位于internal包中，它只能在Go的标准库或特定包中使用，我们只能用下面的伪代码来展示weak.Pointer的机制：</p>
<pre><code>package main

import (
    "fmt"
    "runtime"
    "unsafe"
    "internal/weak"
)

type MyStruct struct {
    name string
}

func main() {
    // 创建一个对象，obj可以理解为该对象的强指针
    obj := &amp;MyStruct{name: "object1"} 

    // 创建一个weak.Pointer指向obj，weakPtr是对obj指向内存的弱指针
    weakPtr := weak.Make(obj)

    // 显示对象的值，通过强指针和弱指针都可以
    fmt.Println("Before GC:", weakPtr.Value())
    fmt.Println("Before GC:", *obj)

    // 释放原始对象的强指针
    obj = nil

    // 强制执行GC，这时由于弱指针无法阻止GC，obj指向的内存可能被回收
    runtime.GC()

    // 查看弱指针是否仍然有效，这里不能直接使用obj，因为对象可能已经被回收
    fmt.Println("After GC:", weakPtr.Value())
}
</code></pre>
<p>弱指针有一些典型的使用场景，比如在缓存机制中，可能希望引用某些对象而不阻止它们被垃圾回收。这样可以在内存不足时自动释放不再使用的缓存对象；又比如在某些场景下，不希望对象长时间驻留在内存中，但仍然希望能够在需要时重新创建或加载它们，即延迟加载的对象；在某些数据结构中（如哈希表或链表），持有强指针可能会导致内存泄漏，弱指针可以有效避免这种情况。</p>
<blockquote>
<p>注：目前Knyszek已经提出proposal，<a href="https://github.com/golang/go/issues/67552">将weak包提升为标准库公共API</a>，该proposal已经被accept，最早将在Go 1.24版本落地。</p>
</blockquote>
<h3>3.3 unique包的优势</h3>
<p>从上面示例和原理示意图来看，unique包的设计和实现有几个显著的优势：</p>
<ul>
<li>泛型支持</li>
</ul>
<p>通过使用Go的泛型特性，unique包可以处理任何可比较的类型，大大扩展了其应用范围，不再局限于字符串类型。</p>
<ul>
<li>高效的内存管理</li>
</ul>
<p>unique包使用了运行时级别的弱指针实现，确保当所有相关的Handle&#91;T&#93;(即强指针)都不再被使用时，内部map中的值可以被垃圾回收，这既避免了内存长期占用，也避免了内存泄漏问题。</p>
<ul>
<li>快速比较操作</li>
</ul>
<p>Handle&#91;T&#93;类型的比较操作被优化为简单的指针比较，这比直接比较值(特别是对于大型结构体或长字符串内容)要快得多。</p>
<h3>3.4 unique包的实际应用</h3>
<p>unique包刚刚诞生，目前在Go标准库中的实际应用主要就是在net/netip包中，替代了之前由go4.org/intern移植到标准库中的internal/intern包。</p>
<p>net/netip包使用unique来优化Addr结构体中的addrDetail字段：</p>
<pre><code>type Addr struct {
    // 其他字段...

    // Details about the address, wrapped up together and canonicalized.
    z unique.Handle[addrDetail]
}

// addrDetail represents the details of an Addr, like address family and IPv6 zone.
type addrDetail struct {
    isV6   bool   // IPv4 is false, IPv6 is true.
    zoneV6 string // != "" only if IsV6 is true.
}

// z0, z4, and z6noz are sentinel Addr.z values.
// See the Addr type's field docs.
var (
    z0    unique.Handle[addrDetail]
    z4    = unique.Make(addrDetail{})
    z6noz = unique.Make(addrDetail{isV6: true})
)

// WithZone returns an IP that's the same as ip but with the provided
// zone. If zone is empty, the zone is removed. If ip is an IPv4
// address, WithZone is a no-op and returns ip unchanged.
func (ip Addr) WithZone(zone string) Addr {
    if !ip.Is6() {
        return ip
    }
    if zone == "" {
        ip.z = z6noz
        return ip
    }
    ip.z = unique.Make(addrDetail{isV6: true, zoneV6: zone})
    return ip
}
</code></pre>
<p>通过使用unique，net/netip包能够显著减少处理大量IP地址时的内存占用。特别是对于具有相同zone的IPv6地址，内存使用可以大幅降低。</p>
<p>下面我们也通过一个简单的示例来看看使用unique包的内存占用减少的效果。</p>
<h3>3.5 内存占用减少的效果</h3>
<p>现在我们创建100w个长字符串，这100w个字符串中，有1000种不同的字符串，相当于每种字符串有1000个重复值。下面分别用unique包和不用unique包来演示这个示例，看看内存占用情况：</p>
<pre><code>// unique-examples/effect_with_unique.go 

package main

import (
    "fmt"
    "runtime"
    "strings"
    "unique"
)

const (
    numItems    = 1000000
    stringLen   = 20
    numDistinct = 1000
)

func main() {
    // 创建一些不同的字符串
    distinctStrings := make([]string, numDistinct)
    for i := 0; i &lt; numDistinct; i++ {
        distinctStrings[i] = strings.Repeat(string(rune('A'+i%26)), stringLen)
    }

    // 使用unique包
    withUnique := make([]unique.Handle[string], numItems)
    for i := 0; i &lt; numItems; i++ {
        withUnique[i] = unique.Make(distinctStrings[i%numDistinct])
    }

    runtime.GC() // 强制GC
    printMemUsage("With unique")

    runtime.KeepAlive(withUnique)
}

func printMemUsage(label string) {
    var m runtime.MemStats
    runtime.ReadMemStats(&amp;m)
    fmt.Printf("%s:\n", label)
    fmt.Printf("  Alloc = %v MiB\n", bToMb(m.Alloc))
    fmt.Printf("  TotalAlloc = %v MiB\n", bToMb(m.TotalAlloc))
    fmt.Printf("  Sys = %v MiB\n", bToMb(m.Sys))
    fmt.Printf("  HeapAlloc = %v MiB\n", bToMb(m.HeapAlloc))
    fmt.Printf("  HeapSys = %v MiB\n", bToMb(m.HeapSys))
    fmt.Printf("  HeapInuse = %v MiB\n", bToMb(m.HeapInuse))
    fmt.Println()
}

func bToMb(b uint64) uint64 {
    return b / 1024 / 1024
}

// unique-examples/effect_without_unique.go
... 

func main() {
    // 创建一些不同的字符串
    distinctStrings := make([]string, numDistinct)
    for i := 0; i &lt; numDistinct; i++ {
        distinctStrings[i] = strings.Repeat(string(rune('A'+i%26)), stringLen)
    }

    // 不使用unique包
    withoutUnique := make([]string, numItems)
    for i := 0; i &lt; numItems; i++ {
        withoutUnique[i] = distinctStrings[i%numDistinct]
    }

    runtime.GC() // 强制GC以确保准确的内存使用统计
    printMemUsage("Without unique")

    runtime.KeepAlive(withoutUnique)
}

...
</code></pre>
<p>下面分别运行这两个源码：</p>
<pre><code>$go run effect_with_unique.go
With unique:
  Alloc = 7 MiB
  TotalAlloc = 7 MiB
  Sys = 15 MiB
  HeapAlloc = 7 MiB
  HeapSys = 11 MiB
  HeapInuse = 8 MiB

$go run effect_without_unique.go
Without unique:
  Alloc = 15 MiB
  TotalAlloc = 15 MiB
  Sys = 22 MiB
  HeapAlloc = 15 MiB
  HeapSys = 19 MiB
  HeapInuse = 15 MiB
</code></pre>
<p>这个结果清楚地显示了使用unique包后的内存节省。不使用unique包时，每个重复的字符串都会单独分配内存。而使用unique包后，相同的字符串只会分配一次，大大减少了内存使用。在实际应用中，内存节省的效果可能更加显著，特别是在处理大量重复数据（如日志处理、文本分析等）的场景中。</p>
<h2>4. 小结</h2>
<p>本文粗略探讨了Go 1.23版本引入的unique包：我们从字符串interning技术说起，介绍了Go社区在interning技术实现方面的努力历程，重点阐述了unique包的API设计、实现原理及其优势。</p>
<p>我们看到：unique包不仅支持传统的字符串interning，还扩展到任何可比较类型的值。其核心API设计简洁，通过Handle&#91;T&#93;类型和Make、Value方法实现了高效的值interning。</p>
<p>在实现原理上，unique包巧妙地结合了hashtrieMap、细粒度锁以及与runtime内gc相关函数，实现了支持可比较类型、interned值可被GC且支持快速比较的方案。</p>
<p>总的来说，unique包为Go开发者提供了一个强大而灵活的interning工具，有望在未来的Go社区项目中得到广泛应用。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/unique-examples">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://commaok.xyz/post/intern-strings/">Interning strings in Go</a> &#8211; https://commaok.xyz/post/intern-strings/</li>
<li><a href="https://en.wikipedia.org/wiki/String_interning">Interning</a> &#8211; https://en.wikipedia.org/wiki/String_interning</li>
<li><a href="https://github.com/golang/go/issues/62483">unique: new package with unique.Handle</a> &#8211; https://github.com/golang/go/issues/62483</li>
<li><a href="https://go.dev/blog/unique">New unique package</a> &#8211; https://go.dev/blog/unique</li>
<li><a href="https://github.com/golang/go/issues/69370">unique: large string still referenced, after interning only a small substring</a> &#8211; https://github.com/golang/go/issues/69370</li>
<li><a href="https://tailscale.com/blog/netaddr-new-ip-type-for-go?ref=tonybai.com">netaddr.IP: a new IP address type for Go</a> &#8211; https://tailscale.com/blog/netaddr-new-ip-type-for-go</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>编译Go应用的黑盒挑战：无源码只有.a文件，你能搞定吗？</title>
		<link>https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go/</link>
		<comments>https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go/#comments</comments>
		<pubDate>Wed, 30 Aug 2023 12:58:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[buildmode]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-build]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[GOCACHE]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[goinstall]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[gorun]]></category>
		<category><![CDATA[gotool]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[importcfg]]></category>
		<category><![CDATA[ldflags]]></category>
		<category><![CDATA[library]]></category>
		<category><![CDATA[link]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[source]]></category>
		<category><![CDATA[std]]></category>
		<category><![CDATA[uber]]></category>
		<category><![CDATA[zap]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[编译]]></category>
		<category><![CDATA[链接]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3974</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go 上周末，一个Gopher在微信上与我交流了一个有关Go程序编译的问题。他的述求说起来也不复杂，那就是合作公司提供的API包仅包括golang archive(使用go build -buildmode=archive构建的.a文件)，没有Go包的源码。如何将这个.a链接到项目构建出的最终可执行程序中呢？ 对于C、C++、Java程序员来说，仅提供静态链接库或动态链接库(包括头文件)、jar包而不提供源码的API是十分寻常的。但对于Go来说，仅提供Go包的archive(.a)文件，而不提供Go包源码的情况却是极其不常见的。究其原因，简单来说就是go build或go run不支持！ 注：《Go语言精进之路vo1》一书的第16条“理解Go语言的包导入”对Go的编译过程和原理做了系统说明。 那么真的就没有方法实现没有source、仅基于.a文件的Go应用构建了吗？也不是。的确有一些hack的方法可以实现这点，本文就来从技术角度来探讨一下这些hack方法，但并不推荐使用！ 1. 回顾go build不支持”no source, only .a” 我们首先来回顾一下go build在”no source, only .a”下的表现。为此，我们先建立一个实验环境，其目录和文件布局如下： // 没有外部依赖的api包: foo $tree goarchive-nodeps goarchive-nodeps ├── Makefile ├── foo.a ├── foo.go └── go.mod $tree library library └── github.com └── bigwhite └── foo.a // 依赖foo包的app工程 $tree app-link-foo app-link-foo ├── Makefile ├── go.mod [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/how-to-build-with-only-archive-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go">本文永久链接</a> &#8211; https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go</p>
<p>上周末，一个Gopher在微信上与我交流了一个有关Go程序编译的问题。他的述求说起来也不复杂，那就是合作公司提供的API包仅包括golang archive(使用go build -buildmode=archive构建的.a文件)，没有Go包的源码。如何将这个.a链接到项目构建出的最终可执行程序中呢？</p>
<p>对于C、C++、Java程序员来说，仅提供静态链接库或<a href="https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/">动态链接库</a>(包括头文件)、jar包而不提供源码的API是十分寻常的。但对于Go来说，仅提供Go包的archive(.a)文件，而不提供Go包源码的情况却是极其不常见的。究其原因，简单来说就是<strong>go build或go run不支持</strong>！</p>
<blockquote>
<p>注：<a href="https://item.jd.com/13694000.html">《Go语言精进之路vo1》</a>一书的第16条“理解Go语言的包导入”对Go的编译过程和原理做了系统说明。</p>
</blockquote>
<p>那么真的就没有方法实现没有source、仅基于.a文件的Go应用构建了吗？也不是。的确有一些hack的方法可以实现这点，本文就来从技术角度来探讨一下这些hack方法，但并<strong>不推荐使用</strong>！</p>
<h2>1. 回顾go build不支持”no source, only .a”</h2>
<p>我们首先来回顾一下go build在”no source, only .a”下的表现。为此，我们先建立一个实验环境，其目录和文件布局如下：</p>
<pre><code>// 没有外部依赖的api包: foo

$tree goarchive-nodeps
goarchive-nodeps
├── Makefile
├── foo.a
├── foo.go
└── go.mod

$tree library
library
└── github.com
    └── bigwhite
        └── foo.a

// 依赖foo包的app工程
$tree app-link-foo
app-link-foo
├── Makefile
├── go.mod
└── main.go
</code></pre>
<p>这里我们已经将app-link-foo依赖的foo.a构建了出来(通过go build -buildmode=arhive)，并放入了library对应的目录下。</p>
<blockquote>
<p>注：可通过ar -x foo.a命令可以查看foo.a的组成。</p>
</blockquote>
<p>现在我们使用go build来构建app-link-foo工程：</p>
<pre><code>$cd app-link-foo
$go build
main.go:6:2: no required module provides package github.com/bigwhite/foo; to add it:
    go get github.com/bigwhite/foo
</code></pre>
<p>我们看到：go build会分析app-link-foo的依赖，并要求获取其依赖的foo包的代码，但我们无法满足go build这一要求！</p>
<p>有人可能会说：go build支持向go build支持向compiler和linker传递参数，是不是将foo.a的位置告知compiler和linker就可以了呢？我们来试试：</p>
<pre><code>$go build -x -v -gcflags '-I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library' -ldflags '-L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library' -o main main.go
main.go:6:2: no required module provides package github.com/bigwhite/foo; to add it:
    go get github.com/bigwhite/foo
make: *** [build] Error 1
</code></pre>
<p>我们看到：即便向go build传入gcflags和ldflags参数，告知了foo.a的搜索路径，go build依然报错，仍然提示需要foo包的源码！也就是说go build还没到调用go tool compile和go tool link那一步就开始报错了！</p>
<p>go build不支持在无源码情况下链接.a，那么我们<strong>只能绕过go build</strong>了！</p>
<h2>2. 绕过go bulid</h2>
<p>认真读过<a href="https://item.jd.com/13694000.html">《Go语言精进之路vo1》</a>一书的朋友都会知道：go build实质是调用go tool compile和go tool link两个命令来完成go应用的构建过程的，使用go build -x -v可以查看到go build的详细构建过程。</p>
<p>接下来，我们就来扮演一下”go build”，以手动的方式分别调用go tool compile和go tool link，看看是否能达到无需依赖包源码就能成功构建的目标。</p>
<p>我们以foo.a这个自身没有外部依赖的go archive为例，用手动方式构建一下app-link-foo这个工程。</p>
<p>首先确保通过-buildmode=archive构建出的foo.a被正确放入library/github.com/bigwhite下面。</p>
<p>接下来，我们通过go tool compile编译一下app-link-foo：</p>
<pre><code>$cd app-link-foo
$go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go
</code></pre>
<p>我们看到：手动执行go tool compile在通过-I传入依赖库的.a文件时是可以正常编译出object file(目标文件)的。go tool compile的手册告诉我们-I选项为compile提供了搜索包导入路径的目录：</p>
<pre><code>$go tool compile -h
  ... ...
  -I directory
        add directory to import search path
  ... ...
</code></pre>
<p>接下来我们用go tool link将main.o和foo.a链接在一起形成可执行二进制文件main：</p>
<pre><code>$cd app-link-foo
$go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o
</code></pre>
<p>通过go tool link并在-L传入foo.a的链接路径的情况下，我们成功地将main.o和foo.a链接在了一起，形成了最终的可执行文件main。</p>
<p>go tool link的-L选项为link提供了搜索.a的路径：</p>
<pre><code>$go tool link -h
  ... ...
  -L directory
        add specified directory to library path
  ... ...
</code></pre>
<p>执行一下编译链接后的二进制文件main，我们将看到与预期相同的输出结果：</p>
<pre><code>$./main
invoke foo.Add
11
</code></pre>
<p>有些童鞋在执行go tool compile时可能会遇到找不到fmt.a或fmt.o的错误！这是因为Go 1.20版本及以后，Go安装包默认将不会在\$GOROOT/pkg/\$GOOS_\$GOARCH下面安装标准库的.a文件集合，这样go tool compile在这个路径下面就找不到app-link-foo所依赖的fmt.a：</p>
<pre><code>➜  /Users/tonybai/.bin/go1.20/pkg git:(master) ✗ $ls
darwin_amd64/    include/    tool/
➜  /Users/tonybai/.bin/go1.20/pkg git:(master) ✗ $cd darwin_amd64
➜  /Users/tonybai/.bin/go1.20/pkg/darwin_amd64 git:(master) ✗ $ls
</code></pre>
<p>解决方法也很简单，那就是手动执行下面命令编译和安装一下标准库的.a文件：</p>
<pre><code>$GODEBUG=installgoroot=all  go install std

➜  /Users/tonybai/.bin/go1.20/pkg/darwin_amd64 git:(master) ✗ $ls
archive/    database/    fmt.a        index/        mime/        plugin.a    strconv.a    time/
bufio.a        debug/        go/        internal/    mime.a        reflect/    strings.a    time.a
bytes.a        embed.a        hash/        io/        net/        reflect.a    sync/        unicode/
compress/    encoding/    hash.a        io.a        net.a        regexp/        sync.a        unicode.a
container/    encoding.a    html/        log/        os/        regexp.a    syscall.a    vendor/
context.a    errors.a    html.a        log.a        os.a        runtime/    testing/
crypto/        expvar.a    image/        math/        path/        runtime.a    testing.a
crypto.a    flag.a        image.a        math.a        path.a        sort.a        text/
</code></pre>
<p>这样无论是go tool compile，还是go tool link都会找到对应的标准库包了！</p>
<p>在这个例子中，foo.a仅依赖标准库，没有依赖第三方库，这样相对简单一些。通常合作伙伴提供的.a中的包都是依赖第三方的包的，下面我们就来看看如果.a有第三方依赖，上面的编译链接方法是否还能奏效！</p>
<h2>3. 要链接的.a文件自身也依赖第三方包</h2>
<p>goarchive-with-deps目录下的bar.a就是一个自身也依赖第三方包的go archive文件，它依赖的是uber的<a href="https://tonybai.com/2021/07/14/uber-zap-advanced-usage">zap日志包</a>以及zap包的依赖链，下面是bar的go.mod文件的内容：</p>
<pre><code>// goarchive-with-deps/go.mod

module github.com/bigwhite/bar

go 1.20

require go.uber.org/zap v1.25.0

require go.uber.org/multierr v1.10.0
</code></pre>
<p>我们先来安装app-link-foo的思路来编译链接一下app-link-bar：</p>
<pre><code>$cd app-link-bar
$make
go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go
go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o
/Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/link: cannot open file /Users/tonybai/.bin/go1.20/pkg/darwin_amd64/go.uber.org/zap.o: open /Users/tonybai/.bin/go1.20/pkg/darwin_amd64/go.uber.org/zap.o: no such file or directory
make: *** [all] Error 1
</code></pre>
<p>上面报的错误符合预期，因为zap.a尚没有放入build-with-archive-only/library下面。接下来我们基于uber zap的源码构建出一个zap.a并放入指定目录。bar.a依赖的uber zap的版本为v1.25.0，于是我们git clone一下uber zap，checkout出v1.25.0并执行构建：</p>
<pre><code>$cd go/src/go.uber.org/zap
$go build -o zap.a -buildmode=archive .
$cp zap.a /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library/go.uber.org/
</code></pre>
<p>再来编译一下app-link-bar：</p>
<pre><code>$make
go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go
go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o
/Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/link: fingerprint mismatch: go.uber.org/zap has b259b1e07032c6d9, import from github.com/bigwhite/bar expecting 8118f660c835360a
make: *** [all] Error 1
</code></pre>
<p>我们看到go tool link报错，提示“fingerprint mismatch”。这个错误的意思是bar.a期望的zap包的指纹与我们提供的在Library目录下的zap包的指纹不一致！</p>
<p>我们重新用go build -v -x来看一下bar.a的构建过程：</p>
<pre><code>$go build -x -v  -o bar.a -buildmode=archive
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3367014838
github.com/bigwhite/bar
mkdir -p $WORK/b001/
cat &gt;/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3367014838/b001/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile fmt=/Users/tonybai/Library/Caches/go-build/d3/d307b52dabc7d78a8ff219fb472fbc0b0a600038f43cd4c737914f8ccbd2bd70-d
packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d
EOF
cd /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/goarchive-with-deps
/Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=&gt;" -p github.com/bigwhite/bar -lang=go1.20 -complete -buildid mIMNOXMPJH00mEpw6WVc/mIMNOXMPJH00mEpw6WVc -goversion go1.20 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./bar.go
/Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/60/604b60360d384c49eb9c030a2726f02588f54375748ce1421e334bedfda2af47-d # internal
mv $WORK/b001/_pkg_.a bar.a
rm -r $WORK/b001/
</code></pre>
<p>我们看到在编译bar.a的过程中，go tool compile用的是-importcfg来得到的go.uber.org/zap的位置，而从打印的内容来看，go.uber.org/zap指向的是go module cache中的某个文件：packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d。</p>
<p>那是不是在build app-link-bar时也使用这个同样的go.uber.org/zap就可以成功通过go tool link的过程呢？我们来试一下：</p>
<pre><code>$cd app-link-bar
$make build-with-importcfg
go tool compile -importcfg import.link -o main.o main.go
go tool link -importcfg import.link -o main main.o

$./main
invoke foo.Add
{"level":"info","ts":1693203940.0701509,"caller":"goarchive-with-deps/bar.go:14","msg":"invoke bar.Add\n"}
11
</code></pre>
<p>使用-importcfg的确成功的编译链接了app-link-bar，其执行结果也符合预期！注意：这里我们放弃了之前使用的-I和-L，即便应用-I和-L，在与-importcfg联合使用时，go tool compile和link也会以-importcfg的信息为准！</p>
<p>现在还有一个问题摆在面前，那就是上述命令行中的import.link这个文件的内容是啥，又是如何生成的呢？这里的import.link文件十分“巨大”，有500多行，其内容大致如下：</p>
<pre><code>// app-link-bar/import.link

# import config
packagefile internal/goos=/Users/tonybai/Library/Caches/go-build/fa/facce9766a2b3c19364ee55c509863694b205190c504a3831cde7c208bb09f37-d
packagefile vendor/golang.org/x/crypto/chacha20=/Users/tonybai/Library/Caches/go-build/e0/e042b43b78d3596cc00e544a40a13e8cd6b566eb8f59c2d47aeb0bbcbd52aa56-d
... ...

packagefile github.com/bigwhite/bar=/Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library/github.com/bigwhite/bar.a
packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d
packagefile go.uber.org/zap/zapcore=/Users/tonybai/Library/Caches/go-build/e0/e0d81701b5d15628ce5bf174e5c1b7482c13ac3a3c868e9b054da8b1596eaace-d
packagefile go.uber.org/zap/internal/pool=/Users/tonybai/Library/Caches/go-build/bf/bfa96ebb89429b870e2c50c990c1945384e50d10ba354a3dab2b995a813c56a3-d
packagefile go.uber.org/zap/internal=/Users/tonybai/Library/Caches/go-build/33/33cb66c30939b8be915ddc1e237a04688f52c492d3ae58bfbc6196fff8b6b2b5-d
packagefile go.uber.org/zap/internal/bufferpool=/Users/tonybai/Library/Caches/go-build/68/68e58338a5acd96ee1733de78547720f26f4e13d8333defbc00099ac8560c8e8-d
packagefile go.uber.org/zap/buffer=/Users/tonybai/Library/Caches/go-build/7b/7bf00a1d4a69ddb1712366f45451890f3205b58ba49627ed4254acd9b0938ef8-d
packagefile go.uber.org/multierr=/Users/tonybai/Library/Caches/go-build/e7/e7cc278d56fc8262d9cf9de840a04aa675c75f8ac148e955c1ae9950c58c8034-d
packagefile go.uber.org/zap/internal/exit=/Users/tonybai/Library/Caches/go-build/18/187b2b490c810f37c3700132fba12b805e74bd3c59303972bcf74894a63de604-d
packagefile go.uber.org/zap/internal/color=/Users/tonybai/Library/Caches/go-build/e4/e419c93bea7ff2782b2047cf9e7ad37b07cf4a5a5b7f361bf968730e107a495b-d
</code></pre>
<p>这里包含了编译链接app-link-bar是依赖的标准库包、bar.a以及bar包依赖的所有第三方包的实际包.a文件的位置，显然这里用的大多数都是go module cache中的包缓存。</p>
<p>那么这个import.link如何得到呢？Go在golang.org/x/tools包中有一个<a href="https://raw.githubusercontent.com/golang/tools/master/internal/goroot/importcfg.go">importcfg.go文件</a>，基于该文件中的Importcfg函数可以获取标准库相关所有包的package link信息。我将该文件放在了build-with-archive-only/importcfg下了，大家可以自行取用。</p>
<p>importcfg生成了大部分package link，但仍会有一些bar.a依赖的第三方的包的link没有着落，go tool link在链接时会报错，根据报错信息中提供的包导入路径信息，比如：找不到go.uber.org/zap/internal/exit、go.uber.org/zap/internal/color，我们可以利用下面go list命令找到这些包的在本地go module cache中的link位置：</p>
<pre><code>$go list -export -e -f "{{.ImportPath}} {{.Export}}" go.uber.org/zap/internal/exit go.uber.org/zap/internal/color
go.uber.org/zap/internal/exit /Users/tonybai/Library/Caches/go-build/18/187b2b490c810f37c3700132fba12b805e74bd3c59303972bcf74894a63de604-d
go.uber.org/zap/internal/color /Users/tonybai/Library/Caches/go-build/e4/e419c93bea7ff2782b2047cf9e7ad37b07cf4a5a5b7f361bf968730e107a495b-d
</code></pre>
<p>然后可以手工将这些信息copy到import.link中。import.link文件就是在这样自动化+手工的过程中生成的（当然你完全可以自己编写一个工具，获取app-link-bar所需的所有package的link信息）。</p>
<h2>4. 小结</h2>
<p>到这里，我们通过hack的方法实现了在没有源码只有.a文件情况下的可执行程序的编译。</p>
<p>不过上述仅仅是纯技术上的探索，并非标准答案，也更非理想的答案。经过上述探索后，更巩固了我的观点：<strong>不要仅使用.a来构建go应用</strong>。</p>
<p>但非要这么做，如果你是.a的提供方，考虑fingerprint mismatch的情况，你估计要考虑在提供.a的同时，还要提供import.link、你构建.a时所有用到的go module cache的副本，并提供安装这些副本到目标主机上的脚本。这样你的.a用户才可能使用相同的依赖版本完成对.a文件的链接过程。</p>
<p>本文试验的代码都是在<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20/">Go 1.20版本</a>下编译链接的。如果编译.a的Go版本与编译链接可执行文件的Go版本不同，是否会失败呢？这个问题就当做作业留个大家去探索了！</p>
<p>本文涉及的代码可以从<a href="https://github.com/bigwhite/experiments/blob/master/build-with-archive-only">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用反射操作channel</title>
		<link>https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels/</link>
		<comments>https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels/#comments</comments>
		<pubDate>Tue, 15 Nov 2022 13:53:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Const]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[default]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[receive]]></category>
		<category><![CDATA[Reflect]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[select-case]]></category>
		<category><![CDATA[send]]></category>
		<category><![CDATA[反射]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[性能基准测试]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[枚举]]></category>
		<category><![CDATA[标准库]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3721</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels 今年教师节极客时间送给讲师4999 SVIP卡，一直没顾过来用，上周激活后在极客时间的众多精品课和专栏中徜徉，收获颇丰。尤其是在拜读鸟窝老师的《Go并发编程实战课》 后，get到一个以前从未用过的“技能点”：使用reflect操作channel，这里整理一下，把它分享给大家。 1. channel常规语法的“限制” Go语言实现了基于CSP（Communicating Sequential Processes）理论的并发方案。方案包含两个重要元素，一个是Goroutine，它是Go应用并发设计的基本构建与执行单元；另一个就是channel，它在并发模型中扮演着重要的角色。channel既可以用来实现Goroutine间的通信，还可以实现Goroutine间的同步。 我们先来简要回顾一下有关channel的常规语法。 我们可以通过make(chan T, n)创建元素类型为T、容量为n的channel类型实例，比如： ch1 := make(chan int) // 创建一个无缓冲的channel实例ch1 ch2 := make(chan int, 5) // 创建一个带缓冲的channel实例ch2 Go提供了“&#60;-”操作符用于对channel类型变量进行发送与接收操作，下面是一些对上述channel ch1和ch2进行收发操作的代码示例： ch1 &#60;- 13 // 将整型字面值13发送到无缓冲channel类型变量ch1中 n := &#60;- ch1 // 从无缓冲channel类型变量ch1中接收一个整型值存储到整型变量n中 ch2 &#60;- 17 // 将整型字面值17发送到带缓冲channel类型变量ch2中 m := &#60;- ch2 // 从带缓冲channel类型变量ch2中接收一个整型值存储到整型变量m中 Go不仅提供了单独操作channel的语法，还提供了可以同时对多个channel进行操作的select-case语法，比如下面代码： select { [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/using-reflect-to-manipulate-channels-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels">本文永久链接</a> &#8211; https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels</p>
<hr />
<p>今年教师节极客时间送给讲师4999 SVIP卡，一直没顾过来用，上周激活后在极客时间的众多精品课和专栏中徜徉，收获颇丰。尤其是在拜读鸟窝老师的<a href="http://gk.link/a/11OCq">《Go并发编程实战课》</a> 后，get到一个以前从未用过的“技能点”：<strong>使用reflect操作channel</strong>，这里整理一下，把它分享给大家。</p>
<h3>1. channel常规语法的“限制”</h3>
<p>Go语言实现了基于CSP（Communicating Sequential Processes）理论的并发方案。方案包含两个重要元素，一个是Goroutine，它是Go应用并发设计的基本构建与执行单元；另一个就是channel，它在并发模型中扮演着重要的角色。channel既可以用来实现Goroutine间的通信，还可以实现Goroutine间的同步。</p>
<p>我们先来简要回顾一下有关channel的常规语法。</p>
<p>我们可以通过make(chan T, n)创建元素类型为T、容量为n的channel类型实例，比如：</p>
<pre><code>ch1 := make(chan int)    // 创建一个无缓冲的channel实例ch1
ch2 := make(chan int, 5)  // 创建一个带缓冲的channel实例ch2
</code></pre>
<p>Go提供了“&lt;-”操作符用于对channel类型变量进行发送与接收操作，下面是一些对上述channel ch1和ch2进行收发操作的代码示例：</p>
<pre><code>ch1 &lt;- 13    // 将整型字面值13发送到无缓冲channel类型变量ch1中
n := &lt;- ch1  // 从无缓冲channel类型变量ch1中接收一个整型值存储到整型变量n中
ch2 &lt;- 17    // 将整型字面值17发送到带缓冲channel类型变量ch2中
m := &lt;- ch2  // 从带缓冲channel类型变量ch2中接收一个整型值存储到整型变量m中
</code></pre>
<p>Go不仅提供了单独操作channel的语法，还提供了可以同时对多个channel进行操作的select-case语法，比如下面代码：</p>
<pre><code>select {
case x := &lt;-ch1:     // 从channel ch1接收数据
  ... ...

case y, ok := &lt;-ch2: // 从channel ch2接收数据，并根据ok值判断ch2是否已经关闭
  ... ...

case ch3 &lt;- z:       // 将z值发送到channel ch3中:
  ... ...

default:             // 当上面case中的channel通信均无法实施时，执行该默认分支
}
</code></pre>
<p>我们看到：<strong>select语法中的case数量必须是固定的</strong>，我们只能把事先要交给select“监听”的channel准备好，在select语句中平铺开才可以。这就是select语句常规语法的限制，即<strong>select语法不支持动态的case集合</strong>。如果我们要监听的channel个数是不确定的，且在运行时会动态变化，那么select语法将无法满足我们的要求。</p>
<p>那怎么突破这一限制呢？鸟窝老师告诉我们用<a href="https://tonybai.com/2021/04/19/variable-operation-using-reflection-in-go">reflect包</a>。</p>
<h3>2. reflect.Select和reflect.SelectCase</h3>
<p>很多朋友可能和我一样，因为没有使用过reflect包操作channel，就会以为reflect操作channel的能力是Go新版本才提供的，但实则不然。reflect包中用于操作channel的函数Select以及其切片参数的元素类型SelectCase早在Go 1.1版本就加入到Go语言中了，有下图为证：</p>
<p><img src="https://tonybai.com/wp-content/uploads/using-reflect-to-manipulate-channels-3.png" alt="" /></p>
<p>那么如何使用这一“古老”的机制呢？我们一起来看一些例子。</p>
<p>首先我们来看<strong>第一种情况</strong>，也是最好理解的一种情况，<strong>即从一个动态的channel集合进行receive operations的select</strong>，下面是示例代码：</p>
<pre><code>// github.com/bigwhite/experiments/tree/master/reflect-operate-channel/select-recv/main.go
package main

import (
    "fmt"
    "math/rand"
    "reflect"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    var rchs []chan int
    for i := 0; i &lt; 10; i++ {
        rchs = append(rchs, make(chan int))
    }

    // 创建SelectCase
    var cases = createRecvCases(rchs)

    // 消费者goroutine
    go func() {
        defer wg.Done()
        for {
            chosen, recv, ok := reflect.Select(cases)
            if ok {
                fmt.Printf("recv from channel [%d], val=%v\n", chosen, recv)
                continue
            }
            // one of the channels is closed, exit the goroutine
            fmt.Printf("channel [%d] closed, select goroutine exit\n", chosen)
            return
        }
    }()

    // 生产者goroutine
    go func() {
        defer wg.Done()
        var n int
        s := rand.NewSource(time.Now().Unix())
        r := rand.New(s)
        for i := 0; i &lt; 10; i++ {
            n = r.Intn(10)
            rchs[n] &lt;- n
        }
        close(rchs[n])
    }()

    wg.Wait()
}

func createRecvCases(rchs []chan int) []reflect.SelectCase {
    var cases []reflect.SelectCase

    // 创建recv case
    for _, ch := range rchs {
        cases = append(cases, reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(ch),
        })
    }
    return cases
}
</code></pre>
<p>在这个例子中，我们通过createRecvCases这个函数创建一个元素类型为reflect.SelectCase的切片，之后使用reflect.Select可以监听这个切片集合，就像常规select语法那样，从有数据的recv Channel集合中随机选出一个返回。</p>
<p>reflect.SelectCase有三个字段：</p>
<pre><code>// $GOROOT/src/reflect/value.go
type SelectCase struct {
    Dir  SelectDir // direction of case
    Chan Value     // channel to use (for send or receive)
    Send Value     // value to send (for send)
}
</code></pre>
<p>其中Dir字段的值是一个“枚举”，枚举值如下：</p>
<pre><code>// $GOROOT/src/reflect/value.go
const (
    _             SelectDir = iota
    SelectSend              // case Chan &lt;- Send
    SelectRecv              // case &lt;-Chan:
    SelectDefault           // default
)
</code></pre>
<p>从常量名我们也可以看出，Dir用于标识case的类型，SelectRecv表示这是一个从channel做receive操作的case，SelectSend表示这是一个向channel做send操作的case；SelectDefault则表示这是一个default case。</p>
<p>构建好SelectCase的切片后，我们就可以将其传给reflect.Select了。Select函数的语义与select关键字语义是一致的，它会监听传入的所有SelectCase，以上面示例为例，如果所有channel都没有数据，那么reflect.Select会阻塞，直到某个channel有数据或关闭。</p>
<p>Select函数有三个返回值：</p>
<pre><code>// $GOROOT/src/reflect/value.go
func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)
</code></pre>
<p>对于上面示例而言，如果监听的某个case有数据了，那么Select的返回值chosen中存储了该channel在cases切片中的下标，recv中存储了从channel收到的值，recvOK等价于comma, ok模式的ok，当正常接收到由send channel操作发送的值时，recvOK为true，如果channel被close了，recvOK为false。</p>
<p>上面的示例启动了两个goroutine，一个goroutine充当消费者，由reflect.Select监听一组channel，当某个channel关闭时，该goroutine退出；另外一个goroutine则是随机的向这些channel中发送数据，发送10次后，关闭其中某个channel通知消费者退出。</p>
<p>我们运行一下该示例程序，得到如下结果：</p>
<pre><code>$go run main.go
recv from channel [1], val=1
recv from channel [4], val=4
recv from channel [5], val=5
recv from channel [8], val=8
recv from channel [1], val=1
recv from channel [1], val=1
recv from channel [8], val=8
recv from channel [3], val=3
recv from channel [5], val=5
recv from channel [9], val=9
channel [9] closed, select goroutine exit
</code></pre>
<p>我们日常编码时经常会在select语句中加上default分支，以防止select完全阻塞，下面我们就来改造一下示例，让其增加对default分支的支持：</p>
<pre><code>// github.com/bigwhite/experiments/tree/master/reflect-operate-channel/select-recv-with-default/main.go

package main

import (
    "fmt"
    "math/rand"
    "reflect"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    var rchs []chan int
    for i := 0; i &lt; 10; i++ {
        rchs = append(rchs, make(chan int))
    }

    // 创建SelectCase
    var cases = createRecvCases(rchs, true)

    // 消费者goroutine
    go func() {
        defer wg.Done()
        for {
            chosen, recv, ok := reflect.Select(cases)
            if cases[chosen].Dir == reflect.SelectDefault {
                fmt.Println("choose the default")
                continue
            }
            if ok {
                fmt.Printf("recv from channel [%d], val=%v\n", chosen, recv)
                continue
            }
            // one of the channels is closed, exit the goroutine
            fmt.Printf("channel [%d] closed, select goroutine exit\n", chosen)
            return
        }
    }()

    // 生产者goroutine
    go func() {
        defer wg.Done()
        var n int
        s := rand.NewSource(time.Now().Unix())
        r := rand.New(s)
        for i := 0; i &lt; 10; i++ {
            n = r.Intn(10)
            rchs[n] &lt;- n
        }
        close(rchs[n])
    }()

    wg.Wait()
}

func createRecvCases(rchs []chan int, withDefault bool) []reflect.SelectCase {
    var cases []reflect.SelectCase

    // 创建recv case
    for _, ch := range rchs {
        cases = append(cases, reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(ch),
        })
    }

    if withDefault {
        cases = append(cases, reflect.SelectCase{
            Dir:  reflect.SelectDefault,
            Chan: reflect.Value{},
            Send: reflect.Value{},
        })
    }

    return cases
}
</code></pre>
<p>在这个示例中，我们的createRecvCases函数增加了一个withDefault布尔型参数，当withDefault为true时，返回的cases切片中将包含一个default case。我们看到，创建defaultCase时，Chan和Send两个字段需要传入空的reflect.Value。</p>
<p>在消费者goroutine中，我们通过选出的case的Dir字段是否为reflect.SelectDefault来判定是否default case被选出，其余的处理逻辑不变，我们运行一下这个示例：</p>
<pre><code>$go run main.go
recv from channel [8], val=8
recv from channel [8], val=8
choose the default
choose the default
choose the default
choose the default
choose the default
recv from channel [1], val=1
choose the default
choose the default
choose the default
recv from channel [3], val=3
recv from channel [6], val=6
choose the default
choose the default
recv from channel [0], val=0
choose the default
choose the default
choose the default
recv from channel [5], val=5
recv from channel [2], val=2
choose the default
choose the default
choose the default
recv from channel [2], val=2
choose the default
choose the default
recv from channel [2], val=2
choose the default
choose the default
channel [2] closed, select goroutine exit
</code></pre>
<p>我们看到，default case被选择的几率还是蛮大的。</p>
<p>最后，我们再来看看如何使用reflect包向channel中发送数据，看下面示例代码：</p>
<pre><code>// github.com/bigwhite/experiments/tree/master/reflect-operate-channel/select-send/main.go

package main

import (
    "fmt"
    "reflect"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    ch0, ch1, ch2 := make(chan int), make(chan int), make(chan int)
    var schs = []chan int{ch0, ch1, ch2}

    // 创建SelectCase
    var cases = createCases(schs)

    // 生产者goroutine
    go func() {
        defer wg.Done()
        for range cases {
            chosen, _, _ := reflect.Select(cases)
            fmt.Printf("send to channel [%d], val=%v\n", chosen, cases[chosen].Send)
            cases[chosen].Chan = reflect.Value{}
        }
        fmt.Println("select goroutine exit")
        return
    }()

    // 消费者goroutine
    go func() {
        defer wg.Done()
        for range schs {
            var v int
            select {
            case v = &lt;-ch0:
                fmt.Printf("recv %d from ch0\n", v)
            case v = &lt;-ch1:
                fmt.Printf("recv %d from ch1\n", v)
            case v = &lt;-ch2:
                fmt.Printf("recv %d from ch2\n", v)
            }
        }
    }()

    wg.Wait()
}

func createCases(schs []chan int) []reflect.SelectCase {
    var cases []reflect.SelectCase

    // 创建send case
    for i, ch := range schs {
        n := i + 100
        cases = append(cases, reflect.SelectCase{
            Dir:  reflect.SelectSend,
            Chan: reflect.ValueOf(ch),
            Send: reflect.ValueOf(n),
        })
    }

    return cases
}
</code></pre>
<p>在这个示例中，我们针对三个channel：ch0，ch1和ch2创建了写操作的SelectCase，每个SelectCase的Send字段都被赋予了要发送给该channel的值，这里使用了“100+下标号”。</p>
<p>生产者goroutine中有一个“与众不同”的地方，那就是每次某个写操作触发后，我都将该SelectCase中的Chan重置为一个空Value，以防止下次该channel被重新选出：</p>
<pre><code>    cases[chosen].Chan = reflect.Value{}
</code></pre>
<p>运行一下该示例，我们得到：</p>
<pre><code>$go run main.go
recv 101 from ch1
send to channel [1], val=101
send to channel [0], val=100
recv 100 from ch0
recv 102 from ch2
send to channel [2], val=102
select goroutine exit
</code></pre>
<p>通过上面的几个例子我们看到，reflect.Select有着与select等价的语义，且还支持动态增删和修改case，功能不可为不强大，现在还剩一点要care，那就是它的执行性能如何呢？我们接着往下看。</p>
<h3>3. reflect.Select的性能</h3>
<p>我们用benchmark test来对比一下常规select与reflect.Select在执行性能上的差别，下面是benchmark代码：</p>
<pre><code>// github.com/bigwhite/experiments/tree/master/reflect-operate-channel/select-benchmark/benchmark_test.go
package main

import (
    "reflect"
    "testing"
)

func createCases(rchs []chan int) []reflect.SelectCase {
    var cases []reflect.SelectCase

    // 创建recv case
    for _, ch := range rchs {
        cases = append(cases, reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(ch),
        })
    }
    return cases
}

func BenchmarkSelect(b *testing.B) {
    var c1 = make(chan int)
    var c2 = make(chan int)
    var c3 = make(chan int)

    go func() {
        for {
            c1 &lt;- 1
        }
    }()
    go func() {
        for {
            c2 &lt;- 2
        }
    }()
    go func() {
        for {
            c3 &lt;- 3
        }
    }()

    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i &lt; b.N; i++ {
        select {
        case &lt;-c1:
        case &lt;-c2:
        case &lt;-c3:
        }
    }
}

func BenchmarkReflectSelect(b *testing.B) {
    var c1 = make(chan int)
    var c2 = make(chan int)
    var c3 = make(chan int)

    go func() {
        for {
            c1 &lt;- 1
        }
    }()
    go func() {
        for {
            c2 &lt;- 2
        }
    }()
    go func() {
        for {
            c3 &lt;- 3
        }
    }()

    chs := createCases([]chan int{c1, c2, c3})

    b.ReportAllocs()
    b.ResetTimer()

    for i := 0; i &lt; b.N; i++ {
        _, _, _ = reflect.Select(chs)
    }
}
</code></pre>
<p>运行一下该benchmark：</p>
<pre><code>$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/reflect-operate-channel/select-benchmark
... ...
BenchmarkSelect-8            2765396           427.8 ns/op         0 B/op          0 allocs/op
BenchmarkReflectSelect-8     1839706           806.0 ns/op       112 B/op          6 allocs/op
PASS
ok      github.com/bigwhite/experiments/reflect-operate-channel/select-benchmark    3.779s
</code></pre>
<p>我们看到：reflect.Select的执行效率相对于select还是要差的，并且在其执行过程中还要做额外的内存分配。</p>
<h3>4. 小结</h3>
<p>本文介绍了reflect.Select与SelectCase的结构以及如何使用它们在不同场景下操作channel。但大多数情况下，我们是不需要使用reflect.Select，常规select语法足以满足我们的要求。并且reflect.Select有对cases数量的约束，最大支持65536个cases，虽然这个约束对于大多数场合而言足够用了。</p>
<p>本文涉及的示例源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/reflect-operate-channel">这里</a>下载。</p>
<hr />
<p><img src="https://tonybai.com/wp-content/uploads/using-reflect-to-manipulate-channels-2.png" alt="" /></p>
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>让reviewdog支持gitlab-push-commit，守住代码质量下限</title>
		<link>https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor/</link>
		<comments>https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor/#comments</comments>
		<pubDate>Thu, 08 Sep 2022 13:37:30 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.gitlab-ci.yml]]></category>
		<category><![CDATA[.reviewdog.yml]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[diff]]></category>
		<category><![CDATA[fortran]]></category>
		<category><![CDATA[fuzzing]]></category>
		<category><![CDATA[gerrit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[git-commit]]></category>
		<category><![CDATA[git-diff]]></category>
		<category><![CDATA[git-push]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[gitlab]]></category>
		<category><![CDATA[gitlab-runner]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golangci-lint]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[pre-commit-hook]]></category>
		<category><![CDATA[reviewdog]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[代码评审]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[同行评审]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[程序设计语言]]></category>
		<category><![CDATA[编程]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[集成测试]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3654</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor 一. 代码质量保证的手段 从世界上首款计算机高级程序设计语言Fortran自上世纪50年代诞生以来，编程这个行当已经走过了近70年。虽然年头已不少，但不可否认的一点是：软件生产依然无法像硬件那样标准化，同一个小功能，N个程序员的有N种实现方法。 那么如何保证生产出的软件的质量符合我们的要求呢？不同领域的程序员都在进行着努力，比如：做编译器的让编译器更加严格，努力将内存安全问题彻底消除(如Rust)；做工具链的为程序员提供了内置于语言的各种单测、集成测试、接口测试、fuzzing test等工具(如Go工具链)，让程序员可以更容易地对自己所写的代码进行全方位的测试，以期找出更多的代码中的潜在问题&#8230; 当然，还有一种主观的代码质量保证方法目前依旧是主流，它就是是同行的代码评审(code review, cr)。 代码评审的方法主要有两种，一种是大家坐到一个会议室中，对某个人的某段代码“发表大论”；另外一种则是利用像gerrit这样的工具，在线对其他人的某次提交的代码或某PR的代码进行“评头论足”。 不过无论哪种，最初的时候大家都会细无巨细地从语法层面看到代码结构设计，再到业务逻辑层面，但这样做的弊端也是很显而易见，那就是效率低下，不聚焦(focus)。 于是人们想到了：能否利用工具来尽可能地发现语法层面的问题，这样代码评审时，人类专家便可以聚焦代码结构设计与业务逻辑层面的问题，分工明确后，效率自然提升(如下图)： 注：目前绝大多数工具链仅能自动帮助程序员解决语法层面的问题。将来，随着工具的日益强大，工具可以不断升级关注层次，逐渐进化到具备发现代码结构设计问题，甚至可以发现业务层面逻辑问题的能力。 于是就有了reviewdog这样的可以调用各种linter工具对代码进行自动扫描并将问题以comment的形式自动提交的代码仓库的工具。 到这里很多朋友会问，即便让工具来关注语法层面的问题，为何要用reviewdog这样的工具，git的pre-commit hook、git server hooks、利用Make等工具做开发阶段检查等手段也能检查代码中的语法问题，它们不再香了吗？ 下面简单看看这些方法的“问题”(我们假设大家都已经在使用git作为代码版本管理工具)： git pre-commit-hook git pre-commit hook是一个客户端的git hook，它是放在开发人员本地代码copy中的.git/hooks目录下的钩子，当开发人员在本地执行git commit时会被唤起执行。pre-commot hook的问题就在于我们没法在中心代码仓库对pre-commit hook的脚本内容做统一管理和维护。这个更适合开发人员根据自己的喜好、代码素养在自己的开发环境下部署。 此外，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，pre-commit hook就不在生效。 利用Make等工具做本地检查 利用make工具，我们可以在本地build代码之前对代码做lint等各种静态检查，但和pre-commit-hook一样，虽然Makefile可以提交代码仓库，但真正用于检查代码的工具依旧是在开发人员本地，难于对工具版本，设定的检查规则进行统一管理维护，可能导致不同开发人员环境有不一致的情况。另外同样的情况，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，Make工具依赖的代码检查工具可能并不存在，检查环节就无法有效实施。 git server hooks git支持server hooks，gitlab自12.8版本也开始支持server hooks(替换之前的custom hooks)。 Git server支持以下钩子： pre-receive post-receive update 我倒是没有深研究过这些server hooks是否能满足我们的功能要求，但就git server hooks的部署特点就决定了，它不适合，因为它要在gitlab的server上执行，这就意味着我们需要的所有静态代码检查工具都要部署和配置在与gitlab server同一个环境中，这耦合性太强，根本不便于我们对这些静态代码检查工具的管理与日常维护。 而像reviewdog这样的工具将与ci工具(比如gitlab-ci)集成，运行在slave/worker/runner的机器上，而这些机器上的环境便很容易统一的定制与管理。 好了，下面进入reviewdog时间！ 注：我们以代码仓库为gitlab为例，我曾做过小调查，目前企业内部基本都在使用gitlab搭建私有git仓库，除了那些自实现code仓库平台的大厂。 二. [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor">本文永久链接</a> &#8211; https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor</p>
<h3>一. 代码质量保证的手段</h3>
<p>从世界上首款计算机高级程序设计语言<a href="https://fortran-lang.org/en/">Fortran</a>自上世纪50年代诞生以来，编程这个行当已经走过了近70年。虽然年头已不少，但不可否认的一点是：<strong>软件生产依然无法像硬件那样标准化，同一个小功能，N个程序员的有N种实现方法</strong>。</p>
<p>那么如何保证生产出的软件的质量符合我们的要求呢？不同领域的程序员都在进行着努力，比如：做编译器的让编译器更加严格，努力将内存安全问题彻底消除(如<a href="https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together">Rust</a>)；做工具链的为程序员提供了内置于语言的各种单测、集成测试、接口测试、fuzzing test等工具(如Go工具链)，让程序员可以更容易地对自己所写的代码进行全方位的测试，以期找出更多的代码中的潜在问题&#8230;</p>
<p>当然，还有一种主观的代码质量保证方法目前依旧是主流，它就是是<a href="https://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/"><strong>同行的代码评审(code review, cr)</strong></a>。</p>
<p>代码评审的方法主要有两种，一种是大家坐到一个会议室中，对某个人的某段代码“发表大论”；另外一种则是利用像<a href="https://www.gerritcodereview.com">gerrit</a>这样的工具，在线对其他人的某次提交的代码或某PR的代码进行“评头论足”。</p>
<p>不过无论哪种，最初的时候大家都会细无巨细地从语法层面看到代码结构设计，再到业务逻辑层面，但这样做的弊端也是很显而易见，那就是<strong>效率低下，不聚焦(focus)</strong>。</p>
<p>于是人们想到了：能否利用工具来尽可能地发现语法层面的问题，这样代码评审时，人类专家便可以聚焦代码结构设计与业务逻辑层面的问题，分工明确后，效率自然提升(如下图)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-2.png" alt="" /></p>
<blockquote>
<p>注：目前绝大多数工具链仅能自动帮助程序员解决语法层面的问题。将来，随着工具的日益强大，工具可以不断升级关注层次，逐渐进化到具备发现代码结构设计问题，甚至可以发现业务层面逻辑问题的能力。</p>
</blockquote>
<p>于是就有了<a href="https://github.com/reviewdog/reviewdog">reviewdog</a>这样的可以调用各种linter工具对代码进行自动扫描并将问题以comment的形式自动提交的代码仓库的工具。</p>
<p>到这里很多朋友会问，即便让工具来关注语法层面的问题，为何要用reviewdog这样的工具，git的pre-commit hook、git server hooks、利用Make等工具做开发阶段检查等手段也能检查代码中的语法问题，它们不再香了吗？</p>
<p>下面简单看看这些方法的“问题”(我们假设大家都已经在使用git作为代码版本管理工具)：</p>
<ul>
<li>git pre-commit-hook  </li>
</ul>
<p>git pre-commit hook是一个客户端的git hook，它是放在开发人员本地代码copy中的.git/hooks目录下的钩子，当开发人员在本地执行git commit时会被唤起执行。pre-commot hook的问题就在于我们没法在中心代码仓库对pre-commit hook的脚本内容做统一管理和维护。这个更适合开发人员根据自己的喜好、代码素养在自己的开发环境下部署。</p>
<p>此外，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，pre-commit hook就不在生效。</p>
<ul>
<li>利用Make等工具做本地检查</li>
</ul>
<p>利用make工具，我们可以在本地build代码之前对代码做lint等各种静态检查，但和pre-commit-hook一样，虽然Makefile可以提交代码仓库，但真正用于检查代码的工具依旧是在开发人员本地，难于对工具版本，设定的检查规则进行统一管理维护，可能导致不同开发人员环境有不一致的情况。另外同样的情况，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，Make工具依赖的代码检查工具可能并不存在，检查环节就无法有效实施。</p>
<ul>
<li>git server hooks</li>
</ul>
<p>git支持<a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#_server_side_hooks">server hooks</a>，<a href="https://docs.gitlab.com/ee/administration/server_hooks.html">gitlab自12.8版本也开始支持server hooks</a>(替换之前的custom hooks)。</p>
<p>Git server支持以下钩子：</p>
<ul>
<li>pre-receive</li>
<li>post-receive</li>
<li>update</li>
</ul>
<p>我倒是没有深研究过这些server hooks是否能满足我们的功能要求，但就git server hooks的部署特点就决定了，它不适合，因为它要在gitlab的server上执行，这就意味着我们需要的所有静态代码检查工具都要部署和配置在与gitlab server同一个环境中，这耦合性太强，根本不便于我们对这些静态代码检查工具的管理与日常维护。</p>
<p>而像reviewdog这样的工具将与ci工具(比如gitlab-ci)集成，运行在slave/worker/runner的机器上，而这些机器上的环境便很容易统一的定制与管理。</p>
<p>好了，下面进入reviewdog时间！</p>
<blockquote>
<p>注：我们以代码仓库为gitlab为例，我曾做过小调查，目前企业内部基本都在使用gitlab搭建私有git仓库，除了那些自实现code仓库平台的大厂。</p>
</blockquote>
<h3>二. reviewdog是什么</h3>
<p>reviewdog是一个什么样的工具呢？我们来看看下面这幅示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-3.png" alt="" /></p>
<p>我们看到，这是一幅基于gitlab的ci执行流程图，在这个流程中，reviewdog运行在gitlab-runner节点，也就是负责真正执行ci job的节点上。每当开发人员执行一次git push，将commit同步到代码仓库，一次ci job将被触发，在承载该ci job的gitlab-runner节点上，reviewdog被唤起，它做了三件事：</p>
<ul>
<li>调用静态代码检查工具对最新pull下来的代码进行检查；</li>
<li>将代码检查结果(第几行有问题)与commit diff的结果进行比对，得到交集(即commit diff中变更(add和update)的代码行与代码检查结果的行一致的，放入交集中)；</li>
<li>将交集中代码检查结果信息以gitlab commit comment的形式post到gitlab仓库中</li>
</ul>
<p>这样开发人员就可以通过commit页面看到这些comments，并应对这些comment，必要情况下，会修复这些问题。</p>
<p>我们看到reviewdog和其他工具相比，最大的不同就是可以找出commit diff与lint结果中的交集，并与代码仓库交互，将这些交集中的结果以comments的形式放入commit页面，<strong>就像同行代码评审时，同行直接在你的commit页面添加comment一样</strong>。</p>
<p>然而当前版本的reviewdog还不支持直接在gitlab-push-commit上做检查与提交comment，可能是这样的场景较为少见，因为目前开源项目更多采用基于pr(pull request)的工作流，所以reviewdog内置了诸如github-pr-check、github-pr-review、gitlab-mr-commit等工作流的代码review。而像我们使用的基于gitlab-push-commit可能并不多见（当然我们内部使用这种也是有特定上下文的）。</p>
<p>那么如何让reviewdog支持gitlab-push-commit，即对push动作中的commit进行静态代码检查并将结果以comment的形式放入commit页面呢？我们只能<a href="https://github.com/bigwhite/reviewdog">fork reviewdog项目</a>，并在<a href="https://github.com/bigwhite/reviewdog">fork后的项目</a>中自行添加对gitlab-push-commit模式的支持。</p>
<h3>三. 改造reviewdog以支持gitlab-push-commit模式</h3>
<p>reviewdog就是一个命令行工具，通常就是一次性执行，因此它的代码结构较为清晰。我们可以简单围绕它支持的几种reporter模式来搞清楚如何增加对gitlab-push-commit模式的支持。</p>
<p>这里说明一下gitlab-push-commit模式的含义，首先该模式适用于开发人员通过git push推送代码到gitlab时触发的ci job。在该ci job中，reviewdog会运行配置的静态代码分析工具(比如golangci-lint等)对最新的代码进行扫描，并得到问题集合；然后获取最新的commit的sha值(CI_COMMIT_SHA)以及push之前的latest commit的sha值(CI_COMMIT_BEFORE_SHA)，并比较这两个版本间的diff。最后通过文件名与行号将问题集合与diff集合中的“交集”找出来，并将结果以comment形式通过gitlab client api提交到的此次push的最新的那个commit的页面。</p>
<p>目前该模式尚存在一个“瑕疵”，那就是如果一个push中有多个commit，那么gitlab-push-commit模式不会针对每个commit做diff和comment，而只是会用push中的latest commit与push之前的最新commit做比较。</p>
<p>定义清除gitlab-push-commit模式含义后，我们就可以“照葫芦画瓢”的为reviewdog增加该模式的支持了！</p>
<p>在main.go中，我们主要是在run函数中增加一个reporter case分支：</p>
<pre><code>// https://github.com/bigwhite/reviewdog/blob/master/cmd/reviewdog/main.go
func run(r io.Reader, w io.Writer, opt *option) error {
... ...

case "gitlab-push-commit":
    build, cli, err := gitlabBuildWithClient(opt.reporter)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] gitlabBuildWithClient ok\n")

    gc, err := gitlabservice.NewGitLabPushCommitsCommenter(cli, build.Owner, build.Repo, build.SHA)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\n")

    cs = reviewdog.MultiCommentService(gc, cs)
    ds, err = gitlabservice.NewGitLabPushCommitsDiff(cli, build.Owner, build.Repo, build.SHA, build.BeforeSHA)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\n")
... ...

}
</code></pre>
<p>在这个case中，我们主要是为后面的project.Run或reviewdog.Run方法准备gitlab client对象、PushCommitsCommenter对象(位于service/gitlab/gitlab_push_commits.go中)、PushCommitsDiff对象(位于service/gitlab/gitlab_push_commits_diff.go中)等。</p>
<p>gitlab_push_commits.go和gitlab_push_commits_diff.go是新增的两个go源文件，也是参考了同目录下的gitlab_mr_commit.go和gitlab_mr_diff.go改写而成的。具体代码这里就不列出来了，大家有兴趣可以自行阅读。</p>
<h3>四. 部署gitlab-runner验证新版reviewdog</h3>
<p>下面我们就来验证一下上述改造后的reviewdog。</p>
<h4>1. 安装gitlab-runner</h4>
<p>我们先在gitlab上建立一个实验项目，然后为该项目配置ci。如果你的gitlab还没有注册gitlab-runner，可以按下面步骤安装和注册runner节点(可以在顶层group下面建立，这样runner可以在group内共享：settings => CI/CD => Runners => Show runner installation instructions 有部署runner的详细命令说明)：</p>
<pre><code>//假设我们有一个ubuntu 20.04的主机，我们可以按下面命令安装和注册一个gitlab-runner：

sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# Give it permissions to execute
sudo chmod +x /usr/local/bin/gitlab-runner

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

# 注册该runner
sudo gitlab-runner register --url http://{gitlab-server-ip-addr}/ --registration-token {registration token}
</code></pre>
<p>上面命令会在/etc/gitlab-runner下面建立一个runner自用配置文件：config.toml：</p>
<pre><code>//  /etc/gitlab-runner/config.toml

concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "runner for ard group"
  url = "http://gitlab_ip_addr/"
  id = 1
  token = "{registration token}"
  token_obtained_at = 2022-09-01T11:03:43Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "shell"
  shell = "bash"
  environment = ["PATH=/home/tonybai/.bin/go1.18/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"]
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
</code></pre>
<p>这里我选择了shell executor，即基于主机shell执行ci job中的命令。runners下的environment可以设置shell的环境变量，这里的设置将覆盖对应账号(比如gitlab-runner)下的环境变量值。</p>
<p>gitlab-runner部署成功后，我们在group的runners下面便可以看到下面的available runners：</p>
<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-4.png" alt="" /></p>
<blockquote>
<p>注：在创建runner时，我为该runner设置了两个tag：ard和ci。</p>
<p>注：确保runner执行的命令在主机的PATH下面可以找到。</p>
</blockquote>
<h4>2. 创建personal access token</h4>
<p>reviewdog需要通过gitlab client API访问gitlab仓库获取信息并提交comments，这就需要我们为runner执行的命令提供access token。</p>
<p>gitlab有多种access token，比如：personal access token、project access token等。我们创建personal access token，我也测试过project access token，使用<a href="https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html">project access token</a>可以成功提交comment，但是notify mail十有八九无法发送出来。</p>
<p>access token要保存好，因为它只显示一次。</p>
<p>我们将personal access token配置到实验项目的variable中(Settings => CI/CD => variables)，variable的key为REVIEWDOG_GITLAB_API_TOKEN，值为刚刚创建的token。</p>
<p>后续每次CI job执行，该variable会作为预定义的环境变量对job生效。我们的reviewdog便可以使用该token访问gitlab。</p>
<h4>3. 配置实验项目的ci pipeline</h4>
<p>我们可以通过代码的形式配置实验项目的ci pipeline，我们在项目根目录下建立.gitlab-ci.yml文件，其内容如下：</p>
<pre><code>// .gitlab-ci.yml

build-job:
  tags:
      - ard
  stage: build
  script:
    - export CI_REPO_OWNER=ard/incubators
    - export CI_REPO_NAME=learn-gitlab
    - reviewdog -reporter=gitlab-push-commit
  only:
    - master
    - pushes
</code></pre>
<p>.gitlab-ci.yml的具体字段含义可以参考gitlab文档。在这个配置中，值得注意的有几点：</p>
<ul>
<li>使用tags关联runner(这里用ard这个tag)；</li>
<li>script部分是job具体执行的命令列表，这里先设置CI_REPO_OWNER和CI_REPO_NAME两个环境变量，供reviewdog使用；然后执行reviewdog；</li>
<li>only部分描述仅针对master分支的push事件触发ci job。</li>
</ul>
<h4>4. 配置.reviewdog.yml</h4>
<p>最后，我们来配置一下适合实验项目的reviewdog的配置文件。我们同样在项目根目录下建立.reviewdog.yml文件，其内容如下：</p>
<pre><code>runner:
  golangci:
    cmd: golangci-lint run --max-same-issues=0 --out-format=line-number ./...
    errorformat:
      - '%E%f:%l:%c: %m'
      - '%E%f:%l: %m'
      - '%C%.%#'
    level: warning
</code></pre>
<p>在这里我们看到，我们使用golangci-lint这个静态检查工具对实验项目的代码进行检查。这里的&#8211;max-same-issues=0的含义是不限制相同错误的数量。至于.reviewdog.yml的具体格式，reviewdog项目自身的<a href="https://github.com/bigwhite/reviewdog/blob/master/.reviewdog.yml">.reviewdog.yml</a>很具参考价值，大家需要时可以仔细研究。</p>
<h4>5. 推送代码并验证reviewdog的执行结果</h4>
<p>我们可以故意在代码中写下有问题的一些代码，这些问题要保证可以被golangci-lint工具扫描出来，比如：</p>
<pre><code>package main

type Foo struct {
    A int
    B string
    C bool
}

func Demo1() error {
    return nil
}

func Demo2() error {
    return nil
}

func Demo3() error {
    return nil
}

func main() {
    f := &amp;Foo{1, "tony", false}
    _ = f
    Demo2()
    Demo1()
    Demo3()
}
</code></pre>
<p>这里并没有对Demo函数调用进行错误处理，golangci-lint中的errcheck可以检测出这个问题。提交并push这些代码到仓库，稍等片刻，我们便可收到notify mail，打开commit页面，便会看到下面这样的commit comments：</p>
<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-5.png" alt="" /></p>
<p>看到这样的结果，说明reviewdog按预期工作了！</p>
<h3>五. 小结</h3>
<p>本文介绍了如何基于reviewdog对push提交的commit进行静态代码检查并像一个“同行”一样在commit中提交评论的方法。</p>
<p>这样做的目的就是希望通过工具提升代码评审的效率，同时也守住代码质量的下限。</p>
<p>就像本文开始所说的那样，随着检查工具能力的增强，这样的基于reviewdog自动检查代码的方案在保证代码质量方面还可以继续提升。</p>
<p>Go开源了go/ast等工具链，有能力的童鞋可以基于go/ast自行开发具有“特定目的”的检查工具并集成到reviewdog中，这将使得检查更有针对性和有效性。</p>
<p>本文涉及源码在<a href="https://github.com/bigwhite/reviewdog">这里</a>下载 &#8211; https://github.com/bigwhite/reviewdog/</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Go开发Kubernetes Operator：基本结构</title>
		<link>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/</link>
		<comments>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/#comments</comments>
		<pubDate>Mon, 15 Aug 2022 14:47:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[controller]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[CR]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[CustomResourceDefinition]]></category>
		<category><![CDATA[DaemonSet]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[framework]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubebuilder]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[operator]]></category>
		<category><![CDATA[operator-framework]]></category>
		<category><![CDATA[operator-sdk]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[reconcile]]></category>
		<category><![CDATA[reconciliation]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[replicas]]></category>
		<category><![CDATA[ReplicaSet]]></category>
		<category><![CDATA[resource]]></category>
		<category><![CDATA[role]]></category>
		<category><![CDATA[role-binding]]></category>
		<category><![CDATA[scale]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[service-account]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[TPR]]></category>
		<category><![CDATA[webserver]]></category>
		<category><![CDATA[伸缩]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3638</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1 注：文章首图基于《Kubernetes Operators Explained》修改 几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像Operator这样的应用模式和扩展方式日益受到开发者与运维者的欢迎。 我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的拿手好戏，是时候来研究一下operator了。 一. Operator的优点 kubernetes operator的概念最初来自CoreOS &#8211; 一家被红帽(redhat)收购的容器技术公司。 CoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：etcd operator和prometheus operator。 注：etcd于2013年由CoreOS以开源形式发布；prometheus作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。 下面是CoreOS对Operator这一概念的诠释：Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序。 图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档) Operator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。 那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比： 通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。 我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。 在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。 我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。 不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面： 对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入； 从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：kubebuilder、operator framework sdk等； operator的能力也有高低之分，operator framework就提出了一个包含五个等级的operator能力模型(CAPABILITY MODEL)，见下图。使用Go开发高能力等级的operator需要对client-go这个kubernetes官方go client库中的API有深入的了解。 图：operator能力模型(截图来自operator framework官网) 当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。 二. Kubernetes resource、resource type、API和controller介绍 Kubernetes发展到今天，其本质已经显现： Kubernetes就是一个“数据库”(数据实际持久存储在etcd中)； 其API就是“sql语句”； API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)； 每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)； 每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)； [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1">本文永久链接</a> &#8211; https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1</p>
<blockquote>
<p>注：文章首图基于《Kubernetes Operators Explained》修改</p>
</blockquote>
<p><a href="https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/">几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准</a>，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/">Operator这样的应用模式和扩展方式</a>日益受到开发者与运维者的欢迎。</p>
<p>我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的<strong>拿手好戏</strong>，是时候来研究一下operator了。</p>
<h3>一. Operator的优点</h3>
<p><a href="https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html">kubernetes operator的概念最初来自CoreOS</a> &#8211; 一家被红帽(redhat)收购的容器技术公司。</p>
<p>CoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：<a href="https://web.archive.org/web/20170224100544/https://coreos.com/blog/introducing-the-etcd-operator.html">etcd operator</a>和<a href="https://web.archive.org/web/20170224101137/https://coreos.com/blog/the-prometheus-operator.html">prometheus operator</a>。</p>
<blockquote>
<p>注：<a href="https://etcd.io">etcd</a>于2013年由CoreOS以开源形式发布；<a href="https://prometheus.io">prometheus</a>作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。</p>
</blockquote>
<p>下面是CoreOS对Operator这一概念的诠释：<strong>Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-4.png" alt="" /><br />
<center>图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档)</center></p>
<p>Operator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。</p>
<p>那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-2.png" alt="" /></p>
<p>通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。</p>
<p>我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。</p>
<p>在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。</p>
<p>我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。</p>
<p>不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面：</p>
<ul>
<li>对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入；</li>
<li>从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：<a href="https://github.com/kubernetes-sigs/kubebuilder">kubebuilder</a>、<a href="https://sdk.operatorframework.io">operator framework sdk</a>等；</li>
<li>operator的能力也有高低之分，operator framework就提出了一个包含<strong>五个等级的operator能力模型(CAPABILITY MODEL)</strong>，见下图。使用Go开发高能力等级的operator需要对<a href="https://github.com/kubernetes/client-go">client-go</a>这个kubernetes官方go client库中的API有深入的了解。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-3.png" alt="" /><br />
<center>图：operator能力模型(截图来自operator framework官网)</center></p>
<p>当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。</p>
<h3>二. Kubernetes resource、resource type、API和controller介绍</h3>
<p>Kubernetes发展到今天，其本质已经显现：</p>
<ul>
<li>Kubernetes就是一个“数据库”(数据实际持久存储在etcd中)；</li>
<li>其API就是“sql语句”；</li>
<li>API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)；</li>
<li>每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)；</li>
<li>每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)；</li>
<li>Kubernetes这个“数据库”内置了很多“表”，比如Pod、Deployment、DaemonSet、ReplicaSet等；</li>
</ul>
<p>下面是一个Kubernetes API与resource关系的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-5.png" alt="" /></p>
<p>我们看到resource type有两类，一类的namespace相关的(namespace-scoped)，我们通过下面形式的API操作这类resource type的实例：</p>
<pre><code>VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE - 操作某特定namespace下面的resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME - 操作某特定namespace下面的resource type中的某个具体的resource实例
</code></pre>
<p>另外一类则是namespace无关，即cluster范围(cluster-scoped)的，我们通过下面形式的API对这类resource type的实例进行操作：</p>
<pre><code>VERB /apis/GROUP/VERSION/RESOURCETYPE - 操作resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/RESOURCETYPE/NAME - 操作resource type中的某个具体的resource实例
</code></pre>
<p>我们知道Kubernetes并非真的只是一个“数据库”，它是服务编排和容器调度的平台标准，它的基本调度单元是Pod(也是一个resource type)，即一组容器的集合。那么Pod又是如何被创建、更新和删除的呢？这就离不开控制器(controller)了。<strong>每一类resource type都有自己对应的控制器(controller)</strong>。以pod这个resource type为例，它的controller为ReplicasSet的实例。</p>
<p>控制器的运行逻辑如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-6.png" alt="" /><br />
<center>图：控制器运行逻辑(引自《Kubernetes Operators Explained》一文)</center></p>
<p>控制器一旦启动，将尝试获得resource的当前状态(current state)，并与存储在k8s中的resource的期望状态（desired state，即spec)做比对，如果不一致，controller就会调用相应API进行调整，尽力使得current state与期望状态达成一致。这个达成一致的过程被称为<strong>协调(reconciliation)</strong>，协调过程的伪代码逻辑如下：</p>
<pre><code>for {
    desired := getDesiredState()
    current := getCurrentState()
    makeChanges(desired, current)
}
</code></pre>
<blockquote>
<p>注：k8s中有一个object的概念？那么object是什么呢？它类似于Java Object基类或Ruby中的Object超类。不仅resource type的实例resource是一个(is-a)object，resource type本身也是一个object，它是kubernetes concept的实例。</p>
</blockquote>
<p>有了上面对k8s这些概念的初步理解，我们下面就来理解一下Operator究竟是什么！</p>
<h3>三. Operator模式 = 操作对象(CRD) + 控制逻辑(controller)</h3>
<p>如果让运维人员直面这些内置的resource type(如deployment、pod等)，也就是前面“使用operator vs. 不使用operator”对比图中的第二种情况, 运维人员面临的情况将会很复杂，且操作易错。</p>
<p>那么如果不直面内置的resource type，那么我们如何自定义resource type呢, Kubernetes提供了Custom Resource Definition，CRD(在coreos刚提出operator概念的时候，crd的前身是Third Party Resource, TPR)可以用于自定义resource type。</p>
<p>根据前面我们对resource type理解，定义CRD相当于建立新“表”(resource type)，一旦CRD建立，k8s会为我们自动生成对应CRD的API endpoint，我们就可以通过yaml或API来操作这个“表”。我们可以向“表”中“插入”数据，即基于CRD创建Custom Resource(CR)，这就好比我们创建Deployment实例，向Deployment“表”中插入数据一样。</p>
<p>和原生内置的resource type一样，光有存储对象状态的CR还不够，原生resource type有对应controller负责协调(reconciliation)实例的创建、伸缩与删除，CR也需要这样的“协调者”，即我们也需要定义一个controller来负责监听CR状态并管理CR创建、伸缩、删除以及保持期望状态(spec)与当前状态(current state)的一致。这个controller不再是面向原生Resource type的实例，而是<strong>面向CRD的实例CR的controller</strong>。</p>
<p>有了自定义的操作对象类型(CRD)，有了面向操作对象类型实例的controller，我们将其打包为一个概念：“Operator模式”，operator模式中的controller也被称为operator，它是在集群中对CR进行维护操作的主体。</p>
<h3>四. 使用kubebuilder开发webserver operator</h3>
<blockquote>
<p>假设：此时你的本地开发环境已经具备访问实验用k8s环境的一切配置，通过kubectl工具可以任意操作k8s。</p>
</blockquote>
<p><strong>再深入浅出的概念讲解都不如一次实战对理解概念更有帮助</strong>，下面我们就来开发一个简单的Operator。</p>
<p>前面提过operator开发非常verbose，因此社区提供了开发工具和框架来帮助开发人员简化开发过程，目前主流的包括operator framework sdk和kubebuilder，前者是redhat开源并维护的一套工具，支持使用go、ansible、helm进行operator开发(其中只有go可以开发到能力级别5的operator，其他两种则不行)；而kubebuilder则是kubernetes官方的一个sig(特别兴趣小组)维护的operator开发工具。目前基于operator framework sdk和go进行operator开发时，operator sdk底层使用的也是kubebuilder，所以这里我们就直接使用kubebuilder来开发operator。</p>
<p>按照operator能力模型，我们这个operator差不多处于2级这个层次，我们定义一个Webserver的resource type，它代表的是一个基于nginx的webserver集群，我们的operator支持创建webserver示例(一个nginx集群)，支持nginx集群伸缩，支持集群中nginx的版本升级。</p>
<p>下面我们就用kubebuilder来实现这个operator！</p>
<h4>1. 安装kubebuilder</h4>
<p>这里我们采用源码构建方式安装，步骤如下：</p>
<pre><code>$git clone git@github.com:kubernetes-sigs/kubebuilder.git
$cd kubebuilder
$make
$cd bin
$./kubebuilder version
Version: main.version{KubeBuilderVersion:"v3.5.0-101-g5c949c2e",
KubernetesVendor:"unknown",
GitCommit:"5c949c2e50ca8eec80d64878b88e1b2ee30bf0bc",
BuildDate:"2022-08-06T09:12:50Z", GoOs:"linux", GoArch:"amd64"}
</code></pre>
<p>然后将bin/kubebuilder拷贝到你的PATH环境变量中的某个路径下即可。</p>
<h4>2. 创建webserver-operator工程</h4>
<p>接下来，我们就可以使用kubebuilder创建webserver-operator工程了：</p>
<pre><code>$mkdir webserver-operator
$cd webserver-operator
$kubebuilder init  --repo github.com/bigwhite/webserver-operator --project-name webserver-operator

Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.12.2
go: downloading k8s.io/client-go v0.24.2
go: downloading k8s.io/component-base v0.24.2
Update dependencies:
$ go mod tidy
Next: define a resource with:
kubebuilder create api
</code></pre>
<blockquote>
<p>注：&#8211;repo指定go.mod中的module root path，你可以定义你自己的module root path。</p>
</blockquote>
<h4>3. 创建API，生成初始CRD</h4>
<p>Operator包括CRD和controller，这里我们就来建立自己的CRD，即自定义的resource type，也就是API的endpoint，我们使用下面kubebuilder create命令来完成这个步骤：</p>
<pre><code>$kubebuilder create api --version v1 --kind WebServer
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/webserver_types.go
controllers/webserver_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
</code></pre>
<p>之后，我们执行make manifests来生成最终CRD对应的yaml文件：</p>
<pre><code>$make manifests
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
</code></pre>
<p>此刻，整个工程的目录文件布局如下：</p>
<pre><code>$tree -F .
.
├── api/
│   └── v1/
│       ├── groupversion_info.go
│       ├── webserver_types.go
│       └── zz_generated.deepcopy.go
├── bin/
│   └── controller-gen*
├── config/
│   ├── crd/
│   │   ├── bases/
│   │   │   └── my.domain_webservers.yaml
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches/
│   │       ├── cainjection_in_webservers.yaml
│   │       └── webhook_in_webservers.yaml
│   ├── default/
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager/
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus/
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac/
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── role_binding.yaml
│   │   ├── role.yaml
│   │   ├── service_account.yaml
│   │   ├── webserver_editor_role.yaml
│   │   └── webserver_viewer_role.yaml
│   └── samples/
│       └── _v1_webserver.yaml
├── controllers/
│   ├── suite_test.go
│   └── webserver_controller.go
├── Dockerfile
├── go.mod
├── go.sum
├── hack/
│   └── boilerplate.go.txt
├── main.go
├── Makefile
├── PROJECT
└── README.md

14 directories, 40 files
</code></pre>
<h4>4. webserver-operator的基本结构</h4>
<p>忽略我们此次不关心的诸如leader election、auth_proxy等，我将这个operator例子的主要部分整理到下面这张图中：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-7.png" alt="" /></p>
<p>图中的各个部分就是使用kubebuilder生成的<strong>operator的基本结构</strong>。</p>
<p>webserver operator主要由CRD和controller组成：</p>
<ul>
<li>CRD</li>
</ul>
<p>图中的左下角的框框就是上面生成的CRD yaml文件：config/crd/bases/my.domain_webservers.yaml。CRD与api/v1/webserver_types.go密切相关。我们在api/v1/webserver_types.go中为CRD定义spec相关字段，之后make manifests命令可以解析webserver_types.go中的变化并更新CRD的yaml文件。</p>
<ul>
<li>controller</li>
</ul>
<p>从图的右侧部分可以看出，controller自身就是作为一个deployment部署在k8s集群中运行的，它监视CRD的实例CR的运行状态，并在Reconcile方法中检查预期状态与当前状态是否一致，如果不一致，则执行相关操作。</p>
<ul>
<li>其它</li>
</ul>
<p>图中左上角是有关controller的权限的设置，controller通过serviceaccount访问k8s API server，通过role.yaml和role_binding.yaml设置controller的角色和权限。</p>
<h4>5. 为CRD spec添加字段(field)</h4>
<p>为了实现Webserver operator的功能目标，我们需要为CRD spec添加一些状态字段。前面说过，CRD与api中的webserver_types.go文件是同步的，我们只需修改webserver_types.go文件即可。我们在WebServerSpec结构体中增加Replicas和Image两个字段，它们分别用于表示webserver实例的副本数量以及使用的容器镜像：</p>
<pre><code>// api/v1/webserver_types.go

// WebServerSpec defines the desired state of WebServer
type WebServerSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // The number of replicas that the webserver should have
    Replicas int `json:"replicas,omitempty"`

    // The container image of the webserver
    Image string `json:"image,omitempty"`

    // Foo is an example field of WebServer. Edit webserver_types.go to remove/update
    Foo string `json:"foo,omitempty"`
}
</code></pre>
<p>保存修改后，<strong>执行make manifests</strong>重新生成config/crd/bases/my.domain_webservers.yaml</p>
<pre><code>$cat my.domain_webservers.yaml
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.9.2
  creationTimestamp: null
  name: webservers.my.domain
spec:
  group: my.domain
  names:
    kind: WebServer
    listKind: WebServerList
    plural: webservers
    singular: webserver
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: WebServer is the Schema for the webservers API
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            description: WebServerSpec defines the desired state of WebServer
            properties:
              foo:
                description: Foo is an example field of WebServer. Edit webserver_types.go
                  to remove/update
                type: string
              image:
                description: The container image of the webserver
                type: string
              replicas:
                description: The number of replicas that the webserver should have
                type: integer
            type: object
          status:
            description: WebServerStatus defines the observed state of WebServer
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}
</code></pre>
<p>一旦定义完CRD，我们就可以将其安装到k8s中：</p>
<pre><code>$make install
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
{Version:kustomize/v3.8.7 GitCommit:ad092cc7a91c07fdf63a2e4b7f13fa588a39af4f BuildDate:2020-11-11T23:14:14Z GoOs:linux GoArch:amd64}
kustomize installed to /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain created
</code></pre>
<p>检查安装情况：</p>
<pre><code>$kubectl get crd|grep webservers
webservers.my.domain                                             2022-08-06T21:55:45Z
</code></pre>
<h4>6. 修改role.yaml</h4>
<p>在开始controller开发之前，我们先来为controller后续的运行“铺平道路”，即设置好相应权限。</p>
<p>我们在controller中会为CRD实例创建对应deployment和service，这样就要求controller有操作deployments和services的权限，这样就需要我们修改role.yaml，增加service account:  controller-manager 操作deployments和services的权限：</p>
<pre><code>// config/rbac/role.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  creationTimestamp: null
  name: manager-role
rules:
- apiGroups:
  - my.domain
  resources:
  - webservers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - my.domain
  resources:
  - webservers/finalizers
  verbs:
  - update
- apiGroups:
  - my.domain
  resources:
  - webservers/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - apps
  - ""
  resources:
  - services
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
</code></pre>
<p>修改后的role.yaml先放在这里，后续与controller一并部署到k8s上。</p>
<h4>7. 实现controller的Reconcile(协调)逻辑</h4>
<p>kubebuilder为我们搭好了controller的代码架子，我们只需要在controllers/webserver_controller.go中实现WebServerReconciler的Reconcile方法即可。下面是Reconcile的一个简易流程图，结合这幅图理解代码就容易的多了：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-8.png" alt="" /></p>
<p>下面是对应的Reconcile方法的代码：</p>
<pre><code>// controllers/webserver_controller.go

func (r *WebServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := r.Log.WithValues("Webserver", req.NamespacedName)

    instance := &amp;mydomainv1.WebServer{}
    err := r.Get(ctx, req.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Return and don't requeue
            log.Info("Webserver resource not found. Ignoring since object must be deleted")
            return ctrl.Result{}, nil
        }

        // Error reading the object - requeue the request.
        log.Error(err, "Failed to get Webserver")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Check if the webserver deployment already exists, if not, create a new one
    found := &amp;appsv1.Deployment{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, found)
    if err != nil &amp;&amp; errors.IsNotFound(err) {
        // Define a new deployment
        dep := r.deploymentForWebserver(instance)
        log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
        err = r.Create(ctx, dep)
        if err != nil {
            log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Deployment created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Deployment")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Ensure the deployment replicas and image are the same as the spec
    var replicas int32 = int32(instance.Spec.Replicas)
    image := instance.Spec.Image

    var needUpd bool
    if *found.Spec.Replicas != replicas {
        log.Info("Deployment spec.replicas change", "from", *found.Spec.Replicas, "to", replicas)
        found.Spec.Replicas = &amp;replicas
        needUpd = true
    }

    if (*found).Spec.Template.Spec.Containers[0].Image != image {
        log.Info("Deployment spec.template.spec.container[0].image change", "from", (*found).Spec.Template.Spec.Containers[0].Image, "to", image)
        found.Spec.Template.Spec.Containers[0].Image = image
        needUpd = true
    }

    if needUpd {
        err = r.Update(ctx, found)
        if err != nil {
            log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Spec updated - return and requeue
        return ctrl.Result{Requeue: true}, nil
    }

    // Check if the webserver service already exists, if not, create a new one
    foundService := &amp;corev1.Service{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name + "-service", Namespace: instance.Namespace}, foundService)
    if err != nil &amp;&amp; errors.IsNotFound(err) {
        // Define a new service
        srv := r.serviceForWebserver(instance)
        log.Info("Creating a new Service", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
        err = r.Create(ctx, srv)
        if err != nil {
            log.Error(err, "Failed to create new Servie", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Service created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Service")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Tbd: Ensure the service state is the same as the spec, your homework

    // reconcile webserver operator in again 10 seconds
    return ctrl.Result{RequeueAfter: time.Second * 10}, nil
}
</code></pre>
<p>这里大家可能发现了：<strong>原来CRD的controller最终还是将CR翻译为k8s原生Resource，比如service、deployment等。CR的状态变化(比如这里的replicas、image等)最终都转换成了deployment等原生resource的update操作</strong>，这就是operator的精髓！理解到这一层，operator对大家来说就不再是什么密不可及的概念了。</p>
<p>有些朋友可能也会发现，上面流程图中似乎没有考虑CR实例被删除时对deployment、service的操作，的确如此。不过对于一个7&#215;24小时运行于后台的服务来说，我们更多关注的是其变更、伸缩、升级等操作，删除是优先级最低的需求。</p>
<h4>8. 构建controller image</h4>
<p>controller代码写完后，我们就来构建controller的image。通过前文我们知道，这个controller其实就是运行在k8s中的一个deployment下的pod。我们需要构建其image并通过deployment部署到k8s中。</p>
<p>kubebuilder创建的operator工程中包含了Makefile，通过make docker-build即可构建controller image。docker-build使用golang builder image来构建controller源码，不过如果不对Dockerfile稍作修改，你很难编译过去，因为默认GOPROXY在国内无法访问。这里最简单的改造方式是使用vendor构建，下面是改造后的Dockerfile：</p>
<pre><code># Build the manager binary
FROM golang:1.18 as builder

ENV GOPROXY https://goproxy.cn
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
COPY vendor/ vendor/
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
#RUN go mod download

# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
#FROM gcr.io/distroless/static:nonroot
FROM katanomi/distroless-static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]
</code></pre>
<p>下面是构建的步骤：</p>
<pre><code>$go mod vendor
$make docker-build

test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
KUBEBUILDER_ASSETS="/home/tonybai/.local/share/kubebuilder-envtest/k8s/1.24.2-linux-amd64" go test ./... -coverprofile cover.out
?       github.com/bigwhite/webserver-operator    [no test files]
?       github.com/bigwhite/webserver-operator/api/v1    [no test files]
ok      github.com/bigwhite/webserver-operator/controllers    4.530s    coverage: 0.0% of statements
docker build -t bigwhite/webserver-controller:latest .
Sending build context to Docker daemon  47.51MB
Step 1/15 : FROM golang:1.18 as builder
 ---&gt; 2d952adaec1e
Step 2/15 : ENV GOPROXY https://goproxy.cn
 ---&gt; Using cache
 ---&gt; db2b06a078e3
Step 3/15 : WORKDIR /workspace
 ---&gt; Using cache
 ---&gt; cc3c613c19c6
Step 4/15 : COPY go.mod go.mod
 ---&gt; Using cache
 ---&gt; 5fa5c0d89350
Step 5/15 : COPY go.sum go.sum
 ---&gt; Using cache
 ---&gt; 71669cd0fe8e
Step 6/15 : COPY vendor/ vendor/
 ---&gt; Using cache
 ---&gt; 502b280a0e67
Step 7/15 : COPY main.go main.go
 ---&gt; Using cache
 ---&gt; 0c59a69091bb
Step 8/15 : COPY api/ api/
 ---&gt; Using cache
 ---&gt; 2b81131c681f
Step 9/15 : COPY controllers/ controllers/
 ---&gt; Using cache
 ---&gt; e3fd48c88ccb
Step 10/15 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go
 ---&gt; Using cache
 ---&gt; 548ac10321a2
Step 11/15 : FROM katanomi/distroless-static:nonroot
 ---&gt; 421f180b71d8
Step 12/15 : WORKDIR /
 ---&gt; Running in ea7cb03027c0
Removing intermediate container ea7cb03027c0
 ---&gt; 9d3c0ea19c3b
Step 13/15 : COPY --from=builder /workspace/manager .
 ---&gt; a4387fe33ab7
Step 14/15 : USER 65532:65532
 ---&gt; Running in 739a32d251b6
Removing intermediate container 739a32d251b6
 ---&gt; 52ae8742f9c5
Step 15/15 : ENTRYPOINT ["/manager"]
 ---&gt; Running in 897893b0c9df
Removing intermediate container 897893b0c9df
 ---&gt; e375cc2adb08
Successfully built e375cc2adb08
Successfully tagged bigwhite/webserver-controller:latest
</code></pre>
<blockquote>
<p>注：执行make命令之前，先将Makefile中的IMG变量初值改为IMG ?= bigwhite/webserver-controller:latest</p>
</blockquote>
<p>构建成功后，执行make docker-push将image推送到镜像仓库中(这里使用了docker公司提供的公共仓库)。</p>
<h4>9. 部署controller</h4>
<p>之前我们已经通过make install将CRD安装到k8s中了，接下来再把controller部署到k8s上，我们的operator就算部署完毕了。执行make deploy即可实现部署：</p>
<pre><code>$make deploy
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
cd config/manager &amp;&amp; /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize edit set image controller=bigwhite/webserver-controller:latest
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/default | kubectl apply -f -
namespace/webserver-operator-system created
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain unchanged
serviceaccount/webserver-operator-controller-manager created
role.rbac.authorization.k8s.io/webserver-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/webserver-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/webserver-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-proxy-rolebinding created
configmap/webserver-operator-manager-config created
service/webserver-operator-controller-manager-metrics-service created
deployment.apps/webserver-operator-controller-manager created
</code></pre>
<p>我们看到deploy不仅会安装controller、serviceaccount、role、rolebinding，它还会创建namespace，也会将crd安装一遍。也就是说deploy是一个完整的operator安装命令。</p>
<blockquote>
<p>注：使用make undeploy可以完整卸载operator相关resource。</p>
</blockquote>
<p>我们用kubectl logs查看一下controller的运行日志：</p>
<pre><code>$kubectl logs -f deployment.apps/webserver-operator-controller-manager -n webserver-operator-system
1.6600280818476188e+09    INFO    controller-runtime.metrics    Metrics server is starting to listen    {"addr": "127.0.0.1:8080"}
1.6600280818478029e+09    INFO    setup    starting manager
1.6600280818480284e+09    INFO    Starting server    {"path": "/metrics", "kind": "metrics", "addr": "127.0.0.1:8080"}
1.660028081848097e+09    INFO    Starting server    {"kind": "health probe", "addr": "[::]:8081"}
I0809 06:54:41.848093       1 leaderelection.go:248] attempting to acquire leader lease webserver-operator-system/63e5a746.my.domain...
I0809 06:54:57.072336       1 leaderelection.go:258] successfully acquired lease webserver-operator-system/63e5a746.my.domain
1.6600280970724037e+09    DEBUG    events    Normal    {"object": {"kind":"Lease","namespace":"webserver-operator-system","name":"63e5a746.my.domain","uid":"e05aaeb5-4a3a-4272-b036-80d61f0b6788","apiVersion":"coordination.k8s.io/v1","resourceVersion":"5238800"}, "reason": "LeaderElection", "message": "webserver-operator-controller-manager-6f45bc88f7-ptxlc_0e960015-9fbe-466d-a6b1-ff31af63a797 became leader"}
1.6600280970724993e+09    INFO    Starting EventSource    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "source": "kind source: *v1.WebServer"}
1.6600280970725305e+09    INFO    Starting Controller    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer"}
1.660028097173026e+09    INFO    Starting workers    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "worker count": 1}
</code></pre>
<p>可以看到，controller已经成功启动，正在等待一个WebServer CR的相关事件(比如创建)！下面我们就来创建一个WebServer CR!</p>
<h4>10. 创建WebServer CR</h4>
<p>webserver-operator项目中有一个CR sample，位于config/samples下面，我们对其进行改造，添加我们在spec中加入的字段：</p>
<pre><code>// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 3
</code></pre>
<p>我们通过kubectl创建该WebServer CR：</p>
<pre><code>$cd config/samples
$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample created
</code></pre>
<p>观察controller的日志：</p>
<pre><code>1.6602084232243123e+09  INFO    controllers.WebServer   Creating a new Deployment   {"Webserver": "default/webserver-sample", "Deployment.Namespace": "default", "Deployment.Name": "webserver-sample"}
1.6602084233446114e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}
</code></pre>
<p>我们看到当CR被创建后，controller监听到相关事件，创建了对应的Deployment和service，我们查看一下为CR创建的Deployment、三个Pod以及service：</p>
<pre><code>$kubectl get service
NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes                 ClusterIP   172.26.0.1     &lt;none&gt;        443/TCP        22d
webserver-sample-service   NodePort    172.26.173.0   &lt;none&gt;        80:30010/TCP   2m58s

$kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
webserver-sample   3/3     3            3           4m44s

$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          4m52s
</code></pre>
<p>我们访问一下该服务：</p>
<pre><code>$curl http://192.168.10.182:30010
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>服务如预期返回响应！</p>
<h4>11. 伸缩、变更版本和Service自愈</h4>
<p>接下来我们来对CR做一些常见的运维操作。</p>
<ul>
<li>副本数由3变为4</li>
</ul>
<p>我们将CR的replicas由3改为4，对容器实例做一次扩展操作：</p>
<pre><code>// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 4
</code></pre>
<p>然后通过kubectl apply使之生效：</p>
<pre><code>$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample configured
</code></pre>
<p>上述命令执行后，我们观察到operator的controller日志如下：</p>
<pre><code>1.660208962767797e+09   INFO    controllers.WebServer   Deployment spec.replicas change {"Webserver": "default/webserver-sample", "from": 3, "to": 4}
</code></pre>
<p>稍后，查看pod数量：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-v9gvg   1/1     Running   0          42s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          9m41s
</code></pre>
<p>webserver pod副本数量成功从3扩为4。</p>
<ul>
<li>变更webserver image版本</li>
</ul>
<p>我们将CR的image的版本从nginx:1.23.1改为nginx:1.23.0，然后执行kubectl apply使之生效。</p>
<p>我们查看controller的响应日志如下：</p>
<pre><code>1.6602090494113188e+09  INFO    controllers.WebServer   Deployment spec.template.spec.container[0].image change {"Webserver": "default/webserver-sample", "from": "nginx:1.23.1", "to": "nginx:1.23.0"}
</code></pre>
<p>controller会更新deployment，导致所辖pod进行滚动升级：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS              RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running             0          10m
webserver-sample-bc698b9fb-vk6gw   1/1     Running             0          10m
webserver-sample-bc698b9fb-xgrgb   1/1     Running             0          10m
webserver-sample-ffcf549ff-g6whk   0/1     ContainerCreating   0          12s
webserver-sample-ffcf549ff-ngjz6   0/1     ContainerCreating   0          12s
</code></pre>
<p>耐心等一小会儿，最终的pod列表为：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-ffcf549ff-g6whk   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-m6z24   1/1     Running   0          3m12s
webserver-sample-ffcf549ff-ngjz6   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-t7gvc   1/1     Running   0          4m16s
</code></pre>
<ul>
<li>service自愈：恢复被无删除的Service</li>
</ul>
<p>我们来一次“误操作”，将webserver-sample-service删除，看看controller能否帮助service自愈：</p>
<pre><code>$kubectl delete service/webserver-sample-service
service "webserver-sample-service" deleted
</code></pre>
<p>查看controller日志：</p>
<pre><code>1.6602096994710526e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}
</code></pre>
<p>我们看到controller检测到了service被删除的状态，并重建了一个新service！</p>
<p>访问新建的service：</p>
<pre><code>$curl http://192.168.10.182:30010
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>可以看到service在controller的帮助下完成了自愈！</p>
<h3>五. 小结</h3>
<p>本文对Kubernetes Operator的概念以及优点做了初步的介绍，并基于kubebuilder这个工具开发了一个具有2级能力的operator。当然这个operator离完善还有很远的距离，其主要目的还是帮助大家理解operator的概念以及实现套路。</p>
<p>相信你阅读完本文后，对operator，尤其是其基本结构会有一个较为清晰的了解，并具备开发简单operator的能力！</p>
<p>文中涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/webserver-operator">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/tree/master/webserver-operator。</p>
<h3>六. 参考资料</h3>
<ul>
<li>kubernetes operator 101, Part 1: Overview and key features &#8211; https://developers.redhat.com/articles/2021/06/11/kubernetes-operators-101-part-1-overview-and-key-features</li>
<li>Kubernetes Operators 101, Part 2: How operators work &#8211; https://developers.redhat.com/articles/2021/06/22/kubernetes-operators-101-part-2-how-operators-work</li>
<li>Operator SDK: Build Kubernetes Operators &#8211; https://developers.redhat.com/blog/2020/04/28/operator-sdk-build-kubernetes-operators-and-deploy-them-on-openshift</li>
<li>kubernetes doc: Custom Resources &#8211; https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/</li>
<li>kubernetes doc: Operator pattern &#8211; https://kubernetes.io/docs/concepts/extend-kubernetes/operator/</li>
<li>kubernetes doc: API concepts &#8211; https://kubernetes.io/docs/reference/using-api/api-concepts/</li>
<li>Introducing Operators: Putting Operational Knowledge into Software 第一篇有关operator的文章 by coreos &#8211; https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html</li>
<li>CNCF Operator白皮书v1.0 &#8211; https://github.com/cncf/tag-app-delivery/blob/main/operator-whitepaper/v1/Operator-WhitePaper_v1-0.md</li>
<li>Best practices for building Kubernetes Operators and stateful apps &#8211; https://cloud.google.com/blog/products/containers-kubernetes/best-practices-for-building-kubernetes-operators-and-stateful-apps</li>
<li>A deep dive into Kubernetes controllers &#8211;  https://docs.bitnami.com/tutorials/a-deep-dive-into-kubernetes-controllers</li>
<li>Kubernetes Operators Explained &#8211; https://blog.container-solutions.com/kubernetes-operators-explained</li>
<li>书籍《Kubernetes Operator》 &#8211; https://book.douban.com/subject/34796009/</li>
<li>书籍《Programming Kubernetes》 &#8211; https://book.douban.com/subject/35498478/</li>
<li>Operator SDK Reaches v1.0 &#8211; https://cloud.redhat.com/blog/operator-sdk-reaches-v1.0</li>
<li>What is the difference between kubebuilder and operator-sdk &#8211; https://github.com/operator-framework/operator-sdk/issues/1758</li>
<li>Kubernetes Operators in Depth &#8211; https://www.infoq.com/articles/kubernetes-operators-in-depth/</li>
<li>Get started using Kubernetes Operators &#8211; https://developer.ibm.com/learningpaths/kubernetes-operators/ </li>
<li>Use Kubernetes operators to extend Kubernetes’ functionality &#8211; https://developer.ibm.com/learningpaths/kubernetes-operators/operators-extend-kubernetes/</li>
<li>memcached operator &#8211; https://github.com/operator-framework/operator-sdk-samples/tree/master/go/memcached-operator</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/feed/</wfw:commentRss>
		<slash:comments>12</slash:comments>
		</item>
		<item>
		<title>使用Go语言实现eBPF程序内核态与用户态的双向数据交换</title>
		<link>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/</link>
		<comments>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/#comments</comments>
		<pubDate>Mon, 25 Jul 2022 13:26:19 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpf2go]]></category>
		<category><![CDATA[bpfObjects]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BPF_MAP_TYPE_HASH]]></category>
		<category><![CDATA[BTF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CO-RE]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[falco]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-generate]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[isovalent]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katran]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[libbpf-bootstrap]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[llvm-objdump]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[pixie]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[readelf]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[submodule]]></category>
		<category><![CDATA[Thoughtworks]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[火焰图]]></category>
		<category><![CDATA[符号表]]></category>
		<category><![CDATA[系统调用]]></category>
		<category><![CDATA[网络]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3629</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go 在之前的两篇文章中，无论是使用C语言开发eBPF程序，还是使用Go开发的eBPF程序，都是hello world级别的，可能有用，但谈不上十分实用。 通常来说，一个实用的eBPF程序，它的内核态部分与用户态部分是有数据交换的，有了这种数据交换，eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性，eBPF MAP是绕不过去的机制。 在这一篇有关eBPF程序开发的文章中，我们就来看看如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换。 一. why BPF MAP？ 永远不要忘记BPF字节码是运行于OS内核态的代码，这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据，通常仅能通过系统调用陷入内核态来实现。因此，在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。 那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢？用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢？ Linux内核BPF开发者于是就引入了BPF MAP机制。BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道。同时由于bpf map存储在内核分配的内存空间，处于内核态，可以被运行于在内核态的多个BPF程序所共享，同样可以作为多个BPF程序交换和共享数据的机制。 二. BPF MAP不是狭义的map数据结构 BPF MAP究竟是什么呢？它不是我们狭义理解的哈希映射表的数据结构，而是一种通用数据结构，可以存储不同类型数据的通用数据结构。用著名内核BPF开发者Andrii Nakryiko的话来说，MAP就是BPF中代表抽象数据容器(abstract data container)的一个概念。 截至目前，内核BPF支持的MAP类型已经有20+种，下面是libbpf中bpf.h中列出的当前支持的MAP类型： // libbpf/include/uapi/linux/bpf.h enum bpf_map_type { BPF_MAP_TYPE_UNSPEC, BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_PROG_ARRAY, BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_PERCPU_ARRAY, BPF_MAP_TYPE_STACK_TRACE, BPF_MAP_TYPE_CGROUP_ARRAY, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH, BPF_MAP_TYPE_LPM_TRIE, BPF_MAP_TYPE_ARRAY_OF_MAPS, BPF_MAP_TYPE_HASH_OF_MAPS, BPF_MAP_TYPE_DEVMAP, BPF_MAP_TYPE_SOCKMAP, BPF_MAP_TYPE_CPUMAP, BPF_MAP_TYPE_XSKMAP, BPF_MAP_TYPE_SOCKHASH, BPF_MAP_TYPE_CGROUP_STORAGE, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY, BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go">本文永久链接</a> &#8211; https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go</p>
<p>在之前的两篇文章中，无论是<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">使用C语言开发eBPF程序</a>，还是<a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/">使用Go开发的eBPF程序</a>，都是hello world级别的，可能有用，但谈不上十分实用。</p>
<p>通常来说，一个实用的eBPF程序，它的内核态部分与用户态部分是有数据交换的，有了这种数据交换，eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性，<strong>eBPF MAP是绕不过去的机制</strong>。</p>
<p>在这一篇有关eBPF程序开发的文章中，我们就来看看<strong>如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换</strong>。</p>
<h3>一. why BPF MAP？</h3>
<p>永远不要忘记BPF字节码是运行于OS内核态的代码，这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据，通常仅能通过系统调用陷入内核态来实现。因此，在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。</p>
<p>那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢？用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢？</p>
<p>Linux内核BPF开发者于是就引入了<a href="https://www.kernel.org/doc/html/latest/bpf/maps.html">BPF MAP机制</a>。<strong>BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道</strong>。同时由于bpf map存储在内核分配的内存空间，处于内核态，可以被运行于在内核态的多个BPF程序所共享，同样可以作为多个BPF程序交换和共享数据的机制。</p>
<h3>二. BPF MAP不是狭义的map数据结构</h3>
<p>BPF MAP究竟是什么呢？它不是我们狭义理解的哈希映射表的数据结构，而是<a href="https://man7.org/linux/man-pages/man2/bpf.2.html">一种通用数据结构，可以存储不同类型数据的通用数据结构</a>。用著名内核BPF开发者<a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">Andrii Nakryiko</a>的话来说，<strong>MAP就是BPF中代表<a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">抽象数据容器(abstract data container)</a>的一个概念</strong>。</p>
<p>截至目前，内核BPF支持的MAP类型已经有20+种，下面是libbpf中bpf.h中列出的当前支持的MAP类型：</p>
<pre><code>// libbpf/include/uapi/linux/bpf.h
enum bpf_map_type {
    BPF_MAP_TYPE_UNSPEC,
    BPF_MAP_TYPE_HASH,
    BPF_MAP_TYPE_ARRAY,
    BPF_MAP_TYPE_PROG_ARRAY,
    BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    BPF_MAP_TYPE_PERCPU_HASH,
    BPF_MAP_TYPE_PERCPU_ARRAY,
    BPF_MAP_TYPE_STACK_TRACE,
    BPF_MAP_TYPE_CGROUP_ARRAY,
    BPF_MAP_TYPE_LRU_HASH,
    BPF_MAP_TYPE_LRU_PERCPU_HASH,
    BPF_MAP_TYPE_LPM_TRIE,
    BPF_MAP_TYPE_ARRAY_OF_MAPS,
    BPF_MAP_TYPE_HASH_OF_MAPS,
    BPF_MAP_TYPE_DEVMAP,
    BPF_MAP_TYPE_SOCKMAP,
    BPF_MAP_TYPE_CPUMAP,
    BPF_MAP_TYPE_XSKMAP,
    BPF_MAP_TYPE_SOCKHASH,
    BPF_MAP_TYPE_CGROUP_STORAGE,
    BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
    BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
    BPF_MAP_TYPE_QUEUE,
    BPF_MAP_TYPE_STACK,
    BPF_MAP_TYPE_SK_STORAGE,
    BPF_MAP_TYPE_DEVMAP_HASH,
    BPF_MAP_TYPE_STRUCT_OPS,
    BPF_MAP_TYPE_RINGBUF,
    BPF_MAP_TYPE_INODE_STORAGE,
    BPF_MAP_TYPE_TASK_STORAGE,
    BPF_MAP_TYPE_BLOOM_FILTER,
};
</code></pre>
<p>这里数据结构类型众多，但不是本文的重点，我们不一一介绍了。其中的BPF_MAP_TYPE_HASH类型是BPF支持的第一种MAP数据结构，这个类型可以理解为我们日常接触的hash映射表，通过键值对的形式索引数据。在后续的例子中我们将使用这种类型的MAP。</p>
<p>那么BPF MAP是如何可以在内核态与用户态共享数据的？原理是什么呢？</p>
<p>从<a href="https://man7.org/linux/man-pages/man2/bpf.2.html">bpf这个系统调用的说明</a>中，我们能找到端倪。下面是bpf系统调用的函数原型：</p>
<pre><code>// https://man7.org/linux/man-pages/man2/bpf.2.html

#include &lt;linux/bpf.h&gt;

int bpf(int cmd, union bpf_attr *attr, unsigned int size);
</code></pre>
<p>从bpf的原型来看，似乎比较简单。但bpf其实是一个“富调用”，即不止能干一件事，通过cmd传入的值不同，它可以围绕BPF完成很多事情。最主要的功能是加载bpf程序(cmd=BPF_PROG_LOAD)，其次是围绕MAP的一系列操作，包括创建MAP(cmd=BPF_MAP_CREATE)、MAP元素查询(cmd=BPF_MAP_LOOKUP_ELEM)、MAP元素值更新(cmd=BPF_MAP_UPDATE_ELEM)等。</p>
<p>当cmd=BPF_MAP_CREATE时，即bpf执行创建MAP的操作后，bpf调用会返回一个文件描述符fd，<strong>通过该fd后续可以操作新创建的MAP</strong>。通过fd访问map，这个<strong>很unix</strong>！</p>
<p>当然这么底层的系统调用，一般BPF用户态开发人员无需接触到，像libbpf就包装了一系列的map操作函数，这些函数不会暴露map fd给用户，简化了使用方法，提升了使用体验。</p>
<p>下面我们先来看一下如何用C语言实现基于map的BPF用户态与内核态的数据交换。</p>
<h3>三. 使用C基于libbpf使用map的示例</h3>
<p>这个示例改造自<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld">helloworld示例</a>。原helloworld示例在execve这个系统调用被调用时输出一条内核日志(在/sys/kernel/debug/tracing/trace_pipe中可以查看到)，用户态程序并没有与内核态程序做任何数据交换。</p>
<p>在这个新示例(execve_counter)中，我们依然跟踪系统调用execve，不同的是我们对execve进行调用计数，并将技术存储在BPF MAP中。而用户态部分程序则读取该MAP中的计数并定时输出计数值。</p>
<p>我们先来看看BPF内核态部分的源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter/execve_counter.bpf.c

#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;

typedef __u64 u64;
typedef char stringkey[64];

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 128);
    //__type(key, stringkey);
    stringkey* key;
    __type(value, u64);
} execve_counter SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
  stringkey key = "execve_counter";
  u64 *v = NULL;
  v = bpf_map_lookup_elem(&amp;execve_counter, &amp;key);
  if (v != NULL) {
    *v += 1;
  }
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
</code></pre>
<p>和helloworld示例不同，我们在新示例中定义了一个map结构execve_counter，通过SEC宏将其标记为BPF MAP变量。</p>
<p>这个map结构有四个字段：</p>
<ul>
<li>type: 使用的BPF MAP类型(参见前面的bpf_map_type枚举类型)，这里我们使用BPF_MAP_TYPE_HASH，即一个hash散列表结构；</li>
<li>max_entries：map内的key-value对的最大数量；</li>
<li>key: 指向key内存空间的指针。这里我们自定义了一个类型stringkey(char[64])来表示每个key元素的类型；</li>
<li>value: 指向value内存空间的指针，这里value元素的类型为u64，一个64位整型。</li>
</ul>
<p>内核态函数bpf_prog的实现也比较简单：在上面的map中查询”execve_counter”这个key，如果查到了，则将得到的value指针指向的内存中的值加1。</p>
<p>我们再来看看execve_counter这个示例的用户态部分的程序源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve_counter/execve_counter.c

#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include &lt;linux/bpf.h&gt;
#include "execve_counter.skel.h"

typedef __u64 u64;
typedef char stringkey[64];

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct execve_counter_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = execve_counter_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }

    /* Load &amp; verify BPF programs */
    err = execve_counter_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* init the counter */
    stringkey key = "execve_counter";
    u64 v = 0;
    err = bpf_map__update_elem(skel-&gt;maps.execve_counter, &amp;key, sizeof(key), &amp;v, sizeof(v), BPF_ANY);
    if (err != 0) {
        fprintf(stderr, "Failed to init the counter, %d\n", err);
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = execve_counter_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    for (;;) {
            // read counter value from map
            err = bpf_map__lookup_elem(skel-&gt;maps.execve_counter, &amp;key, sizeof(key), &amp;v, sizeof(v), BPF_ANY);
            if (err != 0) {
               fprintf(stderr, "Lookup key from map error: %d\n", err);
               goto cleanup;
            } else {
               printf("execve_counter is %llu\n", v);
            }

            sleep(5);
    }

cleanup:
    execve_counter_bpf__destroy(skel);
    return -err;
}
</code></pre>
<p>map是在execve_counter_bpf__load中完成的创建，跟踪代码你会发现(参考libbpf源码)，最终会调用bpf系统调用创建map。</p>
<p>和helloworld示例不同的是，我们在attach handler之前，先使用libbpf封装的bpf_map__update_elem初始化了bpf map中的key(初始化为0，如果没有这一步，第一次bpf程序执行时，会提示找不到key)。</p>
<p>然后attach handler后，我们在一个循环中每隔5s通过bpf_map__lookup_elem查询一下key=”execve_counter”的值并输出到控制台。</p>
<p>用户态程序之所以可以直接使用map，是因为bpftool基于execve_counter.bpf.c生成的execve_counter.skel.h中包含了map的各种信息。</p>
<p>接下来我们执行make编译一下这个ebpf程序，然后执行并观察输出：</p>
<pre><code>$sudo ./execve_counter
libbpf: loading object 'execve_counter_bpf' from buffer
libbpf: elf: section(3) tracepoint/syscalls/sys_enter_execve, size 192, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 24 insns (192 bytes)
libbpf: elf: section(4) .reltracepoint/syscalls/sys_enter_execve, size 16, link 22, flags 0, type=9
libbpf: elf: section(5) .rodata, size 64, link 0, flags 2, type=1
libbpf: elf: section(6) .maps, size 32, link 0, flags 3, type=1
libbpf: elf: section(7) license, size 13, link 0, flags 3, type=1
libbpf: license of execve_counter_bpf is Dual BSD/GPL
libbpf: elf: section(13) .BTF, size 898, link 0, flags 0, type=1
libbpf: elf: section(15) .BTF.ext, size 176, link 0, flags 0, type=1
libbpf: elf: section(22) .symtab, size 744, link 1, flags 0, type=2
libbpf: looking for externs among 31 symbols...
libbpf: collected 0 externs total
libbpf: map 'execve_counter': at sec_idx 6, offset 0.
libbpf: map 'execve_counter': found type = 1.
libbpf: map 'execve_counter': found key [9], sz = 64.
libbpf: map 'execve_counter': found value [13], sz = 8.
libbpf: map 'execve_counter': found max_entries = 128.
libbpf: map 'execve_c.rodata' (global data): at sec_idx 5, offset 0, flags 480.
libbpf: map 1 is "execve_c.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(3) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #15 against 'execve_counter'
libbpf: prog 'bpf_prog': found map 0 (execve_counter, sec 6, off 0) for insn #15
libbpf: map 'execve_counter': created successfully, fd=4
libbpf: map 'execve_c.rodata': created successfully, fd=5
execve_counter is 0
execve_counter is 0
execve_counter is 9
execve_counter is 23
... ...
</code></pre>
<blockquote>
<p>注：如果不知道如何编译execve_counter这个示例，请先移步<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>了解其构建原理。</p>
</blockquote>
<p>bpftool工具提供了查看map的特性，我们可以通过它查看示例创建的map：</p>
<pre><code>$sudo bpftool map
114: hash  name execve_counter  flags 0x0
    key 64B  value 8B  max_entries 128  memlock 20480B
    btf_id 120
116: array  name execve_c.rodata  flags 0x80
    key 4B  value 64B  max_entries 1  memlock 4096B
    frozen
</code></pre>
<p>我们还可以dump一下整个map：</p>
<pre><code>$sudo bpftool map dump id 114
[{
        "key": "execve_counter",
        "value": 23
    }
]
</code></pre>
<p>我们看到，整个map中就一个键值对(key=”execve_counter”)，其值与示例的用户态部分程序输出的一致。</p>
<p>好了，有了C示例作为基础，我们再来看看如何基于Go来实现这个示例。</p>
<h3>四. 使用Go基于cilium/ebpf实现execve-counter示例</h3>
<p>使用Go开发BPF用户态部分程序要容易的多，cilium/ebpf提供了的包用起来很简单。如果还不知道如何用Go开发ebpf用户态部分的套路，请先移步<a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go">《使用Go语言开发eBPF程序》</a>一文了解一下。</p>
<p>Go语言示例的必不可少的原料是execve_counter.bpf.c，这个C源码文件与上面的execve_counter示例中的execve_counter.bpf.c的唯一差别就是include的头文件改成了common.h：</p>
<pre><code>$diff execve_counter.bpf.c ../execve-counter/execve_counter.bpf.c
1,2c1,2
&lt;
&lt; #include "common.h"
---
&gt; #include &lt;linux/bpf.h&gt;
&gt; #include &lt;bpf/bpf_helpers.h&gt;
</code></pre>
<p>基于原料execve_counter.bpf.c，bpf2go工具会生成用户态部分所需的Go源码，比如：bpfObject中包含的bpf map实例：</p>
<pre><code>// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
    ExecveCounter *ebpf.Map `ebpf:"execve_counter"`
}
</code></pre>
<p>最后，我们在main包main函数中直接使用这些生成的与bpf objects相关的Go函数即可，下面是main.go部分源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf execve_counter.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&amp;objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    // init the map element
    var key [64]byte
    copy(key[:], []byte("execve_counter"))
    var val int64 = 0
    if err := objs.bpfMaps.ExecveCounter.Put(key, val); err != nil {
        log.Fatalf("init map key error: %s", err)
    }

    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case &lt;-ticker.C:
            if err := objs.bpfMaps.ExecveCounter.Lookup(key, &amp;val); err != nil {
                log.Fatalf("reading map error: %s", err)
            }
            log.Printf("execve_counter: %d\n", val)

        case &lt;-stopper:
            // Wait for a signal and close the perf reader,
            // which will interrupt rd.Read() and make the program exit.
            log.Println("Received signal, exiting program..")
            return
        }
    }
}
</code></pre>
<p>在main函数，我们通过objs.bpfMaps.ExecveCounter直接访问map实例，并通过其Put和Lookup方法可以直接操作map。这里要注意的是key的类型必须与execve_counter.bpf.c中的key类型(char[64])保持内存布局一致，不能直接用string类型，否则会在执行时报下面错误：</p>
<pre><code>init map key error: can't marshal key: string doesn't marshal to 64 bytes
</code></pre>
<p>编译和执行execve-counter-go和helloworld-go别无二致：</p>
<pre><code>$make
$go run -exec sudo main.go bpf_bpfel.go

2022/07/17 16:59:52 execve_counter: 0
2022/07/17 16:59:57 execve_counter: 14
^C2022/07/17 16:59:59 Received signal, exiting program..
</code></pre>
<h3>五. 小结</h3>
<p>本文介绍了eBPF内核态部分与用户态部分进行数据交换的主要方法：BPF MAP机制。这里的MAP不是狭义的一种hash散列表，而是一个抽象数据结构容器，目前支持二十几种数据结构，大家可以根据自己的需求挑选适当的结构（可查询手册了解各种数据结构的特点)。</p>
<p>MAP本质上也是由bpf系统调用创建的，bpf程序只需要声明map的key、value、type等组成信息即可。用户态可以通过bpf系统调用返回的fd操作map，libbpf和cilium/ebpf等封装了对fd的操作，这样简化了API的使用。</p>
<p>内核中map的update操作不是原子的，因此当有多个bpf程序并发访问一个map时，需要同步操作。bpf提供了bpf_spin_lock来实现对map操作的同步。我们可以在value类型中加入bpf_spin_lock来同步对value的修改，就像下面的例子(例子来自<a href="https://book.douban.com/subject/33398015/">《Linux Observability with BPF》</a>一书)：</p>
<pre><code>struct concurrent_element {
    struct bpf_spin_lock semaphore;
    int count;
}

struct bpf_map_def SEC("maps") concurrent_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(int),
    .value_size = sizeof(struct concurrent_element),
    .max_entries = 100,
};

int bpf_program(struct pt_regs *ctx) {
      intkey=0;
      struct concurrent_element init_value = {};
      struct concurrent_element *read_value;
      bpf_map_create_elem(&amp;concurrent_map, &amp;key, &amp;init_value, BPF_NOEXIST);
      read_value = bpf_map_lookup_elem(&amp;concurrent_map, &amp;key);
      bpf_spin_lock(&amp;read_value-&gt;semaphore);
      read_value-&gt;count += 100;
      bpf_spin_unlock(&amp;read_value-&gt;semaphore);
}
</code></pre>
<p>本文涉及代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples">这里</a>下载。</p>
<h3>六. 参考资料</h3>
<ul>
<li><a href="https://www.ebpf.top/post/map_internal/">《揭秘BPF map前生今世》</a> &#8211; https://www.ebpf.top/post/map_internal/</li>
<li><a href="https://mp.weixin.qq.com/s/Is84xGHFExE1BPkbPpKjwg">《边缘网络eBPF超能力：eBPF map原理与性能解析》</a> &#8211; https://mp.weixin.qq.com/s/Is84xGHFExE1BPkbPpKjwg</li>
<li><a href="https://man7.org/linux/man-pages/man2/bpf.2.html">bpf系统调用说明</a> &#8211; https://man7.org/linux/man-pages/man2/bpf.2.html</li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/maps.html">官方bpf map参考手册</a> &#8211; https://www.kernel.org/doc/html/latest/bpf/maps.html</li>
<li><a href="https://www.mankier.com/8/bpftool">bpftool参考手册</a> &#8211; https://www.mankier.com/8/bpftool</li>
<li><a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">《Building BPF applications with libbpf-bootstrap》</a> &#8211; https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>使用Go语言开发eBPF程序</title>
		<link>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/</link>
		<comments>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/#comments</comments>
		<pubDate>Tue, 19 Jul 2022 13:11:17 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpf2go]]></category>
		<category><![CDATA[bpfObjects]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BTF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CO-RE]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[falco]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-generate]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[isovalent]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katran]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[libbpf-bootstrap]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[llvm-objdump]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[pixie]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[readelf]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[submodule]]></category>
		<category><![CDATA[Thoughtworks]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[火焰图]]></category>
		<category><![CDATA[符号表]]></category>
		<category><![CDATA[网络]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3625</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/19/develop-ebpf-program-in-go 在前面的《使用C语言从头开发一个Hello World级别的eBPF程序》一文中，我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础，因为到目前为止，无论eBPF程序的用户态部分用什么语言开发，运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来，其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了，Go语言也不例外。 在Go社区中，目前最为活跃的用于开发eBPF用户态部分的Go eBPF包莫过于cilium项目开源的cilium/ebpf，cilium项目背后的Isovalent公司也是eBPF技术在云原生领域应用的主要推手之一。 本文我们就来说说基于cilium/ebpf开发eBPF程序的套路！ 一. 探索cilium/ebpf项目示例 cilium/ebpf项目借鉴了libbpf-boostrap的思路，通过代码生成与bpf程序内嵌的方式构建eBPF程序用户态部分。为了搞清楚基于cilium/ebpf开发ebpf程序的套路，我们先来探索一下cilium/ebpf项目提供的示例代码。 我们首先来下载和看看ebpf的示例的结构。 下载cilium/ebpf项目 $ git clone https://github.com/cilium/ebpf.git Cloning into 'ebpf'... remote: Enumerating objects: 7054, done. remote: Counting objects: 100% (183/183), done. remote: Compressing objects: 100% (112/112), done. remote: Total 7054 (delta 91), reused 124 (delta 69), pack-reused 6871 Receiving objects: 100% (7054/7054), 10.91 MiB &#124; [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/develop-ebpf-program-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go">本文永久链接</a> &#8211; https://tonybai.com/2022/07/19/develop-ebpf-program-in-go</p>
<p>在前面的<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中，我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础，因为到目前为止，无论eBPF程序的用户态部分用什么语言开发，运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来，其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了，Go语言也不例外。</p>
<p>在Go社区中，目前最为活跃的用于开发eBPF用户态部分的Go eBPF包莫过于cilium项目开源的<a href="https://github.com/cilium/ebpf/">cilium/ebpf</a>，cilium项目背后的<a href="https://isovalent.com/">Isovalent公司</a>也是eBPF技术在云原生领域应用的主要推手之一。</p>
<p>本文我们就来说说<strong>基于cilium/ebpf开发eBPF程序的套路</strong>！</p>
<h3>一. 探索cilium/ebpf项目示例</h3>
<p>cilium/ebpf项目借鉴了<a href="https://github.com/libbpf/libbpf-bootstrap">libbpf-boostrap</a>的思路，通过代码生成与bpf程序内嵌的方式构建eBPF程序用户态部分。为了搞清楚基于cilium/ebpf开发ebpf程序的套路，我们先来探索一下cilium/ebpf项目提供的示例代码。</p>
<p>我们首先来下载和看看ebpf的示例的结构。</p>
<ul>
<li>下载cilium/ebpf项目</li>
</ul>
<pre><code>$ git clone https://github.com/cilium/ebpf.git
Cloning into 'ebpf'...
remote: Enumerating objects: 7054, done.
remote: Counting objects: 100% (183/183), done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 7054 (delta 91), reused 124 (delta 69), pack-reused 6871
Receiving objects: 100% (7054/7054), 10.91 MiB | 265.00 KiB/s, done.
Resolving deltas: 100% (4871/4871), done.
</code></pre>
<ul>
<li>探索ebpf项目示例代码结构</li>
</ul>
<p>ebpf示例在examples目录下，我们以tracepoint_in_c为例看看其组织形式：</p>
<pre><code>$tree tracepoint_in_c
tracepoint_in_c
├── bpf_bpfeb.go
├── bpf_bpfeb.o
├── bpf_bpfel.go
├── bpf_bpfel.o
├── main.go
└── tracepoint.c

0 directories, 6 files
</code></pre>
<p>根据经验判断，这里面的tracepoint.c对应的是ebpf程序内核态部分，而main.go和bpf_bpfel.go/bpf_bpfeb.go则是ebpf程序用户态部分，至于bpf_bpfeb.o/bpf_bpfel.o应该是某种中间目标文件。通过readelf -a bpf_bpfeb.o查看该中间文件：</p>
<pre><code>$readelf -a bpf_bpfeb.o
ELF Header:
  Magic:   7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, big endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1968 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 1
... ...

</code></pre>
<p>我们看到这是一个内含linux bpf字节码的elf文件(Machine: Linux BPF)。</p>
<p>阅读了cilium/ebpf的相关文档，我搞明白了这几个文件的关系，用下面示意图呈现给大家：</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-ebpf-program-in-go-2.png" alt="" /></p>
<p>ebpf程序的源码文件(比如图中tracepoint.c)经过bpf2go(cilium/ebpf提供的一个代码生成工具)被编译(bpf2go调用clang)为ebpf字节码文件bpf_bpfeb.o(大端)和bpf_bpfel.o(小端)，然后bpf2go会基于ebpf字节码文件生成bpf_bpfeb.go或bpf_bpfel.go，ebpf程序的字节码会以二进制数据的形式内嵌到这两个go源文件中，以bpf_bpfel.go为例，我们可以在其代码中找到下面内容(利用<a href="https://tonybai.com/2021/02/25/some-changes-in-go-1-16">go:embed特性</a>)：</p>
<pre><code>//go:embed bpf_bpfel.o
var _BpfBytes []byte
</code></pre>
<p>main.go则是ebpf程序用户态部分的主程序，将main.go与bpf_bpfeb.go或bpf_bpfel.go之一一起编译就形成了ebpf程序。</p>
<p>有了对cilium/ebpf项目示例的初步探索后，我们来构建ebpf示例代码。</p>
<h3>二. 构建ebpf示例代码</h3>
<p>cilium/ebpf提供了便利的构建脚本，我们只需在ebpf/examples下面执行”make -C ..”即可进行示例代码的构建。</p>
<p>make构建过程会基于quay.io/cilium/ebpf-builder镜像启动构建容器，不过在国内的童鞋需要像下面一样对Makefile内容做一丁点修改，增加GOPROXY环境变量，否则wall外的go module无法拉取：</p>
<pre><code>$git diff ../Makefile
diff --git a/Makefile b/Makefile
index 3a1da88..d7b1712 100644
--- a/Makefile
+++ b/Makefile
@@ -48,6 +48,7 @@ container-all:
        ${CONTAINER_ENGINE} run --rm ${CONTAINER_RUN_ARGS} \
                -v "${REPODIR}":/ebpf -w /ebpf --env MAKEFLAGS \
                --env CFLAGS="-fdebug-prefix-map=/ebpf=." \
+               --env GOPROXY="https://goproxy.io" \
                --env HOME="/tmp" \
                "${IMAGE}:${VERSION}" \
                $(MAKE) all

</code></pre>
<p>这之后再执行构建就会顺利得到我们所要的结果：</p>
<pre><code>$ cd examples
$ make -C ..
make: Entering directory '/root/go/src/github.com/cilium/ebpf'
docker run --rm  --user "0:0" \
    -v "/root/go/src/github.com/cilium/ebpf":/ebpf -w /ebpf --env MAKEFLAGS \
    --env CFLAGS="-fdebug-prefix-map=/ebpf=." \
    --env GOPROXY="https://goproxy.io" \
    --env HOME="/tmp" \
    "quay.io/cilium/ebpf-builder:1648566014" \
    make all
make: Entering directory '/ebpf'
find . -type f -name "*.c" | xargs clang-format -i
go generate ./cmd/bpf2go/test
go: downloading golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34
Compiled /ebpf/cmd/bpf2go/test/test_bpfel.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfel.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfel.go
Compiled /ebpf/cmd/bpf2go/test/test_bpfeb.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfeb.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfeb.go
go generate ./internal/sys
enum AdjRoomMode
enum AttachType
enum Cmd
enum FunctionId
enum HdrStartOff
enum LinkType
enum MapType
enum ProgType
enum RetCode
enum SkAction
enum StackBuildIdStatus
enum StatsType
enum XdpAction
struct BtfInfo
... ...
attr ProgRun
attr RawTracepointOpen
cd examples/ &amp;&amp; go generate ./...
go: downloading github.com/cilium/ebpf v0.8.2-0.20220424153111-6da9518107a8
go: downloading golang.org/x/sys v0.0.0-20211001092434-39dca1131b70
Compiled /ebpf/examples/cgroup_skb/bpf_bpfel.o
Stripped /ebpf/examples/cgroup_skb/bpf_bpfel.o
Wrote /ebpf/examples/cgroup_skb/bpf_bpfel.go
Compiled /ebpf/examples/cgroup_skb/bpf_bpfeb.o
Stripped /ebpf/examples/cgroup_skb/bpf_bpfeb.o
Wrote /ebpf/examples/cgroup_skb/bpf_bpfeb.go
Compiled /ebpf/examples/fentry/bpf_bpfeb.o
Stripped /ebpf/examples/fentry/bpf_bpfeb.o
Wrote /ebpf/examples/fentry/bpf_bpfeb.go
Compiled /ebpf/examples/fentry/bpf_bpfel.o
Stripped /ebpf/examples/fentry/bpf_bpfel.o
Wrote /ebpf/examples/fentry/bpf_bpfel.go
Compiled /ebpf/examples/kprobe/bpf_bpfel.o
Stripped /ebpf/examples/kprobe/bpf_bpfel.o
Wrote /ebpf/examples/kprobe/bpf_bpfel.go
Stripped /ebpf/examples/uretprobe/bpf_bpfel_x86.o
... ...
Wrote /ebpf/examples/uretprobe/bpf_bpfel_x86.go
ln -srf testdata/loader-clang-14-el.elf testdata/loader-el.elf
ln -srf testdata/loader-clang-14-eb.elf testdata/loader-eb.elf
make: Leaving directory '/ebpf'
make: Leaving directory '/root/go/src/github.com/cilium/ebpf'
</code></pre>
<p>以uretprobe下面的ebpf为例，我们运行一下：</p>
<pre><code>$go run -exec sudo uretprobe/*.go
2022/06/05 18:23:23 Listening for events..
</code></pre>
<p>打开一个新的terminal，然后在用户home目录下执行vi .bashrc。在上面的uretprobe程序的执行窗口我们能看到：</p>
<pre><code>2022/06/05 18:24:34 Listening for events..
2022/06/05 18:24:42 /bin/bash:readline return value: vi .bashrc
</code></pre>
<p>这就表明uretprobe下面的ebpf程序如预期地执行了。</p>
<h3>三. 使用cilium/ebpf为前文的Hello World eBPF程序开发用户态部分</h3>
<p>有了对cilium/ebpf示例程序的初步了解，下面我们就来为前面的<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中的那个helloworld ebpf程序开发用户态部分。</p>
<p>回顾一下那个hello world ebpf程序的C源码：</p>
<pre><code>// github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld-go/helloworld.bpf.c
#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
</code></pre>
<p>当这个ebpf程序被加载到内核中后，每当execve这个系统调用被执行，该ebpf程序都会被调用一次，我们就会在/sys/kernel/debug/tracing/trace_pipe中看到对应的日志输出。</p>
<h4>1. 使用bpf2go将ebpf核心态程序转换为Go代码</h4>
<p>根据我们在前面探索cilium/ebpf示例程序时所得到的“套路”，我们接下来第一个要做的就是将helloworld.bpf.c转换为Go代码文件，这一转换过程不可缺少的工具就是cilium/ebpf提供的bpf2go工具，我们先来安装一下该工具：</p>
<pre><code>$go install github.com/cilium/ebpf/cmd/bpf2go@latest
</code></pre>
<p>接下来，我们可以直接使用bpf2go工具将helloworld.ebpf.c转换为对应的go源文件：</p>
<pre><code>$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/test/ebpf/libbpf/include/uapi -I /usr/local/bpf/include -idirafter /usr/local/include -idirafter /usr/lib/llvm-10/lib/clang/10.0.0/include -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include

Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<p>不过这里有一个问题，那就是bpf2go命令行后面的一系列提供给clang编译器的头文件引用路径参考了<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中的Makefile。如果按照这些头文件路径来引用，虽然bpf2go转换可以成功，但是我们需要依赖并安装libbpf这个库，这显然不是我们想要的。</p>
<p>cilium/ebpf在examples中提供了一个headers目录，这个目录中包含了开发ebpf程序用户态部分所需的所有头文件，我们使用它作为我们的头文件引用路径。不过要想基于这个headers目录构建ebpf，我们需要将helloworld.bpf.c中的原头文件include语句由：</p>
<pre><code>#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
</code></pre>
<p>改为：</p>
<pre><code>#include "common.h"
</code></pre>
<p>接下来我们再来执行bpf2go工具进行转换：</p>
<pre><code>$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/go/src/github.com/cilium/ebpf/examples/headers

Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<p>我们看到bpf2go顺利生成ebpf字节码与对应的Go源文件。</p>
<h4>2. 构建helloworld ebpf程序用户态部分</h4>
<p>下面是参考cilium/ebpf示例而构建的helloword ebpf程序用户态部分的main.go源码：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go
package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
)

func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&amp;objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    //SEC("tracepoint/syscalls/sys_enter_execve")
    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    log.Printf("Successfully started! Please run \"sudo cat /sys/kernel/debug/tracing/trace_pipe\" to see output of the BPF programs\n")

    // Wait for a signal and close the perf reader,
    // which will interrupt rd.Read() and make the program exit.
    &lt;-stopper
    log.Println("Received signal, exiting program..")
}
</code></pre>
<p>我们知道一个ebpf程序有几个关键组成：</p>
<ul>
<li>ebpf程序数据</li>
<li>map：用于用户态与内核态的数据交互</li>
<li>挂接点(attach point)</li>
</ul>
<p>根据<a href="https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md">cilium/ebpf架构</a>的说明，ebpf包将前两部分抽象为了一个数据结构bpfObjects：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go

// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
    bpfPrograms
    bpfMaps
}
</code></pre>
<p>我们看到，main函数通过生成的loadBpfObjects函数将ebpf程序加载到内核，并填充bpfObjects结构，一旦加载bpf程序成功，后续我们便可以使用bpfObjects结构中的字段来完成其余操作，比如通过link包的函数将bpf程序与目标挂节点对接在一起(如文中的link.Tracepoint函数），这样挂接后，bpf才能在对应的事件发生后被回调执行。</p>
<p>下面编译执行一下该helloworld示例：</p>
<pre><code>$go run -exec sudo main.go bpf_bpfel.go
[sudo] password for tonybai:
2022/06/05 14:12:40 Successfully started! Please run "sudo cat /sys/kernel/debug/tracing/trace_pipe" to see output of the BPF programs
</code></pre>
<p>之后新打开一个窗口，执行sudo cat /sys/kernel/debug/tracing/trace_pipe，当execve被调用时，我们就能看到类似下面的日志输出：</p>
<pre><code>&lt;...&gt;-551077  [000] .... 6062226.208943: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551077  [000] .... 6062226.209098: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551079  [007] .... 6062226.215421: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551079  [007] .... 6062226.215578: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-554756  [007] .... 6063476.785212: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-554756  [007] .... 6063476.785378: 0: invoke bpf_prog: Hello, World!
</code></pre>
<h4>3. 使用go generate来驱动bpf2go的转换</h4>
<p>在生成代码方面，Go工具链原生提供了go generate工具，cilium/ebpf的examples中也是利用go generate来驱动bpf2go将bpf程序转换为Go源文件的，这里我们也来做一下改造。</p>
<p>首先我们在main.go的main函数上面增加一行go:generate指示语句：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf helloworld.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal,  1)
    ... ...
}
</code></pre>
<p>这样当我们显式执行go generate语句时，go generate会扫描到该指示语句，并执行后面的命令。这里使用了几个变量，变量是定义在Makefile中的。当然如果你不想使用Makefile，也可以将变量替换为相应的值。这里我们使用Makefile，下面是Makefile的内容：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/Makefile

CLANG ?= clang-10
CFLAGS ?= -O2 -g -Wall -Werror

LIBEBPF_TOP = /home/tonybai/go/src/github.com/cilium/ebpf
EXAMPLES_HEADERS = $(LIBEBPF_TOP)/examples/headers

all: generate

generate: export BPF_CLANG=$(CLANG)
generate: export BPF_CFLAGS=$(CFLAGS)
generate: export BPF_HEADERS=$(EXAMPLES_HEADERS)
generate:
    go generate ./...
</code></pre>
<p>有了该Makefile后，我们执行make命令便可以执行bpf2go对bpf程序的转换：</p>
<pre><code>$make
go generate ./...
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<h3>四. 小结</h3>
<p>本文我们讲解了如何基于cilium/ebpf包来开发ebpf的用户态部分。</p>
<p>ebpf借鉴了libbpf的思路，通过生成代码与数据内嵌的方式来构建ebpf的用户态部分。</p>
<p>ebpf提供了bpf2go工具，可以将bpf的C源码转换为相应的go源码。</p>
<p>ebpf将bpf程序抽象为bpfObjects，通过生成的loadBpfObjects完成bpf程序加载到内核的过程，然后利用ebpf库提供的诸如link之类的包实现ebpf与内核事件的关联。</p>
<p>ebpf包的玩法还有很多，这一篇仅仅是为了打好基础，在后续文章中，我们还会针对各种类型的bpf程序做进一步学习和说明。</p>
<p>本文代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld-go">这里</a>下载。</p>
<h3>无. 参考资料</h3>
<ul>
<li><a href="https://www.ebpf.top/post/ebpf_go/">使用Go语言管理和分发ebpf程序</a> &#8211; https://www.ebpf.top/post/ebpf_go/</li>
<li><a href="https://lpc.events/event/4/contributions/449/attachments/239/529/A_pure_Go_eBPF_library.pdf">A Pure Go eBPF library</a> &#8211; https://lpc.events/event/4/contributions/449/attachments/239/529/A_pure_Go_eBPF_library.pdf</li>
<li><a href="https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md">cilium ebpf library architecture</a> &#8211; https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Go基于国密算法实现双向认证</title>
		<link>https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm/</link>
		<comments>https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm/#comments</comments>
		<pubDate>Sun, 17 Jul 2022 13:45:57 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[CA]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[CipherSuite]]></category>
		<category><![CDATA[Client]]></category>
		<category><![CDATA[config]]></category>
		<category><![CDATA[CSR]]></category>
		<category><![CDATA[DH]]></category>
		<category><![CDATA[DHE]]></category>
		<category><![CDATA[ECDHE]]></category>
		<category><![CDATA[ESDSA]]></category>
		<category><![CDATA[gmsm]]></category>
		<category><![CDATA[gmssl]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[handshake]]></category>
		<category><![CDATA[hash]]></category>
		<category><![CDATA[MAC]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[mkcert]]></category>
		<category><![CDATA[openssl]]></category>
		<category><![CDATA[PKI]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[Server]]></category>
		<category><![CDATA[SHA-1]]></category>
		<category><![CDATA[SHA256]]></category>
		<category><![CDATA[SM2]]></category>
		<category><![CDATA[SM3]]></category>
		<category><![CDATA[SM4]]></category>
		<category><![CDATA[SM9]]></category>
		<category><![CDATA[SSL]]></category>
		<category><![CDATA[TLCP]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[TLS1.2]]></category>
		<category><![CDATA[TLS1.3]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[x509]]></category>
		<category><![CDATA[传输层]]></category>
		<category><![CDATA[公钥]]></category>
		<category><![CDATA[加密]]></category>
		<category><![CDATA[双向认证]]></category>
		<category><![CDATA[商密]]></category>
		<category><![CDATA[国密]]></category>
		<category><![CDATA[密码套件]]></category>
		<category><![CDATA[对称加密]]></category>
		<category><![CDATA[握手]]></category>
		<category><![CDATA[数字证书]]></category>
		<category><![CDATA[椭圆曲线算法]]></category>
		<category><![CDATA[私钥]]></category>
		<category><![CDATA[签名]]></category>
		<category><![CDATA[非对称加密]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3617</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm 国内做2B(to Biz)或2G(to Gov)产品和解决方案的企业都绕不过国密算法，越来越多的国内甲方在采购需求中包含了基于国密算法的认证、签名、加密等需求。对于国内的车联网平台来说，支持基于国密的双向认证也是大势所趋。在这篇文章中，我就来说说如何基于国密算法实现双向认证，即使用国密算法的安全传输层双向认证。 一. 简要回顾基于TLS的双向认证 在《Go语言精进之路》第2册的第51条中，我详细介绍了TLS的建连握手与双向认证过程，并对非对称加密与公钥证书的原理做了系统全面的讲解。为了让大家更好地理解后面的内容，这里简单回顾一下基于TLS的双向认证。 TLS，全称Transport Layer Security，即安全传输层。其前身为SSL（Secure Socket Layer）。TLS是建构在TCP传输层之上和应用层之下的、为应用层提供端到端安全连接和传输服务的虚拟协议层。 应用层基于TLS的通信都是加密的(如上图所示)，保证了传输数据的安全，即便被窃听，攻击者也无法拿到明文数据(密钥够长，加密算法强度够强的前提下)。对于应用开发者而言，重点在于TLS连接的建立过程，连接一旦建立，后续的加解密传输过程就很容易了。 TLS连接的建立过程称为TLS握手(handshake)，握手的过程见下图(适用于TLS 1.2)： 上图引自《Go语言精进之路》第2册的图51-5 关于握手的各个步骤的详细说明，大家可以参考《Go语言精进之路》第2册的第51条中的内容，这里不赘述。 从图中我们可以看到：TLS连接的建立过程需要数字证书的参与，而数字证书主要用于对通信双方的身份进行验证以及参与双方会话密钥的协商与生成。一般情况下，客户端会校验服务端的公钥证书，服务端不会校验客户端公钥证书。但在一些安全级别较高的系统中，服务端也会要求校验客户端的公钥证书(TLS握手阶段，服务端向客户端发送CertificateRequest请求)。 下面我们就来看一个基于TLS的双向认证的实例。 二. 基于TLS双向认证的示例 我们先来看看示例开发与执行的环境并创建相关的数字证书。 1. 环境与数字证书 我们在Ubuntu 20.04.3 LTS环境使用Go 1.18版本开发和执行该示例。示例是一个echo server，即将client端发来的数据重新发回client端，下面是示意图： 开发基于TLS的应用离不开数字证书，因此在开发程序之前，我们先来生成server与client所用的各类公钥数字证书。 在开发和测试环境，我们可以使用自签发的公钥数字证书。我们可以先生成自用的CA私钥与证书，然后利用该CA签发我们所需的服务端和客户端的公钥证书。制作证书最著名的工具是openssl，不过openssh使用起来较为复杂，这些年一些开发者体验更好的工具也逐渐成熟，比如由Go项目前安全负责人Filippo Valsorda开源的mkcert就是一个不错的工具，本文就使用这个工具建立CA并签发制作各类证书。 我们先来安装mkcert工具： $go install filippo.io/mkcert@latest go: downloading filippo.io/mkcert v1.4.4 go: downloading golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 go: downloading software.sslmate.com/src/go-pkcs12 v0.2.0 go: downloading golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 接下来，生成并安装local [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm">本文永久链接</a> &#8211; https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm</p>
<p>国内做2B(to Biz)或2G(to Gov)产品和解决方案的企业都绕不过<strong>国密算法</strong>，越来越多的国内甲方在采购需求中包含了基于国密算法的认证、签名、加密等需求。对于国内的车联网平台来说，支持基于国密的双向认证也是大势所趋。在这篇文章中，我就来说说如何基于国密算法实现双向认证，即<strong>使用国密算法的安全传输层双向认证</strong>。</p>
<hr />
<h3>一. 简要回顾基于TLS的双向认证</h3>
<p>在<a href="https://item.jd.com/13694000.html">《Go语言精进之路》</a>第2册的第51条中，我详细介绍了TLS的建连握手与双向认证过程，并对非对称加密与公钥证书的原理做了系统全面的讲解。为了让大家更好地理解后面的内容，这里简单回顾一下<strong>基于TLS的双向认证</strong>。</p>
<p>TLS，全称Transport Layer Security，即<strong>安全传输层</strong>。其前身为SSL（Secure Socket Layer）。TLS是建构在TCP传输层之上和应用层之下的、<strong>为应用层提供端到端安全连接和传输服务的虚拟协议层</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-3.png" alt="" /></p>
<p>应用层基于TLS的通信都是加密的(如上图所示)，保证了传输数据的安全，即便被窃听，攻击者也无法拿到明文数据(密钥够长，加密算法强度够强的前提下)。对于应用开发者而言，重点在于TLS连接的建立过程，连接一旦建立，后续的加解密传输过程就很容易了。</p>
<p>TLS连接的建立过程称为<strong>TLS握手(handshake)</strong>，握手的过程见下图(适用于TLS 1.2)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-4.png" alt="" /><br />
<center>上图引自《Go语言精进之路》第2册的图51-5</center></p>
<p>关于握手的各个步骤的详细说明，大家可以参考<a href="https://item.jd.com/13694000.html">《Go语言精进之路》第2册</a>的第51条中的内容，这里不赘述。</p>
<p>从图中我们可以看到：TLS连接的建立过程<strong>需要数字证书的参与</strong>，而数字证书主要用于对通信双方的身份进行验证以及参与双方会话密钥的协商与生成。一般情况下，客户端会校验服务端的公钥证书，服务端不会校验客户端公钥证书。但在一些安全级别较高的系统中，服务端也会要求校验客户端的公钥证书(TLS握手阶段，服务端向客户端发送CertificateRequest请求)。</p>
<p>下面我们就来看一个基于TLS的双向认证的实例。</p>
<h3>二. 基于TLS双向认证的示例</h3>
<p>我们先来看看示例开发与执行的环境并创建相关的数字证书。</p>
<h4>1. 环境与数字证书</h4>
<p>我们在Ubuntu 20.04.3 LTS环境使用<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18版本</a>开发和执行该示例。示例是一个echo server，即将client端发来的数据重新发回client端，下面是示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-5.png" alt="" /></p>
<p>开发基于TLS的应用离不开数字证书，因此在开发程序之前，我们先来生成server与client所用的各类公钥数字证书。</p>
<p>在开发和测试环境，我们可以使用<strong>自签发的公钥数字证书</strong>。我们可以先生成<strong>自用的CA私钥与证书</strong>，然后利用该CA签发我们所需的服务端和客户端的公钥证书。制作证书最著名的工具是<a href="https://www.openssl.org">openssl</a>，不过openssh使用起来较为复杂，这些年一些开发者体验更好的工具也逐渐成熟，比如由Go项目前安全负责人<a href="https://filippo.io/">Filippo Valsorda</a>开源的<a href="https://github.com/FiloSottile/mkcert">mkcert</a>就是一个不错的工具，本文就使用这个工具建立CA并签发制作各类证书。</p>
<p>我们先来安装mkcert工具：</p>
<pre><code>$go install filippo.io/mkcert@latest
go: downloading filippo.io/mkcert v1.4.4
go: downloading golang.org/x/net v0.0.0-20220421235706-1d1ef9303861
go: downloading software.sslmate.com/src/go-pkcs12 v0.2.0
go: downloading golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
</code></pre>
<p>接下来，生成并安装local CA根证书：</p>
<pre><code>$mkcert -install
Created a new local CA:
The local CA is now installed in the system trust store!
</code></pre>
<p>从mkcert的输出来看，CA私钥和证书被安装到所谓system trust store中。这个system trust store在不同平台上的位置不同。在linux上有如下几个位置：</p>
<pre><code>// github.com/FiloSottile/mkcert/truststore_linux.go 

func init() {
    switch {
    case binaryExists("apt"):
        CertutilInstallHelp = "apt install libnss3-tools"
    case binaryExists("yum"):
        CertutilInstallHelp = "yum install nss-tools"
    case binaryExists("zypper"):
        CertutilInstallHelp = "zypper install mozilla-nss-tools"
    }
    if pathExists("/etc/pki/ca-trust/source/anchors/") {
        SystemTrustFilename = "/etc/pki/ca-trust/source/anchors/%s.pem"
        SystemTrustCommand = []string{"update-ca-trust", "extract"}
    } else if pathExists("/usr/local/share/ca-certificates/") {
        SystemTrustFilename = "/usr/local/share/ca-certificates/%s.crt"
        SystemTrustCommand = []string{"update-ca-certificates"}
    } else if pathExists("/etc/ca-certificates/trust-source/anchors/") {
        SystemTrustFilename = "/etc/ca-certificates/trust-source/anchors/%s.crt"
        SystemTrustCommand = []string{"trust", "extract-compat"}
    } else if pathExists("/usr/share/pki/trust/anchors") {
        SystemTrustFilename = "/usr/share/pki/trust/anchors/%s.pem"
        SystemTrustCommand = []string{"update-ca-certificates"}
    }
}
</code></pre>
<p>在我的ubuntu 20.04环境中，CA的公钥证书被<strong>安装(install)</strong>在/usr/local/share/ca-certificates下面了：</p>
<pre><code>$ls -l /usr/local/share/ca-certificates/
total 4
-rw-r--r-- 1 root root 1631 Jul  3 16:22 mkcert_development_CA_333807542491031300702675758897110223851.crt
</code></pre>
<p>生成的CA私钥在哪里呢？我们可以通过-CAROOT参数获得该位置：</p>
<pre><code>$mkcert -CAROOT
/home/tonybai/.local/share/mkcert

$ls -l /home/tonybai/.local/share/mkcert
total 8
-r-------- 1 tonybai tonybai 2484 Jul  3 16:22 rootCA-key.pem
-rw-r--r-- 1 tonybai tonybai 1631 Jul  3 16:22 rootCA.pem
</code></pre>
<p>这里的rootCA.pem与系统信任区中的mkcert_development_CA_333807542491031300702675758897110223851.crt与rootCA.pem内容是一样的。后者是mkcert将rootCA.pem安装到系统可信store后的结果。通过mkcert -uninstall可以删除/usr/local/share/ca-certificates下面的CA公钥证书。但/home/tonybai/.local/share/mkcert下的CA私钥与证书不会被删除。后续若再执行mkcert install，CA证书不会重新生成，现有的rootCA.pem还会被install到/usr/local/share/ca-certificates下面。</p>
<p>接下来我们分别server端和client端的私钥与证书。</p>
<p>server端key和cert：</p>
<pre><code>$mkcert -key-file key.pem -cert-file cert.pem example.com 

Created a new certificate valid for the following names
 - "example.com"

The certificate is at "cert.pem" and the key at "key.pem"

It will expire on 3 October 2024
</code></pre>
<p>我们可以通过下面命令查看证书内容：</p>
<pre><code>$openssl x509 -in cert.pem -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            fc:cc:96:17:55:2d:70:e8:67:3e:b2:25:a9:b8:a3:80
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul  7 09:05:09 2022 GMT
            Not After : Oct  7 09:05:09 2024 GMT
        Subject: O = mkcert development certificate, OU = tonybai@tonybai
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:a6:d1:00:f7:da:03:d0:06:17:cb:ee:b4:99:30:
                    20:66:d0:78:b0:94:67:0b:7a:37:d2:76:21:71:9a:
                    7a:17:d6:44:0a:7b:f1:24:71:2f:ed:b5:67:66:a1:
                    1f:b0:e6:3b:18:66:de:f4:83:78:9a:bc:f5:ae:88:
                    23:a1:f9:7d:7c:3e:7f:a8:f9:9f:54:d0:68:48:b9:
                    d0:56:10:a0:84:0b:cf:a8:bc:b8:74:3f:3c:27:db:
                    ff:28:1d:63:e8:79:a6:93:44:a8:14:43:53:bf:e8:
                    ca:ee:bf:4c:63:f7:23:51:e6:a2:8d:0b:9a:7d:95:
                    2e:bc:37:ae:6d:ea:9e:0e:e6:e0:c5:8e:07:0c:d4:
                    9b:50:30:de:31:c9:97:ee:ac:7e:33:ab:0d:6f:87:
                    f3:70:2b:22:26:8d:a8:95:8e:1f:0e:b7:61:71:e8:
                    36:06:a7:f4:d8:d2:f6:89:12:26:fd:7e:6b:19:a2:
                    2a:4c:d7:cb:7e:09:fc:65:86:be:b6:c2:0b:fb:b5:
                    d8:63:07:aa:ba:59:ab:fc:34:0d:4a:d1:93:dd:62:
                    b0:3a:cd:e1:21:79:13:e4:f4:45:00:f7:10:a1:bc:
                    c7:51:38:84:c4:75:22:5e:5f:a9:11:07:34:16:9f:
                    ad:c7:94:af:57:30:17:77:49:14:6e:16:ff:d8:00:
                    78:11
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Authority Key Identifier:
                keyid:A8:C4:06:2D:2C:25:71:EC:08:C8:1A:92:9A:F2:52:87:22:6E:85:2D

            X509v3 Subject Alternative Name:
                DNS:example.com
    Signature Algorithm: sha256WithRSAEncryption
         be:6e:90:60:bd:43:b9:3a:09:14:c2:44:22:88:a6:af:e5:22:
         d3:97:19:64:8b:59:5d:60:33:36:01:a1:4e:01:eb:7e:5c:6a:
         48:c4:04:a6:0a:e4:91:95:db:5a:2c:c8:e9:93:fa:37:34:6d:
         81:d1:96:ed:5b:67:ae:27:e3:d3:ea:ee:5c:74:0f:6e:f1:48:
         72:d2:75:85:a1:70:0f:a0:9a:73:7a:ca:b8:7b:92:46:27:73:
         e5:f8:ec:72:f8:fc:ac:5f:22:68:0c:d6:8c:20:5b:93:e1:52:
         17:79:57:71:33:5b:98:05:11:8a:cb:d4:3c:b2:24:4b:7b:c5:
         32:8f:ae:1f:a5:af:9d:3a:9b:bb:fc:46:8a:d6:48:39:86:de:
         f3:f7:54:03:45:8d:bd:40:91:26:d2:29:0a:c4:91:cf:b2:5c:
         41:d5:66:24:02:6d:60:22:ea:78:0d:b0:66:80:b9:5d:03:27:
         09:c7:aa:61:1b:ee:e4:08:21:7e:be:bb:13:8a:fb:d8:9e:24:
         5f:5b:a2:4a:d5:db:be:a2:84:74:03:fb:04:37:d0:b3:c4:b7:
         4e:3e:31:a7:2d:5d:62:bd:aa:68:3c:84:d9:32:cb:f2:93:7a:
         3a:8a:2b:c3:81:76:f0:b5:f5:3c:d4:69:8d:5e:f8:39:74:88:
         2b:56:7f:2b:4c:f9:39:2a:f2:4d:15:75:a1:f3:62:ee:57:ce:
         f7:33:c7:cc:a6:97:25:f0:66:bf:5d:5b:c2:d7:d3:ee:20:be:
         c3:5f:fb:9a:50:59:b8:e7:ea:d2:4c:35:9d:48:3f:93:63:96:
         3c:52:dd:b8:d6:ba:1f:30:18:2e:c4:3d:3a:03:66:e1:a3:48:
         6e:a0:5d:b0:0b:65:d4:40:9e:da:5c:36:b1:ac:6b:9e:1f:01:
         69:8a:92:63:7d:27:79:42:bd:d4:f5:e2:d3:bf:8e:97:2f:57:
         ae:0b:f8:c1:b1:35:47:d0:4e:77:b0:e7:88:69:4b:44:dc:01:
         6e:6e:4d:87:e2:71
</code></pre>
<p>接下来，我们再生成client端的key和cert。client端的cert专门用来提供给server端进行证书验证的，我们需要向mkcert传入-client选项生成client端证书：</p>
<pre><code>$mkcert -client -key-file client-key.pem -cert-file client-cert.pem client1

Created a new certificate valid for the following names
 - "client1"

The certificate is at "client-cert.pem" and the key at "client-key.pem"

It will expire on 3 October 2024
</code></pre>
<p>同样，我们可以通过下面命令查看客户端证书的内容：</p>
<pre><code>$openssl x509 -in client-cert.pem -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            62:59:40:5c:e7:5a:61:74:73:bf:08:b0:d9:a7:d4:a1
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul  7 09:11:27 2022 GMT
            Not After : Oct  7 09:11:27 2024 GMT
        Subject: O = mkcert development certificate, OU = tonybai@tonybai
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:e5:25:c6:a1:c9:e2:5f:64:72:bd:ed:fc:24:fa:
                    12:8d:9c:30:52:8d:d8:5a:e7:f4:0c:b5:d5:0a:ef:
                    06:26:e3:06:54:54:cc:72:77:4e:22:cd:22:04:c0:
                    08:2e:94:2d:0f:cc:e8:9f:b9:c5:f4:13:8e:d1:f4:
                    bb:64:9d:1a:74:1b:e3:8c:95:2c:18:44:ec:e7:2c:
                    ec:0c:19:0f:e1:e6:1a:22:e7:3e:a6:1b:35:6e:05:
                    5f:c3:04:3f:1a:0f:c4:55:6f:ff:15:a0:a0:de:44:
                    5c:2d:3d:0b:dc:8a:01:ca:d2:2a:71:9d:b7:3a:d2:
                    10:9f:79:76:e0:a7:14:aa:d8:f0:90:bd:7c:4d:2d:
                    45:e6:16:ab:1d:03:7f:d8:97:4f:4d:41:13:76:72:
                    35:f2:41:b7:f1:3b:a8:42:d4:79:39:fd:f6:8d:10:
                    d1:54:06:60:6a:79:04:6c:6f:05:37:9c:4e:e7:ba:
                    9d:87:e8:05:65:9a:22:56:91:cb:03:bd:89:42:16:
                    66:92:bf:df:50:27:f2:81:89:c0:c5:46:f7:01:e8:
                    80:d0:4d:2e:ae:7f:5a:e9:fa:69:f3:50:c4:58:48:
                    dc:b5:20:13:01:3a:ac:fd:a8:69:2d:20:a9:55:cc:
                    90:4a:f1:f7:3f:9e:3b:7a:cb:77:c7:d2:c4:2b:4f:
                    4c:09
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Client Authentication, TLS Web Server Authentication
            X509v3 Authority Key Identifier:
                keyid:A8:C4:06:2D:2C:25:71:EC:08:C8:1A:92:9A:F2:52:87:22:6E:85:2D

            X509v3 Subject Alternative Name:
                DNS:client1
    Signature Algorithm: sha256WithRSAEncryption
         a6:68:a8:b3:cf:8c:8c:f6:03:56:68:e4:d3:02:cd:ec:8d:fa:
         7f:73:56:c2:91:fa:d8:65:82:a7:f5:d9:8b:32:2a:3b:f9:59:
         71:0c:f8:d3:b6:d3:b3:11:99:f6:f6:d1:ab:d9:1e:fc:bd:f5:
         71:d9:35:4e:0e:fb:f2:f9:65:12:f2:1d:26:77:7d:eb:2c:52:
         80:2c:05:64:0f:99:35:83:31:b0:eb:71:85:04:48:d6:f6:29:
         92:81:f5:22:ee:77:8b:3d:e8:66:6a:5f:59:69:73:15:bb:69:
         46:e9:df:8c:7c:1d:28:b5:71:ed:2e:ca:8e:d3:08:da:85:b4:
         6c:26:89:85:16:c3:9a:e4:45:ef:3d:16:a2:32:45:70:e5:7e:
         82:e1:55:32:e7:1a:63:6b:56:8f:11:70:53:6f:71:d8:e0:76:
         bc:af:bd:dc:53:d6:fb:f0:b6:29:5f:3b:3f:dd:5c:58:b4:f0:
         d2:bb:63:d6:7f:b6:5f:29:ac:43:fa:56:f6:38:a4:03:6e:f3:
         b6:0d:e3:94:4c:0e:de:28:0c:63:27:94:5c:c8:15:78:c1:3b:
         a3:9f:f3:7f:d8:79:c1:ee:23:da:42:ef:25:40:a1:b9:e4:54:
         c4:d0:6b:81:b8:c1:b6:78:aa:d9:25:31:25:fe:5c:a8:d4:46:
         61:38:2e:6e:ba:34:b6:21:cb:66:47:9e:4f:ca:e2:6a:6a:06:
         60:d4:cb:fd:e6:a2:d5:d3:44:40:f1:f9:a9:0d:38:47:a4:20:
         1a:59:4f:14:ab:ab:e9:20:53:91:1b:0e:57:7b:2e:72:d6:1c:
         73:37:d3:17:f6:65:75:ef:27:19:ee:32:2d:ac:ca:46:c4:aa:
         ea:60:d8:6c:fa:62:ad:d4:34:f5:f9:57:48:8f:c0:b3:30:0e:
         13:ec:69:7b:52:97:d6:f5:fa:16:bb:38:c6:03:2f:1a:21:6e:
         bb:69:2a:74:dc:3c:71:3e:af:91:dd:28:86:ca:c8:3b:58:29:
         07:3b:5c:67:3d:31
</code></pre>
<p>我们看到：client-cert.pem与cert.pem在“X509v3 Extended Key Usage”一项有差别，client-pert.pem除了包含TLS Web Server Authentication，还增加了TLS Web Client Authentication。</p>
<h4>2. echoserver与echoclient</h4>
<p>Go标准库提供了tls的基本实现，支持tls1.2和1.3版本。下面是echoserver的主要源码：</p>
<pre><code>// github.com/bigwhite/experiments/gmssl-examples/tls/server/server.go
func main() {
    cert, err := tls.LoadX509KeyPair("./certs/cert.pem", "./certs/key.pem")
    if err != nil {
        fmt.Println("load x509 keypair error:", err)
        return
    }
    cfg := &amp;tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
    }
    listener, err := tls.Listen("tcp", ":18000", cfg)
    if err != nil {
        fmt.Println("listen error:", err)
        return
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("accept error:", err)
            return
        }
        fmt.Println("accept connection:", conn.RemoteAddr())
        go func() {
            for {
                // echo "request"
                var b = make([]byte, 16)
                _, err := conn.Read(b)
                if err != nil {
                    fmt.Println("connection read error:", err)
                    conn.Close()
                    return
                }

                fmt.Println(string(b))
                _, err = conn.Write(b)
                if err != nil {
                    fmt.Println("connection write error:", err)
                    return
                }
            }
        }()
    }
}

</code></pre>
<p>我们看到基于tls的echoserver与一个普通的tcp server的代码差别不多，最主要就是在创建listener时传入了一个tls.Config结构，这个结构中有tls握手(handshake)所需要的全部信息，包括server端使用的私钥与证书(通过LoadX509KeyPair加载)以及对client端进行证书校验的标志(ClientAuth:   tls.RequireAndVerifyClientCert)。一旦连接建立，握手成功，后续的数据读写都和基于tcp连接的普通服务端程序无异。</p>
<p>下面是echoclient的主要源码：</p>
<pre><code>// github.com/bigwhite/experiments/gmssl-examples/tls/client/client.go

func main() {
    cert, err := tls.LoadX509KeyPair("./certs/client-cert.pem", "./certs/client-key.pem")
    if err != nil {
        fmt.Println("load x509 keypair error:", err)
        return
    }

    conn, err := tls.Dial("tcp", "example.com:18000", &amp;tls.Config{
        Certificates: []tls.Certificate{cert},
    })
    if err != nil {
        fmt.Println("failed to connect: " + err.Error())
        return
    }
    defer conn.Close()

    fmt.Println("connect ok")
    for i := 0; i &lt; 100; i++ {
        _, err := conn.Write([]byte("hello, gm"))
        if err != nil {
            fmt.Println("conn write error:", err)
            return
        }

        var b = make([]byte, 16)
        _, err = conn.Read(b)
        if err != nil {
            fmt.Println("conn read error:", err)
            return
        }
        fmt.Println(string(b))
        time.Sleep(time.Second)
    }
}
</code></pre>
<p>client端的代码更为简单一些，只需load client端使用的私钥与证书，然后传给tls.Config实例。tls.Dial使用该Config实例便可以顺利连接echoserver。</p>
<h4>3. 用于验证对方证书的CA证书</h4>
<p>在上面两个程序中都没有提到CA证书，那么server端和client端用什么去验证对方的公钥证书呢？其实依旧是用mkcert创建的CA证书去验证，只不过由于mkcert将CA证书安装到了操作系统trust store路径中，程序可以在系统CA证书中自动找到用来验证client和server两端公钥证书的CA证书，因此无需在程序中显式加载特定CA证书。</p>
<p>如果我们执行mkcert -uninstall，那么client程序在与server作tls handshake时就会报如下错误：</p>
<pre><code>// client程序的输出日志：
failed to connect: x509: certificate signed by unknown authority

// server程序的输出日志：
accept connection: 127.0.0.1:56734
connection read error: remote error: tls: bad certificate
</code></pre>
<h3>三. 密码算法在TLS握手以及后续通信过程中的作用</h3>
<p>在TLS握手阶段，密码算法起到了关键作用。那在握手的每个阶段都在使用什么算法呢？我们看看下面使用curl命令访问https站点的输出：</p>
<pre><code>$curl -v https://baidu.com
*   Trying 220.181.38.148:443...
* TCP_NODELAY set
* Connected to baidu.com (220.181.38.148) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
... ...
</code></pre>
<p>在这段内容中，我们看到这样一行输出：</p>
<pre><code>SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
</code></pre>
<p>这行后面的<strong>ECDHE-RSA-AES128-GCM-SHA256</strong>就是在握手过程中以及后续通信阶段会使用到的算法。这样的一串称为<strong>密码套件(cipher suite)</strong>，在SSL协议时代被称为<a href="https://datatracker.ietf.org/doc/html/draft-hickman-netscape-ssl-00#appendix-C.4">cipher kinds</a>。</p>
<p>密码套件一般由多个用途不同的密码算法名称组合而成(套件中的算法都是要配合使用的，单独使用没法保证信息安全传输)。下面是openssl-1.1.1f支持的密码套件列表：</p>
<pre><code>$openssl ciphers -V | column -t
0x13,0x02  -  TLS_AES_256_GCM_SHA384         TLSv1.3  Kx=any       Au=any    Enc=AESGCM(256)             Mac=AEAD
0x13,0x03  -  TLS_CHACHA20_POLY1305_SHA256   TLSv1.3  Kx=any       Au=any    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0x13,0x01  -  TLS_AES_128_GCM_SHA256         TLSv1.3  Kx=any       Au=any    Enc=AESGCM(128)             Mac=AEAD
0xC0,0x2C  -  ECDHE-ECDSA-AES256-GCM-SHA384  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AESGCM(256)             Mac=AEAD
0xC0,0x30  -  ECDHE-RSA-AES256-GCM-SHA384    TLSv1.2  Kx=ECDH      Au=RSA    Enc=AESGCM(256)             Mac=AEAD
0x00,0x9F  -  DHE-RSA-AES256-GCM-SHA384      TLSv1.2  Kx=DH        Au=RSA    Enc=AESGCM(256)             Mac=AEAD
0xCC,0xA9  -  ECDHE-ECDSA-CHACHA20-POLY1305  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xCC,0xA8  -  ECDHE-RSA-CHACHA20-POLY1305    TLSv1.2  Kx=ECDH      Au=RSA    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xCC,0xAA  -  DHE-RSA-CHACHA20-POLY1305      TLSv1.2  Kx=DH        Au=RSA    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xC0,0x2B  -  ECDHE-ECDSA-AES128-GCM-SHA256  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AESGCM(128)             Mac=AEAD
0xC0,0x2F  -  ECDHE-RSA-AES128-GCM-SHA256    TLSv1.2  Kx=ECDH      Au=RSA    Enc=AESGCM(128)             Mac=AEAD
0x00,0x9E  -  DHE-RSA-AES128-GCM-SHA256      TLSv1.2  Kx=DH        Au=RSA    Enc=AESGCM(128)             Mac=AEAD
0xC0,0x24  -  ECDHE-ECDSA-AES256-SHA384      TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AES(256)                Mac=SHA384
... ...
</code></pre>
<p>我们看看上面输出的后四列的含义：</p>
<ul>
<li>第四列（Kx）</li>
</ul>
<p>Kx代表key exchange，这一列是密钥交换算法，常见的密钥交换算法包括DH(Diffie-Hellman)、DHE(Diffie-Hellman Ephemeral)、ECDHE(在DHE算法的基础上利用了ECC椭圆曲线特性)等。在tls握手阶段，密钥交换算法用于在不安全的通道上协商会话加密(对称加密)算法密钥。</p>
<ul>
<li>第五列（Au)</li>
</ul>
<p>Au代表authentication，这一列是身份认证算法，通常是非对称加密算法，比如：RSA、ECDSA等。该算法用于服务端与客户端相互验证对方的公钥数字证书时。</p>
<ul>
<li>第六列（Enc）</li>
</ul>
<p>Enc代表对称加密算法，比如：AES、CHACHA20等，对称加密算法在tls握手后用于对客户端与服务端交互的数据进行加解密，它的加解密性能要比非对称加密算法快上很多。</p>
<ul>
<li>第七列（Mac）</li>
</ul>
<p>Mac代表Message Authentication Code，消息认证码算法，本质上是一个hash函数，用于计算数据的摘要值，是常用的用于保证消息数据完整性的工具。常见的算法有SHA1、SHA256等。</p>
<p>有了这些知识，我们再回到前面的<strong>ECDHE-RSA-AES128-GCM-SHA256</strong>，我们可以知道这个密码套件使用ECDHE作为密钥交换算法，使用RSA作为服务器认证算法（非对称加密），使用AES128-GCM作为对称加密算法，使用SHA256作为消息认证码算法。</p>
<blockquote>
<p>注：TLS 1.3版本的握手过程已经修改，仅需对称加密和Mac算法参与，因此TLS 1.3的密码套件格式已经变化。在TLS 1.3中，密码套件仅用于协商对称加密和MAC算法。对应的，我们看到上面OpenSSL输出的TLSv1.3版本的密码套件(如TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256等)的Kx和Au都是any。换句话说：TLSv1.2和TLSv1.3版本的密码套件并不兼容，不能混用(TLS v1.3的密码套件不能用在TLS v1.2版本中，反之亦然)。</p>
</blockquote>
<p>Go标准库(Go 1.18.3)内置支持的cipher suite如下：</p>
<pre><code>// $GOROOT/src/crypto/tls/cipher_suites.go
func CipherSuites() []*CipherSuite {
    return []*CipherSuite{
        {TLS_RSA_WITH_AES_128_CBC_SHA, "TLS_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_RSA_WITH_AES_256_CBC_SHA, "TLS_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_RSA_WITH_AES_128_GCM_SHA256, "TLS_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_RSA_WITH_AES_256_GCM_SHA384, "TLS_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},

        {TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", supportedOnlyTLS13, false},
        {TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384", supportedOnlyTLS13, false},
        {TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256", supportedOnlyTLS13, false},

        {TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
    }
}
</code></pre>
<blockquote>
<p>每个密码套件具有唯一标识值(value)，这些值在https://www.iana.org/assignments/tls-parameters/tls-parameters.xml中有标准参考。</p>
</blockquote>
<p>我们看到这些密码套件中的算法都是一些耳熟能详的国际标准密码算法，但并没有看到我们国家的国密的影子？我们国家的国密算法都有哪些？是否可以作为TLS握手过程使用的密码套件的一部分呢？如何基于国密算法实现一个安全传输层呢？我们接下来就正式进入国密算法(前面的铺垫有些长^_^)。</p>
<h3>四. 国家商用密码(国密)介绍</h3>
<p>密码算法是最基础、最重要的密码技术。国家密码管理局近十年来，先后发布了祖冲之序列密码算法、SM2~SM9等商用密码系列（SM系列）算法，构成了包含序列密码算法、对称密码算法、非对称密码算法、密码杂凑算法和标识密码算法等在内的完整、自主国产密码算法体系。2019年10月26日，第十三届全国人民代表大会常务委员会第十四次会议表决通过了《中华人民共和国密码法》，并于2020年1月1日起施行。这些算法目前也已经成为ISO/IEC相关国际标准了(只是在国外的应用还<strong>极少</strong>)。</p>
<p>国密是<strong>国家商用密码标准</strong>的简称。商用密码是指对不涉及国家秘密内容的信息进行加密保护或者安全认证。公民、法人和其他组织可以依法使用商用密码保护网络与信息安全。所有的国密相关标准都可以在<a href="http://www.gmbz.org.cn/main/bzlb.html">这个站点查询</a>到：http://www.gmbz.org.cn/main/bzlb.html。国密算法用SM(“商密”二字的拼音头字母组合)作为前缀标识，比如上面提到的SM2、SM3、SM4、SM9等等。</p>
<p>我们较为熟悉的是像RSA、AES这样的国际标准常用密码算法。初次看到SM2、SM3等算法名字的童鞋可能会有点懵，这些算法是什么密码算法，用在哪里？SM系列密码算法有很多，我们不能一一说明，我们重点来看看与安全传输层建立与认证相关的算法：</p>
<ul>
<li>SM2：椭圆曲线公钥密码算法</li>
</ul>
<p>SM2是用在公钥基础设施（PKI）领域的椭圆曲线公钥密码算法，与大名鼎鼎的RSA算法一样，是一种用于<strong>非对称加密</strong>的算法。该算法包括3部分：数字签名算法、密钥协商算法和加密/解密算法。该算法推荐使用素域为256比特的椭圆曲线。与RSA公钥密码算法相比，SM2椭圆曲线公钥密码算法具有安全性高、密钥短、速度快等优势。256比特的SM2椭圆曲线公钥密码算法密码强度已超过RSA-2048。SM2椭圆曲线公钥密码算法使用的密钥长度通常为192～256比特，而RSA公钥密码算法通常需要1024～2048比特。在同等安全强度下，SM2椭圆曲线公钥密码算法在用私钥签名时，速度远超RSA公钥密码算法。</p>
<p>SM2椭圆曲线公钥密码算法广泛应用于电子政务、移动办公、电子商务、移动支付、电子证书等领域。在公钥基础设施（PKI）领域，基于SM2椭圆曲线公钥密码算法的数字证书应用最具有代表性。</p>
<ul>
<li>SM3：密码杂凑算法</li>
</ul>
<p>SM3实质就是一种<strong>密码散列函数</strong>标准，再简单地说就是Hash函数。和我们熟悉的Hash散列算法SHA-1、<strong>SHA256</strong>等一样，SM3也主要用于数字签名及验证、消息认证码(MAC)生成及验证、随机数生成等领域。</p>
<p>SM3密码杂凑算法消息分组长度为512比特，输出摘要长度为256比特。SM3密码杂凑算法在MD（Message Digest）结构的基础上，新增了16步全异或操作、消息双字介入、加速雪崩效应的P置换等多种设计技术，能够有效避免高概率的局部碰撞，有效抵抗强碰撞性的差分攻击、弱碰撞性的线性攻击和比特追踪攻击等密码攻击方法。SM3密码杂凑算法能够有效抵抗目前已知的攻击方法，具有较高的安全冗余，在安全级别上与SHA256相当。</p>
<ul>
<li>SM4：分组密码算法</li>
</ul>
<p>SM4分组密码算法广泛应用于有<strong>对称加解密</strong>需求的应用系统和产品，与我们熟知的AES对称加密算法具有相同用途。</p>
<p>SM4算法的分组长度为128比特，密钥长度为128比特，加密算法和密钥扩展算法都采用32轮非线性迭代结构，解密算法与加密算法相同，只是轮密钥的使用顺序相反，解密轮密钥是加密轮密钥的逆序。轮变换使用的模块包括异或运算、8比特输入8比特输出的S盒，以及一个32比特输入的线性置换。</p>
<p>在密码指标性能方面，SM4分组密码算法的S盒设计已达到欧美分组密码标准算法的水平，具有较高的安全特性。线性置换的分支数达到了最优，可以抵抗差分攻击、线性攻击、代数攻击等。它具有算法速度快、实现效率高、安全性好等优点，主要用于保护数据的机密性。</p>
<p>除了密码算法之外，国家密码管理局还颁布一系列<strong>周边标准</strong>，比如基于国密的SSL传输层安全协议，以下简称为<strong>国密SSL</strong>。</p>
<p>最初的国密SSL是作为密码行业标准存在的，没有独立的协议标准定义，而是定义在SSL LPN产品的技术规范里，即<a href="www.gmbz.org.cn/main/viewfile/20180110021416665180.html">《GM/T 0024-2014 SSL VPN技术规范》</a>。</p>
<p>后来，国密SSL从密码行业标准上升到了独立的国家标准，也就是《GB/T 38636-2020 信息安全技术 传输层密码协议(TLCP)》，新版标准基本兼容《GM/T 0024-2014 SSL VPN技术规范》，主要变化是增加了GCM的密码套件：ECC_SM4_GCM_SM3和ECDHE_SM4_GCM_SM3以及去掉了行标《GM/T 0024-2014》中的涉及SM1和RSA的密码套件。</p>
<p>国密SSL是参考TLS 1.1制定的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-6.png" alt="" /></p>
<p>但“遗憾”的是<strong>国密SSL与TLS协议并不兼容</strong>，这就意味着现有的各个编程语言实现的TLS实现在不经改造的前提下，是无法支持国密SSL握手过程的。</p>
<p>此外，前面提到的TLS握手涉及到的证书都是RSA证书(前面证书内容中Public Key Algorithm: rsaEncryption)，即用RSA算法生成的公钥，用RSA算法的CA私钥签过名的证书。另一端在验证证书时，也要用RSA算法公钥(CA公钥)验证证书。如果我们要支持基于SM2算法的证书体系，需要CA、参与通信的两端都要支持SM2算法。而如今支持SM2的CA少之又少。并且，从前面我们看到的Go标准库TLS实现内置的密码套件列表来看，<strong>我们也没有看到SM2等国密算法实现的踪影</strong>。</p>
<p>不得不说，这是当前支持国密的一个“尴尬”。</p>
<p>那么我们要如何支持国密证书以及国密SSL呢？我们继续向下看。</p>
<h3>五. 基于国密证书的tls身份认证</h3>
<h4>1. 使用openssl生成国密证书并验证是否可以成功进行tls握手</h4>
<p>openssl是加解密领域“风向标”，openssl在1.1.1版本中加入对SM系列算法的支持：</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-2.png" alt="" /></p>
<p>大家可以通过下面命令查看你的openssl是否支持SM2椭圆曲线公钥密码算法：</p>
<pre><code>$openssl ecparam -list_curves | grep SM2
  SM2       : SM2 curve over a 256 bit prime field
</code></pre>
<p>如果支持，我们就可以利用该算法制作国密证书了(openssl-sm2/certs下面)。</p>
<ul>
<li>使用SM2创建server端私钥</li>
</ul>
<pre><code>$openssl ecparam -genkey -name SM2 -out server-sm2.key
</code></pre>
<ul>
<li>创建server csr </li>
</ul>
<pre><code>$openssl req -new -out server-sm2.csr -key server-sm2.key
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
</code></pre>
<p>查看该csr：</p>
<pre><code>$openssl req -in server-sm2.csr -noout -text
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:5b:3f:7e:c7:36:43:9c:22:cf:68:34:73:7f:c2:
                    11:23:05:2d:e5:34:5f:29:30:11:c5:c4:f1:df:e2:
                    97:9d:5c:eb:6c:29:3e:d0:e3:a2:d4:6c:67:e4:4f:
                    42:90:70:a2:dc:db:a6:b4:fd:5d:53:b6:53:8e:fd:
                    a8:37:aa:5e:4b
                ASN1 OID: SM2
        Attributes:
            a0:00
    Signature Algorithm: ecdsa-with-SHA256
         30:45:02:21:00:be:4b:31:93:fb:6a:74:2f:0a:0d:8d:69:08:
         d1:ad:bf:b2:e8:02:c1:76:c5:50:01:f2:f9:c8:1e:6f:1f:4f:
         9b:02:20:2c:43:16:5f:a4:4b:fb:2d:26:13:04:e0:ef:27:d1:
         84:69:41:71:9a:aa:e8:29:1d:98:f8:0c:df:be:52:c6:9d
</code></pre>
<p>可以看到：</p>
<pre><code>Public Key Algorithm: id-ecPublicKey
ASN1 OID: SM2
</code></pre>
<p>sm2算法是id-ecPublicKey算法的别名(alias)：</p>
<pre><code>$openssl list -public-key-algorithms

... ...
Name: sm2
    Alias for: id-ecPublicKey
</code></pre>
<ul>
<li>使用mkcert创建的ca来签发证书</li>
</ul>
<p>该ca是使用RSA算法创建的。我们用它签发sm2证书。</p>
<pre><code>$openssl x509 -req -in server-sm2.csr -CA ~/.local/share/mkcert/rootCA.pem -CAkey ~/.local/share/mkcert/rootCA-key.pem -CAcreateserial -out server-sm2-signed-by-rsa-ca.pem -days 5000
Signature ok
subject=C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
Getting CA Private Key
</code></pre>
<p>查看生成的server端证书：</p>
<pre><code>$openssl x509 -in server-sm2-signed-by-rsa-ca.pem -noout -text
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            5c:9c:ac:2f:03:8e:4e:72:fd:41:8a:c5:eb:8e:d4:c0:fc:0f:8a:4b
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul 11 06:23:28 2022 GMT
            Not After : Mar 19 06:23:28 2036 GMT
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:5b:3f:7e:c7:36:43:9c:22:cf:68:34:73:7f:c2:
                    11:23:05:2d:e5:34:5f:29:30:11:c5:c4:f1:df:e2:
                    97:9d:5c:eb:6c:29:3e:d0:e3:a2:d4:6c:67:e4:4f:
                    42:90:70:a2:dc:db:a6:b4:fd:5d:53:b6:53:8e:fd:
                    a8:37:aa:5e:4b
                ASN1 OID: SM2
    Signature Algorithm: sha256WithRSAEncryption
         2b:67:c0:12:41:ad:da:2a:2f:9f:89:81:f1:ef:4a:4b:6d:66:
         e8:93:62:e0:68:d4:5b:0e:8a:83:2b:4d:77:36:d1:8e:f2:d6:
         92:b0:7f:db:12:78:49:ac:c4:80:2b:ca:c8:70:91:c3:2f:31:
         8d:5d:97:27:60:77:95:e6:61:7c:62:c4:f5:0c:ce:90:43:7d:
         0c:f6:4e:8d:62:f3:67:08:4b:7e:5e:ad:0b:11:13:13:30:ec:
         d2:fc:78:ae:77:ca:97:f1:eb:fd:a3:5d:0f:58:70:a0:b3:2a:
         6e:91:eb:81:37:6f:54:a9:56:9b:11:3c:4e:63:0b:a2:d7:d6:
         36:b4:7f:d2:90:c3:15:ab:9b:bf:86:98:bb:9a:1c:64:71:3b:
         92:4c:aa:89:d1:8b:03:35:34:ad:64:66:83:bc:0d:5f:38:ba:
         a0:07:82:92:1b:44:ef:72:c2:36:eb:38:84:ac:a1:d3:44:17:
         a8:7b:d5:64:f6:55:05:5f:3a:3b:b5:eb:1a:66:51:33:7a:76:
         ce:e3:cc:82:04:f2:28:70:90:3a:57:a5:db:32:08:47:f1:4d:
         81:33:87:dd:b6:dc:4f:4f:49:59:e2:ac:71:a4:2f:7e:08:14:
         b0:cd:96:2d:fb:3d:b8:f2:c5:db:de:b9:0c:fe:91:15:fb:b1:
         2e:df:23:6f:3e:26:2c:66:db:5e:e2:f6:f3:1f:23:2c:5c:70:
         1d:d1:2b:b2:6e:ae:87:c6:cd:53:44:23:b0:1d:8d:08:40:3c:
         02:87:81:1d:65:04:2a:b8:c6:f5:59:28:6a:ea:22:95:d3:e2:
         24:93:9e:6c:d6:d7:0a:25:5b:4e:4a:cf:43:4c:71:e2:1a:bf:
         26:de:27:14:38:ea:69:9c:a9:bf:12:3a:5b:65:33:4e:83:87:
         81:5e:85:2a:e3:62:c7:5d:0e:15:e7:35:06:35:45:69:db:0b:
         aa:c6:45:e4:74:93:aa:45:e8:6f:22:11:15:14:f1:5a:4e:0a:
         34:e2:74:eb:44:32

</code></pre>
<p>使用openssl的s_server和s_client命令验证是否可以握手成功：</p>
<pre><code>$openssl s_server -tls1_2 -accept 14443 -key server-sm2.key -cert server-sm2-signed-by-rsa-ca.pem -debug -msg
Using default temp DH parameters
ACCEPT

$openssl s_client -connect 127.0.0.1:14443 -debug -msg -tls1_2
</code></pre>
<p>结果s_server报如下错：</p>
<pre><code>... ...
&gt;&gt;&gt; TLS 1.2, Alert [length 0002], fatal handshake_failure
    02 28
ERROR
139761999033664:error:1417A0C1:SSL routines:tls_post_process_client_hello:no shared cipher:../ssl/statem/statem_srvr.c:2283:
shutting down SSL
CONNECTION CLOSED
</code></pre>
<p>可以看到openssl虽然可以生成sm2公钥证书，但在tls 1.2协议下无法成功实现tls握手。</p>
<h4>2. 使用gmssl进行tls握手</h4>
<p>openssl不支持，但国内的大神基于openssl1.1.0建立了gmssl分支，这就是<a href="http://gmssl.org/">gmssl工程</a>。该工程为openssl增加了对国密算法以及gm ssl协议的各种支持。接下来我们就来试试用gmssl是否可以实现基于sm2证书的tls握手成功。</p>
<p>gmssl工程感觉还不够成熟，安装和运行过程有一些“坑”，这里简要说说。</p>
<ul>
<li>安装gmssl</li>
</ul>
<pre><code>$wget -c https://github.com/guanzhi/GmSSL/archive/master.zip
$unzip master.zip
$cd master
</code></pre>
<p>注意在执行其他config命令之前，先在Configure文件和test/build.info这个文件中, 把</p>
<pre><code>use if $^O ne "VMS", 'File::Glob' =&gt; qw/glob/;
</code></pre>
<p>改成：</p>
<pre><code>use if $^O ne "VMS", 'File::Glob' =&gt; qw/:glob/;
</code></pre>
<p>否则会报下面错误：</p>
<pre><code>"glob" is not exported by the File::Glob module
Can't continue after import errors at ./Configure line 18.
</code></pre>
<p>接下来执行下面命令生成Makefile并构建：</p>
<pre><code>$./config
$make
</code></pre>
<p>编译后的文件在apps/gmssl，我将其cp到项目根目录下。执行gmssl：</p>
<pre><code>$./gmssl
./gmssl: symbol lookup error: ./gmssl: undefined symbol: BIO_debug_callback, version OPENSSL_1_1_0d
</code></pre>
<p>gmssl报错了！原因是加载器加载gmssl依赖的动态共享库时选择了系统openssl的相关库了：</p>
<pre><code>$ldd gmssl
    linux-vdso.so.1 (0x00007ffe9cc5b000)
    libssl.so.1.1 =&gt; /lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007fa3ca550000)
    libcrypto.so.1.1 =&gt; /lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007fa3ca27a000)
    libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa3ca257000)
    libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa3ca065000)
    libdl.so.2 =&gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa3ca05f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa3ca6ae000)
</code></pre>
<p>我们要在load时链接gmssl自己的库，需要修改一下LD_LIBRARY_PATH环境变量(这样做会导致openssl执行失败，建议不要放在全局环境变量配置中，可让其仅在某些窗口中生效)：</p>
<pre><code>$export LD_LIBRARY_PATH=/home/tonybai/.bin/gmssl/GmSSL-master:$LD_LIBRARY_PATH
</code></pre>
<p>除此之外，我们还需要做一个操作，那就是在/usr/local/ssl下放置一份openssl.cnf文件(可以从/usr/lib/ssl/openssl.cnf拷贝(openssl version -a，查看OPENSSLDIR))，否则gmssl在执行“gmssl s_server&#8230;”时会报如下错误：</p>
<pre><code>Can't open /usr/local/ssl/openssl.cnf for reading, No such file or directory
139679439200896:error:02001002:system library:fopen:No such file or directory:crypto/bio/bss_file.c:74:fopen('/usr/local/ssl/openssl.cnf','r')
139679439200896:error:2006D080:BIO routines:BIO_new_file:no such file:crypto/bio/bss_file.c:81:
</code></pre>
<p>这里gmssl版本如下：</p>
<pre><code>$gmssl version
GmSSL 2.5.4 - OpenSSL 1.1.0d  19 Jun 2019
</code></pre>
<p>好了，下面我们就来使用gmssl试试我们制作的sm2证书是否可以顺利完成tls握手。</p>
<pre><code>// 服务端
$gmssl s_server  -accept 14443 -key server-sm2.key -cert server-sm2-signed-by-rsa-ca.pem -debug -msg -tls1_2

// 客户端
$gmssl s_client -connect 127.0.0.1:14443 -debug -msg -tls1_2 -verifyCAfile /home/tonybai/.local/share/mkcert/rootCA.pem

---
SSL handshake has read 1209 bytes and written 310 bytes
Verification: OK
---
New, TLSv1.2, Cipher is ECDHE-SM2-WITH-SMS4-GCM-SM3
Server public key is 256 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-SM2-WITH-SMS4-GCM-SM3
    Session-ID: 53B8799C3A6F3752C634F764EB6B136BDFD39CEB0C2E28E7DD98D86F9FF4F333
    Session-ID-ctx:
    Master-Key: 6A50D31E3AEDDDF3FC608277087FB0DAACCC791DB296142ED37DE28E0DDA56FF1BB64431B66A76C468129E00F696338D
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 7200 (seconds)
    TLS session ticket:
    0000 - ee d3 08 4d 21 14 dc c8-40 8c d0 c4 31 f9 16 bc   ...M!...@...1...
    0010 - 85 f9 a2 8c f4 ba cf 90-4d 38 28 03 78 b0 4a 27   ........M8(.x.J'
    0020 - 17 c4 22 df 48 ea 8c 00-5a 92 0f ba eb 8a 1a dc   ..".H...Z.......
    0030 - b3 3d b4 15 ee df fc d0-66 59 5c c2 23 9e a4 4f   .=......fY\.#..O
    0040 - e0 77 54 b1 18 af 73 b0-b4 6a a7 c7 c7 d3 a4 a4   .wT...s..j......
    0050 - 8f 49 ff c7 bc 47 b5 19-09 21 4c db 71 76 d9 a5   .I...G...!L.qv..
    0060 - 49 0b c9 5d 09 b2 da b9-cc ec 04 5a 90 27 07 5f   I..].......Z.'._
    0070 - 2b f2 55 5c f4 69 01 32-90 f5 3a 19 b5 47 84 4c   +.U\.i.2..:..G.L
    0080 - 1c 64 66 63 f3 01 ab fe-b1 70 f7 98 b5 cc 23 8e   .dfc.....p....#.
    0090 - aa f4 1d 8a 79 5e 79 b7-04 f6 69 ed 62 d9 c7 ae   ....y^y...i.b...

    Start Time: 1657529930
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: yes
</code></pre>
<p>从客户端的输出来看，在明确ca证书位置的情况下(使用-verifyCAfile)，可以正确验证server端发来的sm2证书(见“Verify return code: 0 (ok)”)。</p>
<h3>六. 使用Go实现tls/tlcp自适应双向认证</h3>
<p>gmssl为我们展示了一条支持国密的路径，即基于已有的开源项目的实现进行改造。Go标准库并不支持国密，因此在Go社区借鉴标准库中crypto的中算法以及tls包的结构，实现了对sm系列算法以及国密ssl的支持，<a href="https://github.com/tjfoc/gmsm">tjfoc/gmsm</a>就是其中之一。</p>
<blockquote>
<p>注：gmssl也提供了Go API接口，底层通过cgo调用gmssl C代码实现。</p>
</blockquote>
<p>gmsm不仅提供了国密算法的相关实现，还<strong>实现了tls与tclp协议的自适应支持</strong>。在这一小节，我们就用gmsm来演示一个<strong>tls/tlcp自适应双向认证</strong>的例子。</p>
<h4>1. 准备SM2国密公钥证书</h4>
<p>按照gmsm自适应tls/tlcp实现的要求，我们需要先准备一堆证书(tlcp与tls不同，其加密与签名是由两个证书分别完成的，而不仅仅是tls的一个证书)，包括：</p>
<ul>
<li>rsa: 基于rsa的CA证书、server证书和client证书</li>
<li>gm: 基于gm的CA证书、server签名(sign)和加密(enc)证书、client端验证(auth)证书。</li>
</ul>
<p>考虑到mkcert不支持国密，这里我们切换到用gmssl来创建这些证书。我将创建证书的命令集中在两个shell脚本中：gen_rsa_cert.sh和gen_gm_cert.sh，前者用于创建基于RSA的各种证书，后者则是创建基于国密的各种证书。这两个脚本的源码如下：</p>
<ul>
<li>gen_rsa_cert.sh </li>
</ul>
<pre><code>// gmssl-examples/gmsm-tls-and-tlcp/certs/gen_rsa_cert.sh

#!/bin/bash

## RSA Certs

### CA
gmssl genpkey -out ca-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -x509 -new -nodes -key ca-rsa-key.pem -subj "/CN=myca.com" -days 5000 -out ca-rsa-cert.pem 

### server key and cert
gmssl genpkey -out server-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -new -key server-rsa-key.pem -subj "/CN=example.com" -out server-rsa.csr
gmssl x509 -req -in server-rsa.csr -CA ca-rsa-cert.pem -CAkey ca-rsa-key.pem -CAcreateserial -out server-rsa-cert.pem -days 5000  -extfile ./server.cnf -extensions ext
gmssl verify -CAfile ca-rsa-cert.pem server-rsa-cert.pem

### client key and cert
gmssl genpkey -out client-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -new -key client-rsa-key.pem -subj "/CN=client1.com" -out client-rsa.csr
gmssl x509 -req -in client-rsa.csr -CA ca-rsa-cert.pem -CAkey ca-rsa-key.pem -CAcreateserial -out client-rsa-cert.pem -days 5000 -extfile ./client.cnf -extensions ext
gmssl verify -CAfile ca-rsa-cert.pem client-rsa-cert.pem
</code></pre>
<ul>
<li>gen_gm_cert.sh </li>
</ul>
<pre><code>#!/bin/bash

## SM CA

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out ca-gm-key.pem
gmssl req -x509 -new -nodes -key ca-gm-key.pem -subj "/CN=myca.com" -days 5000 -out ca-gm-cert.pem

### server: sign key and cert

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out server-gm-sign-key.pem
gmssl req -new -key server-gm-sign-key.pem -subj "/CN=example.com" -out server-gm-sign.csr
gmssl x509 -req -in server-gm-sign.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out server-gm-sign-cert.pem -days 5000 -extfile ./server.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem server-gm-sign-cert.pem

### server: enc key and cer

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out server-gm-enc-key.pem
gmssl req -new -key server-gm-enc-key.pem -subj "/CN=example.com" -out server-gm-enc.csr
gmssl x509 -req -in server-gm-enc.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out server-gm-enc-cert.pem -days 5000 -extfile ./server.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem server-gm-enc-cert.pem

### client: auth key and cert

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out client-gm-auth-key.pem
gmssl req -new -key client-gm-auth-key.pem -subj "/CN=client1.com" -out client-gm-auth.csr
gmssl x509 -req -in client-gm-auth.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out client-gm-auth-cert.pem -days 5000 -extfile ./client.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem client-gm-auth-cert.pem
</code></pre>
<p>关于上面两个脚本，有几点说明一下：</p>
<ol>
<li>我们建立了两个CA，一个基于RSA，一个基于国密算法；这两个CA分别用于签发基于RSA的证书与基于国密的证书；</li>
<li>在生成证书中我们用到了x509证书的扩展属性subjectAltName、extendedKeyUsage和keyUsage。</li>
</ol>
<p>如果不使用subjectAltName扩展属性，Go语言的x509校验会报如下错误(在Go 1.18及后续版本中，即便设置GODEBUG=x509ignoreCN=0也不行)：</p>
<pre><code>certificate relies on legacy Common Name field, use SANs instead
</code></pre>
<p>同样Go也会对keyUsage做严格校验，如果是用来签名的证书中keyUsage不包含digitalSignature等，握手时也会报错：</p>
<pre><code>tls: the keyusage of cert[0] does not exist or is not for KeyUsageDigitalSignature
</code></pre>
<p>server.cnf与client.cnf的内容如下：</p>
<pre><code>// server.cnf
[req]
prompt = no
distinguished_name = dn
req_extensions = ext
input_password = 

[dn]
CN = example.com
emailAddress = webmaster@example.com
O = hello Ltd
L = Beijing
C = CN

[ext]
subjectAltName = DNS:example.com
extendedKeyUsage = clientAuth,serverAuth
keyUsage = critical,digitalSignature,keyEncipherment

// client.cnf

[req]
prompt = no
distinguished_name = dn
req_extensions = ext
input_password = 

[dn]
CN = client1.com
emailAddress = webmaster@client1.com
O = hello Ltd
L = Beijing
C = CN

[ext]
subjectAltName = DNS:client1.com
extendedKeyUsage = clientAuth
keyUsage = critical,digitalSignature,keyEncipherment
</code></pre>
<p>执行bash gen_rsa_cert.sh和bash gen_gm_cert.sh生成所有示例需要的证书：</p>
<pre><code>$ls *.pem|grep -v key
ca-gm-cert.pem
ca-rsa-cert.pem
client-gm-auth-cert.pem
client-rsa-cert.pem
server-gm-enc-cert.pem
server-gm-sign-cert.pem
server-rsa-cert.pem
</code></pre>
<h4>2. 支持tls与tlcp自适应的server</h4>
<p>下面是支持tls与tlcp自适应的server的源码：</p>
<pre><code>// gmssl-examples/gmsm-tls-and-tlcp/server/server.go
const (
    rsaCertPath     = "certs/server-rsa-cert.pem"
    rsaKeyPath      = "certs/server-rsa-key.pem"
    sm2SignCertPath = "certs/server-gm-sign-cert.pem"
    sm2SignKeyPath  = "certs/server-gm-sign-key.pem"
    sm2EncCertPath  = "certs/server-gm-enc-cert.pem"
    sm2EncKeyPath   = "certs/server-gm-enc-key.pem"
)

func main() {
    pool := x509.NewCertPool()
    rsaCACertPath := "./certs/ca-rsa-cert.pem"
    rsaCACrt, err := ioutil.ReadFile(rsaCACertPath)
    if err != nil {
        fmt.Println("read rsa ca err:", err)
        return
    }
    gmCACertPath := "./certs/ca-gm-cert.pem"
    gmCACrt, err := ioutil.ReadFile(gmCACertPath)
    if err != nil {
        fmt.Println("read gm ca err:", err)
        return
    }
    pool.AppendCertsFromPEM(rsaCACrt)
    pool.AppendCertsFromPEM(gmCACrt)

    rsaKeypair, err := tls.LoadX509KeyPair(rsaCertPath, rsaKeyPath)
    if err != nil {
        fmt.Println("load rsa x509 keypair error:", err)
        return
    }
    sigCert, err := tls.LoadX509KeyPair(sm2SignCertPath, sm2SignKeyPath)
    if err != nil {
        fmt.Println("load x509 gm sign keypair error:", err)
        return
    }
    encCert, err := tls.LoadX509KeyPair(sm2EncCertPath, sm2EncKeyPath)
    if err != nil {
        fmt.Println("load x509 gm enc keypair error:", err)
        return
    }

    cfg, err := tls.NewBasicAutoSwitchConfig(&amp;sigCert, &amp;encCert, &amp;rsaKeypair)
    if err != nil {
        fmt.Println("load basic config error:", err)
        return
    }

    cfg.MaxVersion = tls.VersionTLS12
    cfg.ClientAuth = tls.RequireAndVerifyClientCert
    cfg.ClientCAs = pool

    listener, err := tls.Listen("tcp", ":18000", cfg)
    if err != nil {
        fmt.Println("listen error:", err)
        return
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("accept error:", err)
            return
        }
        fmt.Println("accept connection:", conn.RemoteAddr())
        go func() {
            for {
                // echo "request"
                var b = make([]byte, 16)
                _, err := conn.Read(b)
                if err != nil {
                    fmt.Println("connection read error:", err)
                    conn.Close()
                    return
                }

                fmt.Println(string(b))
                _, err = conn.Write(b)
                if err != nil {
                    fmt.Println("connection write error:", err)
                    return
                }
            }
        }()
    }
}
</code></pre>
<p>说明一下：</p>
<ul>
<li>这里的tls包是并非标准库crypto/tls包，而是github.com/tjfoc/gmsm/gmtls。</li>
<li>由于要自适应tls/tlcp，我们加载了两个CA证书，一个是基于RSA创建的CA证书，一个是基于gm创建的CA证书，用于分别对tls协议和tlcp协议的客户端身份进行验证；</li>
<li>服务端加载了用于tls连接的RSA的server证书：rsaCertPath，同时也加载了用于tlcp连接的server端双证书：sm2SignCertPath和sm2EncCertPath。</li>
</ul>
<h4>3. tls client</h4>
<p>用于该示例的tls client与前面的echoclient十分类似，只不过加载的证书从mkcert生成的cert.pem改为certs/client-rsa-cert.pem，CA证书使用了我们刚刚生成的./certs/ca-rsa-cert.pem。</p>
<p>其他部分没有变化。这里就不罗列源码了，大家可以自行阅读gmssl-examples/gmsm-tls-and-tlcp/tlsclient/client.go</p>
<h4>4. tlcp client</h4>
<p>和tls client相比，我们只是将CA换为./certs/ca-gm-cert.pem，加载的client证书换成了certs/client-gm-auth-cert.pem，其他部分没有变化。这里也不罗列源码了，大家可以自行阅读gmssl-examples/gmsm-tls-and-tlcp/tlcpclient/client.go</p>
<h4>5. 验证tls/tlcp自适应双向认证</h4>
<p>通过make命令可以一键构建出上述的server、tlsclient和tlcpclient。</p>
<p>启动server：</p>
<pre><code>$./echoserver
</code></pre>
<p>启动tlsclient，验证tls双向认证：</p>
<pre><code>$./echo_tls_client
connect ok
hello, tls
hello, tls
... ...
</code></pre>
<p>如果看到上面的tls client输出，说明tls连接建立和双向验证ok。</p>
<p>我们再来启动tlcp client，验证tlcp双向认证：</p>
<pre><code>$./echo_tlcp_client
connect ok
reply: h
reply:
reply: e
reply: llo, tlcp
reply: h
reply:
reply: e
reply: llo, tlcp
reply: h
reply:
reply: e
reply: llo, tlcp
... ..
</code></pre>
<p>我们看到虽然tlcp连接建立成功并成功完成双向认证，但是基于已建立的tlcp的读写操作似乎并不想tls client那样“工整”，对应着server那端的输出如下：</p>
<pre><code>accept connection: 127.0.0.1:58088
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
</code></pre>
<p>虽然两段的数据都是完整的，没有丢失，但发送与接收的“效率”大幅下降，client端发出的一个“hello, tlcp”数据似乎是被分为两次发送出去的。而服务端给客户端的Reply更是分成了“四段”发送的，目前还没有调查为何会出现这种情况，也许与tjfoc/gmsm的实现有关。</p>
<blockquote>
<p>注：实测：tjfoc/gmsm尚不支持在tls协议握手时使用rsa CA证书签发的采用gm算法生成的sm2证书，可以参见gmssl-examples/gmsm-tls-and-tlcp/server_gm和tlsclient_gm。</p>
</blockquote>
<h3>七. 小结</h3>
<p>国密是中国密码标准，和国际密码标准相比，有一定的后发优势，但由于在国际上应用很少，其安全性虽然得到了形式验证，但似乎尚未得到实践中的大规模考验。基于国密的tlcp协议由于与tls不兼容，也导致其在应用上受到了极大的限制。</p>
<p>虽然有gmssl、有像tjfoc/gmsm这样的项目，但总体感觉国密在参考实现方面还不够成熟，生态还很欠缺，国家密码局在推广国密方面往往更多从法规层面。各个厂家往往都是因甲方需要国密而去满足要求，并没有原生推动国密的动力(譬如我们^_^)。</p>
<p>因此，国密任重道远啊。</p>
<p>本文内容仅供参考，可能有理解不正确和代码错误的地方，欢迎指正。</p>
<p>文中示例代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/gmssl-examples">这里</a>下载。</p>
<h3>八. 参考资料</h3>
<ul>
<li><a href="https://weread.qq.com/web/bookDetail/2fb3259071ef04932fbfd2e">《商用密码算法原理与C语言实现》</a> &#8211; https://weread.qq.com/web/bookDetail/2fb3259071ef04932fbfd2e</li>
<li><a href="https://weread.qq.com/web/bookDetail/f3132ec071e072c3f311e99">《商用密码应用与安全性评估》</a> &#8211; https://weread.qq.com/web/bookDetail/f3132ec071e072c3f311e99</li>
<li><a href="http://gmssl.org/docs/docindex.html">《GmSSL项目文档》</a> &#8211; http://gmssl.org/docs/docindex.html</li>
<li><a href="https://go.dev/blog/tls-cipher-suites">《Automatic cipher suite ordering in crypto/tls》</a> &#8211; https://go.dev/blog/tls-cipher-suites</li>
<li><a href="https://zhuanlan.zhihu.com/p/410212375">《国密TLCP协议的过去、现在与未来》</a> &#8211; https://zhuanlan.zhihu.com/p/410212375</li>
<li><a href="https://www.feistyduck.com/library/openssl-cookbook/online/">《Openssl cookbook》</a> &#8211; https://www.feistyduck.com/library/openssl-cookbook/online/</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
