MoreRSS

site iconTonyBai | 白明

《Go语言精进之路》作者,Go C程序员,架构师,技术讲师、撰稿人。先后贡献了lcut、cbehave、buildc多个工具框架。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

TonyBai | 白明的 RSS 预览

Go包构建:专家也未必了解的文件选择细节

2024-11-21 06:57:08

本文永久链接 – https://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package

在Go语言开发中,包(package)是代码组织的基本单位,也是基本的构建单元。Go编译器会将每个包构建成一个目标文件(.a),然后通过链接器将这些目标文件链接在一起,形成最终的可执行程序。

尽管Go包的构建过程看似简单,但实际上蕴含着许多值得深入了解的细节。例如,当我们执行go build命令时,Go编译器是如何选择需要编译的源文件的?你可能会回答:“不就是通过文件名中的ARCH和OS标识以及构建约束(build constraints)来选择的吗?” 虽然你的答案并没有错,但如果我进一步提出以下问题,你是否还能给出确切的答案呢?

假设一个Go源文件使用了如下的构建约束:

//go:build unix

package foo
// ... ...

在执行GOOS=android go build时,这个文件是否会被编译?如果执行的是GOOS=aix go build呢?而“unix”究竟包含了哪些操作系统?

再进一步,当一个源文件的文件名中包含ARCH和操作系统标识,并且文件内容中也使用了构建约束时,Go编译器会如何处理这些信息的优先级?

即使是经验丰富的Go专家,对于上述在包构建过程中涉及的文件选择细节,可能也只能给出模糊的答案。

在实际开发中,我们常常需要针对不同操作系统和架构编写特定的代码,这意味着灵活性与复杂性并存。Go的构建约束和文件名约定虽然为我们提供了灵活性,但也带来了额外的复杂性。理解这些规则不仅有助于优化构建过程,还能有效避免潜在的错误和不必要的麻烦。

在这篇文章中,我将与大家探讨Go包构建过程中源文件选择的细节,包括文件名中ARCH和os标识约定和构建约束的作用,以及二者的优先级处理问题。希望通过这些内容,帮助开发者更好地掌握Go语言的构建机制,从而提高开发效率。

为了更好地说明Go包构建时的文件选择逻辑,我们先从Go包构建的一些“表象”说起。

注:在本文中,我们将使用Go 1.17引入的新版build constraints写法://go:build ,之前的// +build aix darwin dragonfly freebsd js,wasm …写法已经不再被推荐使用。如果你想对旧版build constraints写法有一个全面了解以便与新写法对比,推荐阅读我的《Go语言精进之路:从新手到高手的编程思想、方法和技巧》第2册

1. 表象

在Go工程中,通常一个目录对应一个Go包,每个Go包下可以存在多个以.go为后缀的Go源文件,这些源文件只能具有唯一的包名(测试源文件除外),以标准库fmt包为例,它的目录下的源文件列表如下(以Go 1.23.0源码为例):

$ls $GOROOT/src/fmt
doc.go              export_test.go          print.go            stringer_example_test.go
errors.go           fmt_test.go         scan.go             stringer_test.go
errors_test.go          format.go           scan_test.go
example_test.go         gostringer_example_test.go  state_test.go

在这些文件中,哪些最终进入到了fmt包的目标文件(fmt.a)中呢?贴心的Go工具链为我们提供了查看方法:

$go list -f '{{.GoFiles}}' fmt
[doc.go errors.go format.go print.go scan.go]

对于独立于目标ARCH和OS的fmt包来说,其Go源文件的选择似乎要简单一些。我们看到,除了包测试文件(xxx_test.go),其他文件都被编译到了最终的fmt包中。

我们再来看一个与目标ARCH和OS相关性较高的net包。除去子目录,这个包目录下的Go源文件数量大约有220多个,但在macOS/amd64下通过go list查看最终进入net包目标文件的文件,大约只有几十个:

$go list -f '{{.GoFiles}}' net
[addrselect.go cgo_darwin.go cgo_unix.go cgo_unix_syscall.go conf.go dial.go dnsclient.go dnsclient_unix.go dnsconfig.go dnsconfig_unix.go error_posix.go error_unix.go fd_posix.go fd_unix.go file.go file_unix.go hook.go hook_unix.go hosts.go interface.go interface_bsd.go interface_darwin.go ip.go iprawsock.go iprawsock_posix.go ipsock.go ipsock_posix.go lookup.go lookup_unix.go mac.go mptcpsock_stub.go net.go netcgo_off.go netgo_off.go nss.go parse.go pipe.go port.go port_unix.go rawconn.go rlimit_unix.go sendfile_unix_alt.go sock_bsd.go sock_posix.go sockaddr_posix.go sockopt_bsd.go sockopt_posix.go sockoptip_bsdvar.go sockoptip_posix.go splice_stub.go sys_cloexec.go tcpsock.go tcpsock_posix.go tcpsock_unix.go tcpsockopt_darwin.go tcpsockopt_posix.go udpsock.go udpsock_posix.go unixsock.go unixsock_posix.go unixsock_readmsg_cloexec.go writev_unix.go]

接下来,我们跳出Go标准库,来看一个自定义的示例:

$tree -F buildconstraints/demo1
buildconstraints/demo1
├── foo/
│   ├── f1_android.go
│   ├── f2_linux.go
│   └── f3_darwin.go
└── go.mod

// buildconstraints/demo1/foo/f1_android.go 

//go:build linux

package foo

func F1() {
}

// buildconstraints/demo1/foo/f2_linux.go
//go:build android

package foo

func F2() {
}

// buildconstraints/demo1/foo/f3_darwin.go
//go:build android

package foo

func F3() {
}

在GOOS=android下构建buildconstraints/demo1/foo这个包,哪些文件会被选出来呢,看下面输出结果:

$GOOS=android go list -f '{{.GoFiles}}' github.com/bigwhite/demo1/foo
[f1_android.go f2_linux.go]

如果说前两个示例还好理解,那这第三个示例很可能会让很多开发者觉得有些“发蒙”。 别急,上面三个示例都是表象,接下来,我们就来仔细探索一下Go构建时的文件选择机制。

2. 文件选择机制

Go包构建时选择源文件的机制还是蛮繁琐的,我们需要从源码入手梳理出其主要逻辑,在Go 1.23版本中,Go包构建过程源文件选择逻辑的代码位于\$GOROOT/src/go/build/build.go中,这个源文件有2k多行,不过不用担心,我这里会替你把主要调用逻辑梳理为下图:

函数Import调用Default.Import去获取包的详细信息,信息用build.Package结构表示:

// $GOROOT/src/go/build/build.go
// A Package describes the Go package found in a directory.
  type Package struct {
      Dir           string   // directory containing package sources
      Name          string   // package name
      ImportComment string   // path in import comment on package statement
      Doc           string   // documentation synopsis
      ImportPath    string   // import path of package ("" if unknown)
      Root          string   // root of Go tree where this package lives
      SrcRoot       string   // package source root directory ("" if unknown)
      PkgRoot       string   // package install root directory ("" if unknown)
      PkgTargetRoot string   // architecture dependent install root directory ("" if unknown)
      BinDir        string   // command install directory ("" if unknown)
      Goroot        bool     // package found in Go root
      PkgObj        string   // installed .a file
      AllTags       []string // tags that can influence file selection in this directory
      ConflictDir   string   // this directory shadows Dir in $GOPATH
      BinaryOnly    bool     // cannot be rebuilt from source (has //go:binary-only-package comment)

      // Source files
      GoFiles           []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles)
      ... ...

其中的GoFiles就是参与Go包编译的源文件列表。

Default是默认的上下文信息,包括构建所需的默认goenv中几个环境变量,比如GOARCH、GOOS等的值:

// Default is the default Context for builds.
// It uses the GOARCH, GOOS, GOROOT, and GOPATH environment variables
// if set, or else the compiled code's GOARCH, GOOS, and GOROOT.
var Default Context = defaultContext()

Context的Import方法代码行数很多,对于要了解文件选择细节的我们来说,其中最重要的调用是Context的matchFile方法。

matchFile正是那个用于确定某个Go源文件是否应该被选入最终包文件中的方法。它内部的逻辑可以分为两个主要步骤。

第一步是调用Context的goodOSArchFile方法对Go源文件的名字进行判定,goodOSArchFile方法的判定也有两个子步骤:

  • 判断名字中的OS和ARCH是否在Go支持的OS和ARCH列表中

当前Go支持的OS和ARCH在syslist.go文件中有定义:

// $GOROOT/src/go/build/syslist.go

// knownArch is the list of past, present, and future known GOARCH values.
// Do not remove from this list, as it is used for filename matching.
var knownArch = map[string]bool{
    "386":         true,
    "amd64":       true,
    "amd64p32":    true,
    "arm":         true,
    "armbe":       true,
    "arm64":       true,
    "arm64be":     true,
    "loong64":     true,
    "mips":        true,
    "mipsle":      true,
    "mips64":      true,
    "mips64le":    true,
    "mips64p32":   true,
    "mips64p32le": true,
    "ppc":         true,
    "ppc64":       true,
    "ppc64le":     true,
    "riscv":       true,
    "riscv64":     true,
    "s390":        true,
    "s390x":       true,
    "sparc":       true,
    "sparc64":     true,
    "wasm":        true,
}

// knownOS is the list of past, present, and future known GOOS values.
// Do not remove from this list, as it is used for filename matching.
// If you add an entry to this list, look at unixOS, below.
var knownOS = map[string]bool{
    "aix":       true,
    "android":   true,
    "darwin":    true,
    "dragonfly": true,
    "freebsd":   true,
    "hurd":      true,
    "illumos":   true,
    "ios":       true,
    "js":        true,
    "linux":     true,
    "nacl":      true,
    "netbsd":    true,
    "openbsd":   true,
    "plan9":     true,
    "solaris":   true,
    "wasip1":    true,
    "windows":   true,
    "zos":       true,
}

我们也可以通过下面命令查看:

$go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
freebsd/riscv64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/loong64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/ppc64
openbsd/riscv64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
wasip1/wasm
windows/386
windows/amd64
windows/arm
windows/arm64

注:像sock_bsd.go、sock_posix.go这样的Go源文件,虽然它们的文件名中包含posix、bsd等字样,但这些文件实际上只是普通的Go源文件。其文件名本身并不会影响Go包在构建时选择文件的结果。

  • 调用matchTag来判定该Go源文件名字中的OS和ARCH是否与当前上下文信息中的OS和ARCH匹配

Go支持的源文件名组成格式如下:

  //  name_$(GOOS).*
  //  name_$(GOARCH).*
  //  name_$(GOOS)_$(GOARCH).*
  //  name_$(GOOS)_test.*
  //  name_$(GOARCH)_test.*
  //  name_$(GOOS)_$(GOARCH)_test.*

不过这里有三个例外,即:

如果上下文中的GOOS=android,那么文件名字中OS值为linux的Go源文件也算是匹配的;

如果上下文中的GOOS=illumos,那么文件名字中OS值为solaris的Go源文件也算是匹配的;

如果上下文中的GOOS=ios,那么文件名字中OS值为darwin的Go源文件也算是匹配的。

还有一个特殊处理,那就是当文件名字中OS值为unix时,该源文件可以匹配以下上下文中GOOS的值:

// $GOROOT/src/go/build/syslist.go

// unixOS is the set of GOOS values matched by the "unix" build tag.
// This is not used for filename matching.
// This list also appears in cmd/dist/build.go and
// cmd/go/internal/imports/build.go.
var unixOS = map[string]bool{
    "aix":       true,
    "android":   true,
    "darwin":    true,
    "dragonfly": true,
    "freebsd":   true,
    "hurd":      true,
    "illumos":   true,
    "ios":       true,
    "linux":     true,
    "netbsd":    true,
    "openbsd":   true,
    "solaris":   true,
}

这里面列出os都是所谓的“类Unix”操作系统。

如果goodOSArchFile方法返回文件名匹配成功,那么第二步就是调用Context的shouldBuild方法对Go源文件中的build constraints进行判定,这个判定过程也是调用matchTag完成的,因此规则与上面对matchTag的说明一致。如果判定match成功,那么该源文件将会被Go编译器编译到最终的Go包目标文件中去。

下面我们结合文章第一节“表象”中的那个自定义示例来判定一下为何最终会输出那个结果。

3. 示例分析

在buildconstraints/demo1/foo包目录中,一共有三个Go源文件:

$tree -F foo
foo
├── f1_android.go
├── f2_linux.go
└── f3_darwin.go

注意:当前我的系统为darwin/amd64,但我们使用了GOOS=android的环境变量。我们顺着上一节梳理出来的文件选择判定的主逻辑,对着三个文件逐一过一遍。

  • f1_android.go

首先用goodOSArchFile判定文件名是否匹配。当GOOS=android时,文件名中的os为android,文件名匹配成功,

然后用shouldBuild判定文件中的build constraints是否匹配。该文件的约束为linux,在上面matchTag的三个例外规则里提到过,当GOOS=android时,如果build constraints是linux,是可以匹配的。

因此,f1_android.go将出现在最终编译文件列表中。

  • f2_linux.go

首先用goodOSArchFile判定文件名是否匹配。当GOOS=android时,文件名中的os为linux,linux显然在go支持的os列表中,并且根据matchTag的例外规则,当GOOS=android时,文件名中的os为linux时是可以匹配的。

然后用shouldBuild判定文件中的build constraints是否匹配。该文件的约束为android,与GOOS相同,可以匹配。

因此,f2_linux.go将出现在最终编译文件列表中。

  • f3_darwin.go

首先用goodOSArchFile判定文件名是否匹配。当GOOS=android时,文件名中的os为darwin,虽然darwin在go支持的os列表中,但darwin与GOOS=android并不匹配,因此在goodOSArchFile这步中,f3_darwin.go就被“淘汰”掉了!即便f3_darwin.go中的build constraints为android。

因此,f3_darwin.go不会出现在最终编译文件列表中。

如果再增加一个源文件f4_unix.go,其内容为:

//go:build android

func F4() {
}

这个f4_unix.go是否会出现在最终的包编译文件列表中呢?这个作为思考题留给大家了,也欢迎你在评论区留言,说说你的思考结果。

4. 小结

在Go语言的开发过程中,包的构建是核心环节之一,而源文件的选择则是构建过程中一个复杂且关键的细节。本文深入探讨了Go编译器在执行go build命令时,如何根据文件名中的架构(ARCH)和操作系统(OS)标识,以及构建约束(build constraints),来选择需要编译的源文件。

通过具体示例,本文展示了不同文件名和构建约束如何影响最终的编译结果,并揭示了Go编译器处理这些信息的优先级。理解这些内部机制不仅能帮助开发者优化构建过程,还能有效避免潜在的错误。希望本文的分析能够给大家带去帮助。

注:限于篇幅,本文仅针对包编译文件选择最复杂的部分进行的探索,而像ReleaseTags(比如: go1.21等)、cgo、_test.go后缀等比较明显的约束并未涉及,同时对于新版build constraints的运算符组合也未提及,感兴趣的童鞋可以参考go build constraints官方文档查阅。

本文涉及的源码可以在这里下载。

5. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2024, bigwhite. 版权所有.

走向合规:Go加密库对FIPS 140的支持

2024-11-16 07:14:30

本文永久链接 – https://tonybai.com/2024/11/16/go-crypto-and-fips-140

在今年3月份,Microsoft Azure团队宣布开设Go开发人员博客,旨在向开发者通报Microsoft在Go领域的最新动态,包括如何在Azure上部署Go工作负载以及与Go编程相关的文章。

然而,经过一段时间的关注,我发现该博客上的大多数文章都呈现出类似下图中的标题格式:

似乎微软在紧跟Go的发布节奏,发布自己维护的fork版本。那么,这些fork版本与上游Go究竟有何不同呢?通过查阅其fork版的README文件,我们可以找到答案:

原来微软的Go分支主要是为了向开发者提供符合FIPS 140-2标准Go加密库

近期,Russ Cox也发起了一个新提案,旨在使Go的加密库符合FIPS 140标准,以便能够去除Boring Crypto库。

对于许多对加密领域不太熟悉的读者来说,这可能会引发一系列疑问:什么是FIPS 140标准?Go目前对FIPS 140标准的支持状态如何?新提案将如何影响Go未来对FIPS 140标准的支持?

在这篇文章中,我们就一起了解一下FIPS 140标准、Go对其支持的现状以及未来的支持策略。

1. 什么是FIPS 140标准认证

FIPS 140(联邦信息处理标准第140号)是美国政府制定的一套计算机安全标准,主要用于规定加密模块的要求。该标准由美国国家标准与技术研究院(NIST)发布,旨在确保用于加密的硬件和软件模块满足一定的安全标准。

FIPS 140标准经历了多个版本的演进:

  • FIPS 140-1

于1994年发布,2002年撤回。它首次定义了四个安全级别和十一项要求领域。

  • FIPS 140-2

于2001年发布,考虑了技术的发展和用户反馈,是国际标准ISO/IEC 19790:2006的基础文件。FIPS 140-2仍然在使用,直到2022年4月某些应用程序的测试可以继续进行。FIPS 140-2定义了四个安全级别:

级别1:最低要求,所有组件必须是“生产级”的,且不允许有明显的安全漏洞。
级别2:增加了物理防篡改的要求,要求有角色基础的身份验证。
级别3:要求更高的物理防篡改能力和基于身份的认证,同时要求对模块的关键安全参数接口进行物理或逻辑隔离。
级别4:对物理安全要求更严格,要求能够抵御环境攻击。
  • FIPS 140-3

在2019年发布,作为FIPS 140-2的继任者,FIPS 140-3对标准进行了更新,使其与国际标准更为一致,并引入了新的安全要求。

FIPS 140认证由加密模块验证计划(CMVP)负责,该计划是NIST与加拿大通信安全局(CSE)共同运营的。认证过程涉及对加密模块的详细测试,确保其符合相应的标准要求。所有使用加密的美国联邦政府部门都必须使用经过FIPS 140认证的模块。

FIPS 140并不保证使用该标准的模块一定是安全的,但它确立了一系列文档和测试要求,确保加密模块在设计和实现上的可靠性。对于希望使用加密模块的用户和开发者来说,确认所使用的模块是否有现有的验证证书是非常重要的。

FIPS 140是美国政府对加密模块的要求,许多公司需要遵守这些标准以满足合规性需求,尤其是一些企业在与美国政府及其他受监管行业的合作中,FIPS合规性变得至关重要,这也是微软为何要建立Go Fork分支满足FIPS合规性,以及Go团队发起尽快让Go加密库满足FIPS合规性的提案的根本原因。随着Go在受监管环境中的采用增加,FIPS合规性将影响Go的吸引力和开发者体验。

那么当前Go密码学包对FIPS的支持是怎样的呢?我们继续往下看。

2. Go密码学包对FIPS标准支持的现状

到目前为止(Go 1.23.x版本),Go原生的crypto包并不具备FIPS认证。并且,在2017年的一个名为”crypto: FIPS 140-2 Certification“的issue中,Go密码学领域的首任技术负责人Adam Langley给出了这样的答复:

从Adam Langley的表述中可以看出,他似乎对FIPS 140这样的官方标准持有一种不屑的态度。同时他也指出对于FIPS 140认证感兴趣的开发者,可以尝试使用dev.boringcrypto分支

dev.boringcrypto分支是什么呢?它又是如何实现FPIS 140合规的呢?其实思路很简单,那就是:既然我暂时是不合规的,那我就找一个合规的,然后包装一下提供给开发者使用

那么什么库是合规的呢?BoringSSL,也就是Google fork的OpenSSL库的自维护版本,更精确地一点是BoringSSL库的一部分内容通过了FIPS 140-2认证


截图来自Google Cloud blog

而通过认证的这部分模块被称为BoringCrypto


截图来自NIST官网

Go dev.boringcrypto分支就是通过BoringSSL的binding来实现FIPS 140合规的,通过导入crypto/tls/fipsonly包,可以将所有TLS配置限制为FIPS合规设置,确保使用合规的加密算法。下面是Go 1.23.0中crypto/tls/fipsonly下fipsonly.go的源码,我们可以看到它实际上使用的是crypto/internal/boring下面的合规包:

// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build boringcrypto

// Package fipsonly restricts all TLS configuration to FIPS-approved settings.
//
// The effect is triggered by importing the package anywhere in a program, as in:
//
//  import _ "crypto/tls/fipsonly"
//
// This package only exists when using Go compiled with GOEXPERIMENT=boringcrypto.
package fipsonly

// This functionality is provided as a side effect of an import to make
// it trivial to add to an existing program. It requires only a single line
// added to an existing source file, or it can be done by adding a whole
// new source file and not modifying any existing source files.

import (
    "crypto/internal/boring/fipstls"
    "crypto/internal/boring/sig"
)

func init() {
    fipstls.Force()
    sig.FIPSOnly()
}

Go 1.8开始,Go都会在发布大版本时建立对应该大版本的dev.boriingcrypto分支,如下图:

但Go官方在Go 1.18版本之后似乎就不维护这个分支了。微软也恰是从那时(2022年初)开始fork了Go repo并维护自己的fips合规版本,在Linux上,该Fork使用OpenSSL的Go binding进行加密,而在Windows上使用CNG(Cryptography Next Generation)。

注:尽管使用微软的Go工具链构建的应用程序可以在FIPS兼容模式下运行,但这并不意味着自动符合FIPS认证。开发团队需确保使用FIPS合规的加密原语及工作流程。如果无法提供FIPS合规的实现,修改后的加密运行时将回退到Go标准库的加密方法,如使用crypto/md5或非标准nonce大小的AES-GCM等。

由于BoringSSL是C语言的,Go dev.boringcrypto分支势必要依赖cgo,将部分加密包的内部实现替换为通过FIPS 140认证的模块。但这种方案存在许多问题,如内存不安全代码、影响Go版本更新、性能问题和开发体验等。于是就有了文前提到的旨在移除BoringCrypto的Go团队的新提案。

新提案的内容是什么呢?下面我们就来细致看看。

3. Go加密库原生支持FIPS 140认证的提案

根据Go加密库上一任Tech leader Filippo Valsorda在proposal: crypto: mechanism to enable FIPS mode中的描述,Go团队希望为Go加密库实现FIPS 140-3认证,并允许开发者启用或禁用FIPS模式,以满足合规性要求。

该proposal建议在运行时通过设置GODEBUG标志来启用FIPS模式,新增GODEBUG=fips140选项。并且通过GODEBUG=fips140的值可以控制FIPS模式:

  • on为启用FIPS模式。
  • only:仅允许使用经过批准的加密算法,其他非批准算法将返回错误。
  • enforce(该值依然在讨论中):它会强制执行使用FIPS合规算法,非批准算法将返回错误或导致程序崩溃。

在代码层面,新增crypto/fips140包或放在crypto/internal/fips下,其中包含Enabled() bool函数,用于运行时检查当前是否启用了FIPS模式。

Russ Cox之后在”proposal: cmd/go: add fips140 module selection mechanism“的issue中着重阐述了在Go module层面对fips 140的支持策略,目前仍在更新中,根据Russ Cox 2024.11.11的最新comment,当前设计的策略大致如下:

  • 引入新的目标配置环境变量GOFIPS140用于构建工具链(替代之前在propsal中考虑新增的-fips140命令行标志)

该环境变量可取值为off、latest、inprocess、certified或v1.X.Y-fips.N,默认值为off。使用go version -m或获取debug.BuildInfo时,可显示新的构建设置GOFIPS140,其值为off、devel或具体的版本号。

- off(默认):使用最新源代码,GODEBUG设置为fips140=off。
- latest:使用最新源代码,GODEBUG设置为fips140=on。
- v1.X.Y-fips.N:使用指定的快照。
- inprocess:使用正在进行FIPS标准认证的版本。
- certified:使用经过NIST的FIPS认证的版本。
  • 不将FIPS代码视为module

用户不可见的API或工具将不会将crypto/internal/fips代码视为module。在运行go list时,crypto/internal/fips/…的包可以来自\$GOROOT/src/crypto/internal/fips/…或module cache中的目录,Module字段将为nil,与标准库其他部分一致。

  • 版本管理与模块系统的分离

尽管crypto/internal/fips有语义版本控制的版本集合,但它们与Go模块系统完全分离。存在于lib/fips140中的文件将采用实现定义(implementation-defined)的格式,尽管其格式很可能采用module zip和checksum的形式。

以上策略的实施将增强Go在FIPS 140支持方面的灵活性和可控性,为开发者提供了更清晰的配置选项。通过将FIPS 支持的配置独立于模块系统,开发者可以更方便地管理构建环境,避免潜在的配置冲突。

并且按照Russ Cox的说法,Go团队计划每年进行一次Go加密库的重新验证,以保持模块的合规性和及时更新。

4. 小结

本文探讨了Go语言加密库在FIPS 140标准支持方面的现状及未来发展。FIPS 140是美国政府制定的一套加密模块安全标准,目前的版本为FIPS 140-3,涵盖了最新的安全要求。

我们详细分析了Go加密库的现状,包括通过BoringSSL实现FIPS 140合规的dev.boringcrypto分支,以及微软维护的FIPS 140合规Go Fork版本。此外,Go团队还提出了针对FIPS 140-3认证的新提案,旨在允许开发者在运行时灵活启用或禁用FIPS模式。这一新提案的实施将为Go提供更大的合规灵活性,并为开发者提供清晰的配置选项,从而增强Go在受监管环境中的吸引力和实用性。

目前,关于Go加密库对FIPS 140支持的相关事项仍处于提案阶段,具体思路和实现细节可能会随着进一步的发展而变化。然而,本文通过对FIPS认证的介绍以及Go加密库未来计划的阐述,相信读者已经初步掌握了选择Go加密模块和满足合规性需求的有用信息。

如果你有什么疑问,欢迎在评论区留言,通过讨论碰撞,我们一起进步和成长!

5. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2024, bigwhite. 版权所有.

Gotip安装:基于Go镜像代码仓库

2024-11-15 06:34:15

本文永久链接 – https://tonybai.com/2024/11/15/install-gotip-using-go-repo-mirror

在《Go map使用Swiss Table重新实现,性能最高提升近50%》一文中,我曾使用过Gotip版本对基于Swiss table的新map实现做过benchmark测试。

有过几年Go开发经验的Gopher都知道Gotip版本是啥,但一些初学者可能并不十分清楚。Gotip版本可以理解为Go语言的devel版本,是支持开发者全面体验Go最新特性的主流方法之一,而另外一种方法则是通过Go官网提供的在线Playground(选择Go dev branch,如下图)体验:

不过通过Go playground方法体验Go最新特性会受到各种限制,比如只能体验单一源文件、无法跑benchmark test等。

Gotip本质上就是基于Go repo最新主干代码进行构建的Go版本,为了降低Gopher体验Go最新特性的门槛,Go团队让大家可以通过go install来安装Gotip。如今我们只需两行命令(前提是你的机器上已经有了某个版本的Go)就可以将Gotip安装到自己的机器上:

$go install golang.org/dl/gotip@latest
$gotip download

然而,Gotip版本的本质决定了它在国内的安装过程不会一帆风顺。你在国内执行上述的第二条命令时,很可能会看到如下输出:

$ gotip download
正克隆到 '/root/sdk/gotip'...
fatal: 无法访问 'https://go.googlesource.com/go/':Failed connect to go.googlesource.com:443; 连接超时
gotip: failed to clone git repository: exit status 128

这表明gotip尝试从Google的Go代码仓库克隆代码到本地,但由于众所周知的原因,这一过程常常会失败。

如果屏幕前的你拥有高速的加速器,那么你现在就可以关闭窗口,无需再阅读下面的内容了。但如果你没有,或者你需要在没有加速器的服务器或PC上使用Gotip,那还是请继续读下去。

现在问题就摆在你我面前:如何能让Gotip能成功clone到Go源码呢?一个很容易想到的思路:让Gotip从其他可达的地方clone Go源码不就行了吗

假设这个思路可行,需要满足以下两个条件:

  • Gotip支持从其他地方clone Go源码
  • 国内有一个可达的、快速的Go源码mirror仓库

我们评估一下可行性,先来看第一个条件。Gotip支持传入某些命令行参数并从其他地方clone Go源码么?看看它的usage吧!

$gotip
gotip: not downloaded. Run 'gotip download' to install to /root/sdk/gotip

$gotip -h
gotip: not downloaded. Run 'gotip download' to install to /root/sdk/gotip

$gotip download 2 3 4
gotip: usage: gotip download [CL number | branch name]

我们看到:官方版gotip的usage隐藏“很深”啊(有改进空间哦)!并且,gotip并不支持传入任何mirror仓库的命令行标志或参数。不过好在gotip是开源的,在github.com/golang/dl下可以找到gotip的源码,我们只需要fork并修改一下应该就可以了。

那么第二个条件呢?国内是否有一个可达的、快速的Go源码mirror仓库呢?很遗憾,没有现成的。不过,我们可以手工从github.com/golang/go上下载仓库,然后再push到国内任一家代码托管站点上即可,虽然这么做有些费时费力。好在,国内的码云(gitee.com)提供了一个导入外部仓库并同步的功能,我们可以在码云上直接导入github.com/golang/go,比如我这里就建立了一个公共库并同步了golang/go:gitee.com/bigwhite/go:

综上这个方案是可行的。

接下来就是将上面的方案思路付诸实现了。我fork了github.com/golang/dl到github.com/bigwhite/dl,然后修改了其中的internal/version/gotip.go文件:将https://go.googlesource.com/go改为了https://gitee.com/bigwhite/go.git。

接下来,我们就可以通过下面命令构建一个自己定制的gotip:

$go build -o gotip-gitee golang.org/dl/gotip

这里要注意的是:直接go build golang.org/dl/gotip会报错,因为在顶层目录下存在了gotip这个子目录,与目标可执行文件重名了,所以这里重命名了目标可执行文件。为了方便,我又在github.com/bigwhite/dl下加了一个Makefile,大家只需执行make gotip即可。

注:这是一个很好的向Go项目贡献自己代码的机会,大家可以向Go项目提交PR,为gotip增加类似-m (mirror site)的命令行参数,以支持从第三方Go repo镜像站点下载Go源码并完成gotip的构建和安装过程。

接下来我们就来继续gotip的安装过程:

$ ./gotip-gitee download
正克隆到 '/root/sdk/gotip'...
remote: Enumerating objects: 14793, done.
remote: Counting objects: 100% (14793/14793), done.
remote: Compressing objects: 100% (11974/11974), done.
remote: Total 14793 (delta 2629), reused 10541 (delta 2221), pack-reused 0
接收对象中: 100% (14793/14793), 29.30 MiB | 9.50 MiB/s, 完成.
处理 delta 中: 100% (2629/2629), 完成.
Updating the go development tree...
来自 https://gitee.com/bigwhite/go
 * branch            master     -> FETCH_HEAD
HEAD 目前位于 84e58c8 cmd/internal/obj: add tool to generate Cnames string
Building Go cmd/dist using /root/.bin/go1.23.0. (go1.23.0 linux/amd64)
Building Go toolchain1 using /root/.bin/go1.23.0.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for linux/amd64.
---
Installed Go for linux/amd64 in /root/sdk/gotip
Installed commands in /root/sdk/gotip/bin
Success. You may now run 'gotip'!

这个编译和安装过程大概仅花费2-3分钟左右,非常快!一旦gotip安装完毕,你就可以直接使用gotip版本,体验Go最新特性了!

$ gotip version
go version devel go1.24-84e58c8 Wed Nov 13 05:02:13 2024 +0000 linux/amd64

我们来小结一下!在这篇文章中,我提供了一种在国内安装gotip版本的方法,供大家参考而已。如果你不喜欢使用gitee.com上的mirror仓库,你也可以直接使用github上的go镜像仓库,如果你觉得访问github还比较顺畅的话。

当然屏幕前的读者可能有比我这里更好、更方便地在国内安装gotip版本的方法,也欢迎大家在评论区留言交流!

注:如果你采用我的方法安装gotip,请自行在gitee.com上建立Go仓库的mirror仓库并按需同步。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2024, bigwhite. 版权所有.

Go map使用Swiss Table重新实现,性能最高提升近50%

2024-11-14 06:53:32

本文永久链接 – https://tonybai.com/2024/11/14/go-map-use-swiss-table

2024月11月5日的Go compiler and runtime meeting notes中,我们注意到了一段重要内容,如下图红框所示:

这表明,来自字节的一位工程师在两年多前提出的“使用Swiss table重新实现Go map”的建议即将落地,目前该issue已经被纳入Go 1.24里程碑

Swiss Table是由Google工程师于2017年开发的一种高效哈希表实现,旨在优化内存使用和提升性能,解决Google内部代码库中广泛使用的std::unordered_map所面临的性能问题。Google工程师Matt Kulukundis在2017年CppCon大会上详细介绍了他们在Swiss Table上的工作

目前,Swiss Table已被应用于多种编程语言,包括C++ Abseil库的flat_hash_map(可替换std::unordered_map)Rust标准库Hashmap的默认实现等。

Swiss Table的出色表现是字节工程师提出这一问题的直接原因。字节跳动作为国内使用Go语言较为广泛的大厂。据issue描述,Go map的CPU消耗约占服务总体开销的4%。其中,map的插入(mapassign)和访问(mapaccess)操作的CPU消耗几乎是1:1。大家可千万不能小看4%这个数字,以字节、Google这样大厂的体量,减少1%也意味着真金白银的大幅节省。

Swiss Table被视为解决这一问题的潜在方案。字节工程师初版实现的基准测试结果显示,与原实现相比,Swiss Table在查询、插入和删除操作上均提升了20%至50%的性能,尤其是在处理大hashmap时表现尤为突出;迭代性能提升了10%;内存使用减少了0%至25%,并且不再消耗额外内存。

这些显著的性能提升引起了Go编译器和运行时团队的关注,特别是当时负责该子团队的Austin Clements。在经过两年多的实验和评估后,Go团队成员Michael Pratt基于Swiss Table实现的internal/runtime/maps最终成为Go map的底层默认实现。

在本文中,我们将简单介绍Swiss Table这一高效的哈希表实现算法,并提前看一下Go map的Swiss Table实现。

在进入swiss table工作原理介绍之前,我们先来回顾一下当前Go map的实现(Go 1.23.x)。

1. Go map的当前实现

map,也称为映射,或字典,或哈希表,是和数组等一样的最常见的数据结构。实现map有两个关键的考量,一个是哈希函数(hash function),另一个就是碰撞处理(collision handling)。hash函数是数学家的事情,这里不表。对于碰撞处理,在大学数据结构课程中,老师通常会介绍两种常见的处理方案:

  • 开放寻址法(Open Addressing)

在发生哈希碰撞时,尝试在哈希表中寻找下一个可用的位置,如下图所示k3与k1的哈希值发生碰撞后,算法会尝试从k1的位置开始向后找到一个空闲的位置:

  • 链式哈希法(拉链法, Chaining)

每个哈希桶(bucket)存储一个链表(或其他数据结构),所有哈希值相同的元素(比如k1和k3)都被存储在该链表中。

Go当前的map实现采用的就是链式哈希,当然是经过优化过的了。要了解Go map的实现,关键把握住下面几点:

  • 编译器重写

我们在用户层代码中使用的map操作都会被Go编译器重写为对应的runtime的map操作,就如下面Go团队成员Keith Randall在GopherCon大会上讲解map实现原理的一个截图所示:

  • 链式哈希

前面提过,Go map当前采用的是链式哈希的实现,一个map在内存中的结构大致如下:


来自Keith Randall的ppt截图

我们看到,一个map Header代表了一个map类型的实例,map header中存储了有关map的元数据(图中字段与当前实现可能有少许差异,毕竟那是几年前的一个片子了),如:

- len: 当前map中键值对的数量。
- bucket array: 存储数据的bucket数组,可以对比前面的链式哈希的原理图进行理解,不过不同的是,Go map中每个bucket本身就可以存储多个键值对,而不是指向一个键值对的链表。
- hash seed: 用于哈希计算的种子,用于分散数据并提高安全性。

通常一个bucket可以存储8个键值对,这些键值对是根据键的哈希值分配到对应的bucket中。

注:在《Go语言第一课》专栏中,有关于Go map工作原理的系统说明,感兴趣的童鞋可以看看。

  • 溢出桶(overflow bucket)

每个bucket后面还会有Overflow Bucket。当一个bucket中的数据超出容量时,会创建overflow bucket来存储多余的数据。这样可以避免直接扩展bucket数组,节省内存空间。但如果出现过多的overflow bucket,性能就会下降。

  • “蚂蚁搬家”式的扩容

当map中出现过多overflow bucket而导致性能下降时,我们就要考虑map bucket扩容的事儿了,以始终保证map的操作性能在一个合理的范围。是否扩容由一个名为load factor的参数所控制。load factor是元素数量与bucket数量的比值,比值越高,map的读写性能越差。目前Go map采用了一个经验值来确定是否要扩容,即load factor = 6.5。当load factor超过这个值时,就会触发扩容。所谓扩容就是增大bucket数量(当前实现为增大一倍数量),减少碰撞,让每个bucket中存放的element数量降下来。

扩容需要对存量element做rehash,在元素数量较多的情况下,“一次性”的完成桶的扩容会造成map操作延迟“突增”,无法满足一些业务场景的要求,因此Go map采用“增量”扩容的方式,即在访问和插入数据时,“蚂蚁搬家”式的做点搬移元素的操作,直到所有元素完成搬移。

Go map的当前实现应该可以适合大多数的场合,但依然有一些性能和延迟敏感的业务场景觉得Go map不够快,另外一个常被诟病的就是当前实现的桶扩容后就不再缩容(shrink)了,这会给内存带来压力。


来自issue 20135的截图

下面我们再来看看swiss table的结构和工作原理。

2. Swiss table的工作原理

就像前面提到的,Swiss table并非来自某个大学或研究机构的论文,而是来自Google工程师在工程领域的”最佳实践”,因此关于Swiss table的主要资料都来自Google的开源C++ library Abseil以及开发者的演讲视频。在Abseil库中,它是flat_hash_map、flat_hash_set、node_hash_map以及node_hash_set等数据结构的底层实现,并且Swiss table的实现在2018年9月正式开源

和Go map当前实现不同,Swiss table使用的不是拉链法,而是开放寻址,但并非传统的方案。下面是根据公开资源画出的一个Swiss table的逻辑结构图(注意:并非真实内存布局):

如果用一个式子来表示Swiss table,我们可以用:

A swiss table = N * (metdata array + slots array)

我们看到:swiss table将所谓的桶(这里叫slot)分为多个group,每个group中有16个slot,这也是swiss table的创新,即将开放寻址方法中的probing(探测key碰撞后下一个可用的位置(slot))放到一个16个slot的group中进行,这样的好处是可以通过一个SIMD指令并行探测16个slot,这种方法也被称为Group Probing

在上图中,我们看到一个Group由metadata和16个slot组成。metadata中存储的是元数据,而slot中存储的是元素(key和value)。Group probling主要是基于metadata实现的,Google工程师的演讲有对group probing实现的细节描述。

当我们向swiss table插入一个元素或是查找一个元素时,swiss table会通过hash函数对key进行求值,结果是一个8字节(64bit)的数。和Go map的当前实现一样,这个哈希值的不同bit功用不同,下图是一个来自abseil官网的示例:

哈希值的高57bit被称为H1,低7bit被称为H2。前者用于标识该元素在Group内的索引,查找和插入时都需要它。后者将被用于该元素的元数据,放在metadata中存储,用于快速的group probing之用,也被称为哈希指纹

每个Group的metadata也是一个16字节数组,每个字节对应一个slot,是该slot的控制字节。这个字节的8个bit位的组成如下:


图来自abseil库官网

metadata中的控制字节有三个状态:

  • 最高位为1,其余全零为空闲状态(Empty),即对应的slot尚未曾被任何element占据过;
  • 最高位为0,后7位为哈希指纹(H2),为对应的slot当前已经有element占据的已使用状态
  • 最高位为1,其他位为1111110的,为对应的slot为已删除状态,后续可以被继续使用。

下面是Abseil开发者演进slide中的一个针对swiss table的迭代逻辑:

通过这幅图可以看出H1的作用。不过这里通过pos = pos + 1进行probing(探测)显然是不高效的!metadata之所以设计为如此,并保存了插入元素的哈希指纹就是为了实现高效的probing,下图演示了基于key的hash值的H2指纹通过SIMD指令从16个位置中快速得到匹配的pos的过程:

虽然有两个匹配项,但这个过程就像“布隆过滤器”一样,快速排除了不可能的匹配项,减少了不必要的内存访问。

由此也可以看到:swiss table的16个条目的分组大小不是随意选择的,而是基于SSE2寄存器长度(128bit, 16bytes)和现代CPU的缓存行大小(64字节)优化的,保证了一个Group的控制字节能被单次SIMD指令处理。

此外swiss table也是通过load factor来判定是否需要对哈希表进行扩容,一旦扩容,swiss table通常是会将group数量增加一倍,然后重新计算当前所有元素在新groups中的新位置(rehash),这个过程是有一定开销的。如果不做优化,当表中元素数量较多时,这个过程会导致操作延迟增加。

最后,虽然多数情况是在group内做probing,但当元素插入时,如果当前Group已满,就必须探测到下一个Group,并将元素插入到下一个Group。这样,在该元素的查找操作中,probing也会跨group进行。

到这里,我们已经粗略了解了swiss table的工作原理,那么Go tip对swiss table当前的实现又是怎样的呢?我们下面就来看看。

3. Go tip版本当前的实现

Go tip版本基于swiss table的实现在https://github.com/golang/go/blob/master/src/internal/runtime/maps下。

由于Go map是原生类型,且有了第一版实现,考虑到Go1兼容性,新版基于swiss table的实现也要继承已有的语义约束。同时,也要尽量避免swiss table自身的短板,Go团队在swiss table之上做了局部改进。比如为了将扩容带来的开销降到最低,Go引入了多table的设计,以支持渐进式扩容。也就是说一个map实际上是多个swiss table,而不是像上面说的一个map就是一个swiss table。每个table拥有自己的load factor,可以独立扩容(table的扩容是一次性扩容),这样就可以将扩容的开销从全部数据变为局部少量数据,减少扩容带来的影响

Go swiss-table based map的逻辑结构大致如下:

我们可以看出与C++ swisstable的最直观不同之处除了有多个table外,每个group包含8个slot和一个control word,而不是16个slot。此外,Go使用了二次探测(quadratic probing), 探测序列必须以空slot结束。

为了实现渐进式扩容,数据分散在多个table中;单个table容量有上限(maxTableCapacity),超过上限时分裂成两个table;使用可扩展哈希(extendible hashing)根据hash高位选择table,且每个table可以独立增长。

Go使用Directory管理多个table,Directory是Table的数组,大小为2^globalDepth。如果globalDepth=2,那Directory最多有4个表,分为0×00、0×01、0×10、0×11。Go通过key的hash值的前globalDepth个bit来选择table。这是一种“extendible hashing”,这是一种动态哈希技术,其核心特点是通过动态调整使用的哈希位数(比如上面提到的globalDepth)来实现渐进式扩容。比如:初始可能只用1位哈希值来区分,需要时可以扩展到用2位,再需要时可以扩展到用3位,以此类推。

举个例子,假设我们用二进制表示哈希值的高位,来看一个渐进式扩容的过程:

  • 初始状态 (Global Depth = 1):
directory
hash前缀  指向的table
0*** --> table1 (Local Depth = 1)
1*** --> table2 (Local Depth = 1)
  • 当table1满了需要分裂时,增加一位哈希值 (Global Depth = 2):
directory
hash前缀  指向的table
00** --> table3 (Local Depth = 2)  // 由table1扩容而成
01** --> table4 (Local Depth = 2)  // 由table1扩容而成
10** --> table2 (Local Depth = 1)
11** --> table2 (Local Depth = 1)  // 复用table2因为它的Local Depth还是1
  • 如果table2也满了,需要分裂:
directory
hash前缀  指向的table
00** --> table3 (Local Depth = 2)
01** --> table4 (Local Depth = 2)
10** --> table5 (Local Depth = 2) // 由table2扩容而成
11** --> table6 (Local Depth = 2) // 由table2扩容而成

通过extendible hashing实现的渐进式扩容,每次只处理一部分数据,扩容过程对其他操作影响小,空间利用更灵活。

对于新版go map实现而言,单个Table达到负载因子阈值时触发Table扩容。当需要分裂的Table的localDepth等于map的globalDepth时触发Directory扩容,这就好理解了。

除此之外,Go版本对small map也有特定优化,比如少量元素(<=8)时直接使用单个group,避免或尽量降低swiss table天生在少量元素情况下的性能回退问题。

更多实现细节,大家可以自行阅读https://github.com/golang/go/blob/master/src/internal/runtime/maps/下的Go源码进行理解。

注:目前swiss table版的go map依然还未最终定型,并且后续还会有各种优化加入,这里只是对当前的实现(2024.11.10)做概略介绍,不代表以后的map实现与上述思路完全一致。

4. Benchmark

目前gotip版本中GOEXPERIMENT=swissmap默认已经打开,我们直接用gotip版本即可体验基于swiss table实现的map。

字节工程师zhangyunhao的gomapbench repo提供了对map的性能基准测试代码,不过这个基准测试太多,我大幅简化了一下,只使用Int64,并只测试了元素个数分别为12、256和8192时的情况。

注:我基于Centos 7.9,使用Go 1.23.0和gotip(devel go1.24-84e58c8 linux/amd64)跑的benchmark。

// 在experiments/swiss-table-map/mapbenchmark目录下
$go test -run='^$' -timeout=10h -bench=. -count=10 > origin-map.txt
$GOEXPERIMENT=swissmap gotip test -run='^$' -timeout=10h -bench=. -count=10 > swiss-table-map.txt
$benchstat origin-map.txt swiss-table-map.txt > result.txt

注:gotip版本的安装请参考《Go语言第一课》专栏的第3讲。benchstat安装命令为go install golang.org/x/perf/cmd/benchstat@latest

下面是result.txt中的结果:

goos: linux
goarch: amd64
pkg: demo
cpu: Intel(R) Xeon(R) Platinum
                                  │ origin-map.txt │         swiss-table-map.txt          │
                                  │     sec/op     │    sec/op     vs base                │
MapIter/Int/12-8                      179.7n ± 10%   190.6n ±  4%        ~ (p=0.436 n=10)
MapIter/Int/256-8                     4.328µ ±  5%   3.748µ ±  1%  -13.40% (p=0.000 n=10)
MapIter/Int/8192-8                    137.3µ ±  1%   123.6µ ±  1%   -9.95% (p=0.000 n=10)
MapAccessHit/Int64/12-8               10.12n ±  2%   10.68n ± 14%   +5.64% (p=0.000 n=10)
MapAccessHit/Int64/256-8              10.29n ±  3%   11.29n ±  1%   +9.77% (p=0.000 n=10)
MapAccessHit/Int64/8192-8             25.99n ±  1%   14.93n ±  1%  -42.57% (p=0.000 n=10)
MapAccessMiss/Int64/12-8              12.39n ± 88%   20.99n ± 50%        ~ (p=0.669 n=10)
MapAccessMiss/Int64/256-8             13.12n ±  6%   11.34n ±  7%  -13.56% (p=0.000 n=10)
MapAccessMiss/Int64/8192-8            15.71n ±  1%   14.03n ±  1%  -10.66% (p=0.000 n=10)
MapAssignGrow/Int64/12-8              607.1n ±  2%   622.6n ±  2%   +2.54% (p=0.000 n=10)
MapAssignGrow/Int64/256-8             25.98µ ±  3%   23.22µ ±  1%  -10.64% (p=0.000 n=10)
MapAssignGrow/Int64/8192-8            792.3µ ±  1%   844.1µ ±  1%   +6.54% (p=0.000 n=10)
MapAssignPreAllocate/Int64/12-8       450.2n ±  2%   409.2n ±  1%   -9.11% (p=0.000 n=10)
MapAssignPreAllocate/Int64/256-8     10.412µ ±  1%   6.055µ ±  2%  -41.84% (p=0.000 n=10)
MapAssignPreAllocate/Int64/8192-8     342.4µ ±  1%   232.6µ ±  2%  -32.05% (p=0.000 n=10)
MapAssignReuse/Int64/12-8             374.2n ±  1%   235.4n ±  2%  -37.07% (p=0.000 n=10)
MapAssignReuse/Int64/256-8            8.737µ ±  1%   4.716µ ±  4%  -46.03% (p=0.000 n=10)
MapAssignReuse/Int64/8192-8           296.4µ ±  1%   181.0µ ±  1%  -38.93% (p=0.000 n=10)
geomean                               1.159µ         984.2n        -15.11%

我们看到了除了少数测试项有不足外(比如MapAssignGrow以及一些元素数量少的情况下),大多数测试项中,新版基于swiss table的map的性能都有大幅提升,有些甚至接近50%!

5. 小结

本文探讨了Go语言中的map实现的重塑,即引入Swiss Table这一高效哈希表结构的背景与优势。Swiss Table由Google工程师开发,旨在优化内存使用和提升性能,解决了传统哈希表在高负载情况下的性能瓶颈。通过对比现有的链式哈希实现,Swiss Table展示了在查询、插入和删除操作上显著提高的性能,尤其是在处理大规模数据时。

经过两年多的实验与评估,Go团队决定将Swiss Table作为Go map的底层实现,预计将在Go 1.24中正式落地。新的实现不仅承继了原有的语义约束,还通过引入多表和渐进式扩容的设计,进一步优化了扩容过程的性能。尽管当前实现仍在完善中,但Swiss Table的引入无疑为Go语言的性能提升提供了新的可能性,并为未来进一步优化奠定了基础。

对于那些因Go引入自定义iterator而批评Go团队的Gopher来说,这个Go map的重塑无疑会很对他们的胃口。

本文涉及的源码可以在这里下载。

6. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2024, bigwhite. 版权所有.

Go,15岁了[译]

2024-11-12 18:49:48

本文永久链接 – https://tonybai.com/2024/11/12/go-turns-15

虽然迟到了,但绝不缺席!新任Go技术负责人Austin Clements在Go语言15岁生日后的第二天,在Go官方博客上发表了庆祝文章“Go Turns 15”。在这篇文章中,Austin回顾了过去一年Go项目和社区的变化,以及Go团队的努力工作,并对Go的未来发展进行了展望。我在此对这篇庆生文进行了翻译,供大家参考。


Go,生日快乐!

周日,我们庆祝了Go开源15周年

自从Go诞生10周年以来,无论是Go语言本身还是整个世界都经历了巨大的变化。尽管如此,有些方面依然保持不变:Go始终致力于稳定性、安全性,以及支持软件工程和大规模生产

Go语言发展势头强劲!在过去五年中,Go的用户群增加了三倍多(译注:不知道这个数据从何而来),成为增长最快的编程语言之一。自十五年前诞生以来,Go已成为十大编程语言之一,并成为现代云计算的主要语言


来自TIOBE 2024年11月排行榜(译者配图)


来自Github Octoverse 2024(译者配图)

随着Go 1.22版本在二月份发布和Go 1.23版本在八月份发布,这一年可被称为“for循环之年”。Go 1.22将for循环中引入变量的作用域改为每次迭代,而非整个循环,从而解决了一个长期存在的语言“陷阱”。十多年前,在Go 1发布之前,Go团队对几个语言细节做出了决策,其中就包括for循环是否应该在每次迭代中创建一个新的循环变量。有趣的是,这次讨论非常简短且没有明确的意见。Rob Pike以他一贯的风格结束了讨论,只说了一个字:“stet”(保持原样)。结果也确实如此。尽管当时看似微不足道,但多年的生产经验突显了这一决策的影响。然而,在此期间,我们还构建了强大的工具来理解对Go的变更影响,特别是在整个Google代码库中进行生态系统范围的分析和测试,并建立了与社区合作和获取反馈的流程。在经过广泛的测试、分析和社区讨论后,我们推出了这一变更,并配备了哈希二分工具,以帮助开发者在大规模代码中精确定位受影响的部分。

对for循环的变更仅是是五年演进调整的一部分。这一变更的实现得益于Go 1.21中引入的向前兼容性,而这又建立在四年半前Go 1.14发布的Go模块基础之上。

译注:Go module首次在Go 1.11版本由Russ Cox设计和实现,Go 1.14版本首次宣布Go module具备生产使用的成熟度了。

Go 1.23在此变更的基础上进一步引入了迭代器和用户定义的for-range循环。结合仅仅两年半前在Go 1.18中引入的泛型!——这为自定义集合和许多其他编程模式奠定了强大而人性化的基础。

这些版本还带来了许多生产就绪方面的改进,包括备受期待的标准库HTTP路由器增强执行跟踪的全面重构,以及为所有Go应用程序提供更强的随机性。此外,我们的第一个v2标准库包的引入为未来的标准库演进和现代化建立了模板。

在过去的一年中,我们还谨慎地推出了Go工具的自愿使用的遥测系统。该系统将为Go开发者提供数据,以便他们做出更好的决策,同时保持完全开放和匿名。Go遥测最初出现在gopls(Go语言服务器)中,已经带来了许多改进。这项努力为使Go编程体验变得更加出色奠定了基础。

展望未来,我们正在不断演进Go,以更好地利用当前和未来硬件的能力。在过去的15年中,硬件发生了巨大的变化。为了确保Go能够在接下来的15年中继续支持高性能、大规模的生产工作负载,我们需要适应大型多核处理器、先进的指令集,以及在non-uniform内存层次结构中日益重要的局部性。其中一些改进将是透明的。Go 1.24将推出全新底层实现的map,以提高在现代CPU上的执行效率。同时,我们正在进行新的垃圾回收算法的原型设计,以适应现代硬件的能力和限制。一些改进将以新的API和工具的形式出现,以便Go开发者更好地利用现代硬件。我们正在研究如何支持最新的向量和矩阵硬件指令,以及应用程序如何构建CPU和内存的局部性。指导我们努力的一个核心原则是可组合优化(composable optimization):优化对代码库的影响应该尽可能局部化,以确保对其余代码库开发的便捷性不受影响。

我们将继续确保Go的标准库在默认情况下是安全的,并在设计上也考虑到安全性。这包括不断努力将内置的、原生支持的FIPS认证加密功能纳入其中,使得需要FIPS加密的应用程序只需简单切换一个命令行标志即可使用。此外,我们还在不断改进Go的标准库包,并借鉴math/rand/v2的例子,考虑在哪里可以引入新的API,以显著提高编写安全和可靠的Go代码的便利性。

我们正在努力使Go在人工智能领域表现更好,同时也让人工智能更好地服务于Go,增强其在AI基础设施、应用程序和开发者辅助工具方面的能力。Go是一种非常适合构建生产系统的语言,我们希望它也能成为构建生产级AI系统的优秀语言。作为云基础设施的可靠语言,Go自然成为大型语言模型(LLM)基础设施的理想选择。针对AI应用,我们将继续在流行的AI SDK中为Go提供一流的支持,包括LangChainGoGenkit。从一开始,Go就旨在改善端到端的软件工程过程,因此我们自然希望引入AI的最新工具和技术,以减少开发者的重复劳动,从而留出更多时间来进行更有趣的编程活动!

感谢您!

所有这一切的实现都离不开Go的杰出贡献者和蓬勃发展的社区。十五年前,我们只能憧憬Go所取得的成功以及围绕Go发展起来的社区。感谢每一位参与其中的人,无论贡献大小。我们祝愿大家在新的一年里一切顺利!


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2024, bigwhite. 版权所有.