admin管理员组

文章数量:1437450

Go 1.16 相比 Go 1.15 有哪些值得注意的改动?

本系列旨在梳理 Go 的 release notes 与发展史,来更加深入地理解 Go 语言设计的思路。

/doc/go1.16

Go 1.16 在 Go 1.15 的基础上带来了不少重要的更新和改进。以下是一些值得关注的改动要点:

  1. 平台支持 (Ports) :新增对 macOS ARM64(Apple Silicon)的原生支持 (GOOS=darwin, GOARCH=arm64);原 darwin/arm64 (iOS) 重命名为 ios/arm64;新增 ios/amd64 以支持在 AMD64 macOS 上运行的 iOS 模拟器;Go 1.16 是支持 macOS 10.12 Sierra 的最后一个版本。
  2. 模块 (Modules)GO111MODULE 环境变量默认为 on,即默认启用模块感知模式;go buildgo test 默认不再修改 go.mod/go.sum 文件;go install 支持版本后缀,成为推荐的安装方式;新增 retract 指令用于撤回版本。
  3. go test :测试函数中调用 os.Exit(0) 现在会被视为测试失败,但 TestMain 中的调用仍视为成功;同时使用 -c-i 标志与无法识别的标志时会报错。
  4. vet 工具 :新增一项检查,用于警告在测试创建的 goroutine 中无效调用 testing.TFatalFatalfFailNowSkip 系列方法的情况。
  5. 工具链 (Toolchain) :编译器支持内联包含非标签 for 循环、方法值和类型选择 (type switch) 的函数;链接器性能得到提升(速度加快 20-25%,内存减少 5-15%),适用于所有支持的平台,并能生成更小的二进制文件;Windows 下 go build -buildmode=c-shared 默认启用 ASLR。
  6. 文件嵌入 (Embedded Files) :新增 embed 包和 //go:embed 指令,允许在编译时将静态文件或文件树嵌入到可执行文件中。
  7. 文件系统 (File Systems) :新增 io/fs 包和 fs.FS 接口,为只读文件树提供了统一的抽象;标准库多处已适配此接口;io/ioutil 包被弃用,其功能已迁移至 ioos 包。

下面是一些值得展开的讨论:

模块系统的重要改进和理念转变

Go 1.16 对模块系统进行了多项重要调整,标志着 Go 模块化开发的进一步成熟和规范化。核心变化在于 默认启用模块感知模式强化了依赖管理的确定性

GO111MODULE 环境变量的默认值从 auto 改为 on,这意味着无论当前目录或父目录是否存在 go.mod 文件,go 命令都会默认以模块感知模式运行。这一改变推动开发者全面拥抱 Modules,简化了环境配置。如果需要旧的行为,可以显式设置 GO111MODULE=auto

另一个关键变化是,go buildgo test 等构建命令 默认不再自动修改 go.modgo.sum 文件 。如果构建过程中发现需要添加或更新依赖、校验和,命令会报错退出(行为类似添加了 -mod=readonly 标志)。Go 团队希望开发者能更 显式地管理依赖 ,推荐使用 go mod tidy 来整理依赖关系,或使用 go get 来获取特定依赖。这有助于避免无意中修改依赖,增强了构建的 可复现性 (reproducibility)

go install 命令得到了增强,现在可以直接指定版本后缀来安装可执行文件,例如 go install example/cmd@v1.0.0。这种方式会在模块感知模式下进行构建和安装,并且 忽略当前项目的 go.mod 文件 。这使得安装 Go 工具变得非常方便,不会影响当前工作项目的依赖。官方明确推荐 使用 go install(无论带不带版本后缀)作为模块模式下构建和安装包的主要方式

相应地,使用 go get 来构建和安装包的方式 已被弃用go get 未来将专注于 依赖管理 ,推荐配合 -d 标志使用(仅下载代码,不构建安装)。在未来的版本中,-d 可能会成为 go get 的默认行为。

go.mod 文件新增了 retract 指令。模块作者可以在发现已发布的版本存在严重问题或系误发布时,使用该指令声明撤回特定版本。其他项目在解析依赖时会跳过被撤回的版本,有助于防止问题版本的扩散。

此外,go mod vendorgo mod tidy 支持了 -e 标志,允许在解析某些包出错时继续执行。Go 命令现在会忽略主模块 go.mod 中被 exclude 指令排除的版本,而不是像以前那样选择下一个更高的版本,这进一步增强了构建的确定性。

最后,go get-insecure 标志被弃用,推荐使用 GOINSECUREGOPRIVATEGONOSUMDB 环境变量进行更细粒度的控制。go get example/mod@patch 的行为也发生变化,现在要求 example/mod 必须已存在于主模块的依赖中。

这些变化体现了 Go 语言对依赖管理 规范化、显式化、可复现性 的追求。开发者应适应这些变化,使用 go mod tidygo get -d 管理依赖,使用 go install cmd@version 安装工具,并了解 retract 等新特性来更好地维护自己的模块。

Vet 新增对测试中 Goroutine 内误用 Fatal/Skip 的警告

Go 1.16 的 vet 工具增加了一项新的检查,旨在发现单元测试和基准测试 (benchmark) 中一个常见的错误模式:在测试函数启动的 goroutine 内部调用 testing.Ttesting.BFatalFatalfFailNowSkip 系列方法。

为什么这是错误的?

t.Fatal (及其类似方法) 的设计意图是 立即终止当前运行的测试函数 ,并将该测试标记为失败。然而,当你在一个由测试函数创建的新 goroutine 中调用 t.Fatal 时,它只会终止 这个新创建的 goroutine ,而 不会终止 原本的 TestXxxBenchmarkXxx 函数。这会导致测试函数本身继续执行,可能掩盖了真实的失败情况,或者导致测试结果不可靠。

错误示例:

假设我们有一个测试,需要在后台检查某个条件,如果条件不满足则标记测试失败。

代码语言:go复制
package main

import (
	"testing"
	"time"
)

func checkConditionInBackground() bool {
	time.Sleep(50 * time.Millisecond) // 模拟耗时操作
	return false // 假设条件不满足
}

// 错误的用法
func TestMyFeatureIncorrect(t *testing.T) {
	t.Log("Test started")
	go func() {
		t.Log("Goroutine started")
		if !checkConditionInBackground() {
			// 错误:这只会终止 goroutine,不会终止 TestMyFeatureIncorrect
			// 测试会继续执行并最终(错误地)报告为成功
			t.Fatal("Background condition check failed!")
		}
		t.Log("Goroutine finished check successfully") // 这行不会执行
	}()

	// 主测试 goroutine 继续执行
	time.Sleep(100 * time.Millisecond) // 等待 goroutine 执行(实践中通常用 sync.WaitGroup)
	t.Log("Test finished")             // 这行会执行,测试最终会显示 PASSED
}

在这个错误例子中,当 goroutine 中的 t.Fatal 被调用时,只有这个匿名 func 的 goroutine 被终止了。TestMyFeatureIncorrect 函数本身并不知道后台发生了错误,它会继续执行,直到完成,测试结果会被标记为 PASS,这显然不是我们期望的。Go 1.16 的 vet 工具现在会对此类用法发出警告。

正确的做法:

正确的做法是,在 goroutine 中发现错误时,应该使用 t.Errort.Errorf记录错误 ,然后通过其他方式(例如 return 语句) 安全地退出 goroutine 。主测试 goroutine 需要有一种机制(通常是 sync.WaitGroup)来等待所有子 goroutine 完成,并检查是否记录了任何错误。

代码语言:go复制
package main

import (
	"sync"
	"testing"
	"time"
)

func checkConditionInBackgroundCorrect() bool {
	time.Sleep(50 * time.Millisecond)
	return false
}

// 正确的用法
func TestMyFeatureCorrect(t *testing.T) {
	t.Log("Test started")
	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		defer wg.Done() // 确保 WaitGroup 被正确处理
		t.Log("Goroutine started")
		if !checkConditionInBackgroundCorrect() {
			// 正确:记录错误,然后正常退出 goroutine
			t.Error("Background condition check failed!")
			return // 退出 goroutine
		}
		t.Log("Goroutine finished check successfully")
	}()

	t.Log("Waiting for goroutine...")
	wg.Wait() // 等待 goroutine 执行完毕
	t.Log("Test finished")
	// t.Error 会将测试标记为失败,所以无需额外操作
	// 测试最终会显示 FAILED
}

在这个修正后的例子中,goroutine 使用 t.Error 记录失败信息,然后通过 return 退出。主测试函数使用 sync.WaitGroup 等待 goroutine 完成。因为 t.Error 被调用过,整个 TestMyFeatureCorrect 测试最终会被标记为 FAIL,这准确地反映了测试的实际结果。

开发者在编写并发测试时,应牢记 t.Fatal 等方法的行为,确保它们只在运行测试函数的主 goroutine 中被调用。对于子 goroutine 中的失败情况,应使用 t.Errort.Errorf 记录,并配合同步机制确保主测试函数能感知到这些失败。

使用 embed 包嵌入静态文件

Go 1.16 引入了一个内置的核心特性:文件嵌入。通过新的 embed 包和 //go:embed 编译器指令,开发者可以将静态资源文件(如 HTML 模板、配置文件、图片等)直接 编译进 Go 可执行文件中

为什么需要文件嵌入?

在 Go 1.16 之前,分发包含静态资源的 Go 应用通常需要将可执行文件和资源文件一起打包。这增加了部署的复杂性,容易因文件丢失或路径错误导致程序失败。文件嵌入解决了这个问题,它使得 Go 应用可以 编译成一个完全独立的、包含所有必需资源的单个可执行文件 ,极大地简化了分发和部署过程。

如何使用?

核心是 //go:embed 指令,它必须紧跟在一个 import 块之后,或者在包级别的变量声明之上。该指令告诉编译器将指定的文件或目录内容嵌入到后续声明的变量中。变量的类型决定了嵌入的方式:

  1. 嵌入单个文件到 string
代码语言:go复制
package main

import (
    _ "embed" // 需要导入 embed 包,即使只用 //go:embed
    "fmt"
)

//go:embed message.txt
var message string

func main() {
    fmt.Print(message)
}

假设同目录下有一个 message.txt 文件,内容为 "Hello, Embed!"。编译运行后,程序会打印该文件的内容。

  1. 嵌入单个文件到 []byte
代码语言:go复制
package main

import (
    _ "embed"
    "fmt"
)

//go:embed banner.txt
var banner []byte

func main() {
    fmt.Printf("Banner:\n%s", banner)
}

这对于嵌入非文本文件(如图片)或需要处理原始字节的场景很有用。[]byte 是只读的。

  1. 嵌入文件或目录到 embed.FS

这是最灵活的方式,可以将单个文件、多个文件或整个目录树嵌入到一个符合 io/fs.FS 接口的文件系统中。

假设有如下目录结构:

代码语言:txt复制
.
├── main.go
└── static/
    ├── index.html
    └── css/
        └── style.css
代码语言:go复制
package main

import (
    "embed" // 需要显式导入 embed 包
    "fmt"
    "io/fs"
    "net/http"
)

//go:embed static/*
// 或者 //go:embed static/index.html static/css/style.css
// 或者 //go:embed static
var staticFiles embed.FS

func main() {
    // 读取单个文件
    htmlContent, err := staticFiles.ReadFile("static/index.html")
    if err != nil {
        panic(err)
    }
    fmt.Println("Index HTML:", string(htmlContent))

    cssContent, err := fs.ReadFile(staticFiles, "static/css/style.css") // 也可以用 io/fs.ReadFile
    if err != nil {
        panic(err)
    }
    fmt.Println("CSS:", string(cssContent))

    // 将嵌入的文件系统作为 HTTP 文件服务器
    // 需要去除路径前缀 "static/"
    httpFS, err := fs.Sub(staticFiles, "static")
    if err != nil {
        panic(err)
    }
    http.Handle("/", http.FileServer(http.FS(httpFS))) // 使用 http.FS 转换
    fmt.Println("Serving embedded files on :8080")
    http.ListenAndServe(":8080", nil)
}

//go:embed static/*//go:embed static 会将 static 目录及其所有子目录和文件嵌入到 staticFiles 变量中。这个 embed.FS 类型的变量可以像普通文件系统一样被访问,例如使用 ReadFile 读取文件内容,或者配合 net/httphtml/template 等包使用。

重要细节:

  • //go:embed 指令后的路径是相对于 包含该指令的源文件 的目录。
  • 嵌入的文件内容在编译时确定,运行时是 只读 的。
  • 使用 embed.FS 时,需要导入 embed 包。如果仅嵌入到 string[]byte,理论上只需 import _ "embed" 来激活编译器的嵌入功能,但显式导入 embed 通常更清晰。
  • embed.FS 实现了 io/fs.FS 接口,可以与 Go 1.16 中引入的新的文件系统抽象无缝集成。

文件嵌入是 Go 1.16 中一个非常实用的新特性,它简化了资源管理和应用部署,使得创建单体、自包含的 Go 应用变得更加容易。

新的文件系统接口 io/fs 与 io/ioutil 的弃用

Go 1.16 引入了新的 io/fs 包,其核心是定义了一个 标准的文件系统接口 fs.FS 。这个接口提供了一个 统一的、只读的 文件系统访问抽象。同时,长期以来包罗万象但定义模糊的 io/ioutil 包被正式 弃用

为什么引入 io/fs

在 Go 1.16 之前,Go 标准库中操作文件系统的代码(如 os 包、net/http 包中的文件服务、html/template 包的模板加载等)通常直接依赖于操作系统的文件系统。这导致代码与底层实现耦合紧密,难以对不同类型的文件系统(如内存文件系统、zip 文件、嵌入式文件等)进行统一处理和测试。

io/fs 包的出现解决了这个问题。它定义了简洁的 fs.FS 接口,核心方法是 Open(name string) (fs.File, error)。任何实现了这个接口的类型,都可以被看作是一个文件系统,可以被各种期望使用 fs.FS 的标准库或第三方库消费。

fs.FS 的实现者 (Producers):

  • embed.FS :Go 1.16 新增的 embed 包提供的类型,用于访问编译时嵌入的文件。
  • os.DirFS(dir string)os 包新增的函数,返回一个基于操作系统真实目录的 fs.FS 实现。
代码语言:go复制
package main

import (
    "fmt"
    "io/fs"
    "os"
)

func main() {
    // 使用当前目录创建一个 fs.FS
    fileSystem := os.DirFS(".")
    // 使用 fs.ReadFile 读取文件 (需要 Go 1.16+)
    content, err := fs.ReadFile(fileSystem, "go.mod") // 读取当前目录的 go.mod
    if err != nil {
        if os.IsNotExist(err) {
            fmt.Println("go.mod not found in current directory.")
        } else {
            panic(err)
        }
    } else {
        fmt.Printf("go.mod content:\n%s\n", content)
    }
}
  • zip.Readerarchive/zip 包中的 Reader 类型现在也实现了 fs.FS,可以直接访问 zip 压缩包内的文件。
  • testing/fstest.MapFS :这是一个用于测试的内存文件系统实现,方便编写依赖 fs.FS 的代码的单元测试。

fs.FS 的消费者 (Consumers):

  • net/http.FS()http 包新增的函数,可以将一个 fs.FS 包装成 http.FileSystem,用于 http.FileServer
代码语言:go复制
package main

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed assets
var embeddedAssets embed.FS

func main() {
    // 假设 assets 目录包含 index.html 等静态文件
    // 从 embed.FS 创建子文件系统,去除 "assets" 前缀
    assetsFS, _ := fs.Sub(embeddedAssets, "assets")

    // 将 fs.FS 转换为 http.FileSystem
    httpFS := http.FS(assetsFS)

    // 创建文件服务器
    http.Handle("/", http.FileServer(httpFS))
    http.ListenAndServe(":8080", nil)
}
  • html/template.ParseFS() / text/template.ParseFS() :模板包新增的函数,可以直接从 fs.FS 中加载和解析模板文件。
代码语言:go复制
package main

import (
    "embed"
    "html/template"
    "os"
)

//go:embed templates/*.tmpl
var templateFS embed.FS

func main() {
    // 从 embed.FS 加载所有 .tmpl 文件
    tmpl, err := template.ParseFS(templateFS, "templates/*.tmpl")
    if err != nil {
        panic(err)
    }
    // 执行模板...
    tmpl.ExecuteTemplate(os.Stdout, "hello.tmpl", "World")
}
  • fs.WalkDir() / fs.ReadFile() / fs.Stat()io/fs 包自身也提供了一些通用的辅助函数,用于在任何 fs.FS 实现上进行文件遍历、读取和获取元信息。

io/ioutil 的弃用:

io/ioutil 包长期以来包含了一些方便但功能分散的函数,如 ReadFile, WriteFile, ReadDir, NopCloser, Discard 等。这些功能与其他标准库包(主要是 ioos)的功能有所重叠或关联。为了使标准库的结构更清晰、职责更分明,Go 团队决定 弃用 io/ioutil

io/ioutil 包本身 仍然存在且功能不变 ,以保证向后兼容。但是,官方 不鼓励在新代码中使用它 。其包含的所有功能都已迁移到更合适的包中:

  • ioutil.ReadFile -> os.ReadFile
  • ioutil.WriteFile -> os.WriteFile
  • ioutil.ReadDir -> os.ReadDir (返回 []os.DirEntry,比旧的 []fs.FileInfo 更高效)
  • ioutil.NopCloser -> io.NopCloser
  • ioutil.ReadAll -> io.ReadAll
  • ioutil.Discard -> io.Discard
  • ioutil.TempFile -> os.CreateTemp
  • ioutil.TempDir -> os.MkdirTemp

总结思路:

Go 1.16 通过引入 io/fs 接口,推动了文件系统操作的标准化和解耦 。这使得代码可以更灵活地处理不同来源的文件数据,无论是来自操作系统、内存、嵌入资源还是压缩包。同时,弃用 io/ioutil 并将其功能整合到 ioos 包中,是对标准库进行的一次 整理和规范化 ,使得包的功能划分更加清晰合理。开发者应当积极采用 fs.FS 接口来设计可重用、可测试的文件处理逻辑,并使用 osio 包中新的或迁移过来的函数替代 io/ioutil 的功能。

本文标签: Go 116 相比 Go 115 有哪些值得注意的改动