CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • Go性能调优 说明
  • 1. Go语言性能导论
  • 2 数据结构与算法
  • 3 理解并发
  • 4 Go语言中的等效标准模板库算法
  • 5 Go语言中的矩阵和向量计算
  • 6 编写易读的Go代码
  • 7 Go语言中的模板编程
  • 8 Go语言中的内存管理
  • 9 Go语言中的GPU并行化
  • 10 Go语言中的编译时评估
  • 11. 构建和部署Go代码
    • 构建Go二进制文件
    • go build - 构建你的Go代码
      • 构建标志
      • 构建信息
      • 编译器和链接器标志
      • 构建约束(Build constraints)
      • 文件名约定
    • go clean - 清理你的构建目录
    • 使用go get和go mod获取包依赖项
    • go list
    • go run - 运行你的包
    • go install - 安装你的二进制文件
    • 使用Docker构建Go二进制文件
    • 总结
  • 12 Go代码性能分析
  • 13 跟踪Go代码
  • 14 集群与作业队列
  • 15 跨版本比较代码质量
目录

11. 构建和部署Go代码

# 11. 构建和部署Go代码

在我们掌握了编写高性能Go代码的方法之后,就需要将其交付、验证并持续迭代。这个过程的第一步是部署新的Go代码。Go代码会被编译成二进制文件,这使得我们在代码开发的迭代过程中,能够以模块化的方式部署新的Go代码。我们可以将其推送到一个或多个地方,以便在不同的环境中进行测试。这样做能让我们优化代码,充分利用系统提供的吞吐量。

在本章中,我们将全面了解Go的构建过程。我们会探讨Go编译器如何构建二进制文件,并利用这些知识为当前平台构建大小合适、经过优化的二进制文件。我们将涵盖以下主题:

  • 构建Go二进制文件
  • 使用go clean删除目标文件
  • 使用go get下载并安装依赖项
  • 使用go mod进行依赖项管理
  • 使用go list列出包和模块
  • 使用go run执行程序
  • 使用go install安装包

这些主题将帮助我们从源代码构建高效的Go二进制文件。

# 构建Go二进制文件

在第10章 “Go中的编译时评估” 中,我们讨论了一些Go构建优化方法,这些方法可能有助于优化我们的构建策略。Go的构建系统有很多选项,可以帮助系统管理员为他们的构建策略添加更多的参数化设置。

Go工具提供了多种构建源代码的方法。让我们先对每种方法有一个大致的了解,然后再深入讨论每个包。了解这些命令之间的关键差异,有助于您理解它们如何相互作用,并为特定任务选择合适的工具。让我们来看看这些命令:

  • go build:为项目构建二进制文件,编译包及其依赖项。
  • go clean:从包源目录中删除目标文件和缓存文件。
  • go get:下载并安装包及其依赖项。
  • go mod:Go(相对较新)的内置依赖项模块系统。
  • go list:列出指定的包和模块,并可以显示有关文件、导入和依赖项的重要构建信息。
  • go run:运行并编译指定的Go程序。
  • go install:为项目构建二进制文件,将二进制文件移动到$GOPATH/bin目录,并缓存所有非主包。

在本章中,我们将深入研究Go构建系统的这些不同部分。随着我们对这些程序如何相互协作的了解不断加深,我们将能够明白如何利用它们构建精简且功能丰富的二进制文件,使其在支持的架构和操作系统上按预期运行。

在下一节中,我们将深入了解go build。

# go build - 构建你的Go代码

go build的调用语法如下:

go build [-o output] [build flags] [packages]
1

使用-o定义输出,可使用指定名称的文件来编译二进制文件。当您希望为文件遵循特定的命名约定,或者想根据不同的构建参数(平台/操作系统/git SHA等)来命名二进制文件时,这会很有帮助。

包可以定义为Go源文件的列表,也可以省略。如果指定了Go源文件列表,构建程序将把传入的文件列表视为一个指定单个包的组。如果未定义包,构建程序将验证目录中的包是否可以构建,但会丢弃构建结果。

# 构建标志

Go的构建标志可用于build、clean、install、list、run和test命令。以下是构建标志及其用法描述的表格:

构建标志 描述
-a 强制重新构建包。如果您想确保所有依赖项都是最新的,这个标志会特别有用。
-n 打印编译器使用的命令,但不运行这些命令(类似于其他语言中的预演)。这有助于查看包是如何编译的。
-p n 并行化构建命令。默认情况下,该值设置为构建系统可用的CPU数量。
-race 启用竞态检测。只有特定的架构能够进行竞态检测:
- linux/amd64
- freebsd/amd64
- darwin/amd64
- windows/amd64
-msan 检测C语言中未初始化的内存读取。仅在具有amd64或arm64架构的Linux系统上受支持,并且主机需要使用clang/LLVM编译器。可以通过CC=clang go build -msan example.go来调用。
-v 在编译程序时,将构建的包名输出到标准输出。这有助于验证构建过程中使用了哪些包。
-work 打印Go用于构建二进制文件的临时工作目录的值。默认情况下,该目录通常存储在/tmp/中。
-x 显示构建过程中使用的所有命令。这有助于确定包是如何构建的。更多信息,请参阅 “构建信息” 部分。
-asmflags '[pattern=]arg list' 调用go tool asm时传递的参数列表。
-buildmode=type 告诉构建命令我们想要构建的目标文件类型。目前buildmode有几个类型选项:
- archive:将非主包构建成.a文件。
- c-archive:将主包及其所有导入的包构建成一个C归档文件。
- c-shared:将主包及其导入的包构建成一个C共享库。
- default:创建一个主包列表。
- shared:将所有非主包合并成一个共享库。
- exe:将主包及其导入的包构建成可执行文件。
- pie:将主包及其导入的包构建成位置无关可执行文件(PIE)。
- plugin:将主包及其导入的包构建成一个Go插件。
-compiler name 确定使用哪个编译器。常见的有gccgo和gc。
-gccgoflags gccgo编译器和链接器的调用标志。
-gcflags gc编译器和链接器的调用标志。更多详细信息,请参阅 “编译器和链接器” 部分。
-installsuffix suffix 在包安装目录的名称中添加一个后缀。这用于将输出与默认构建区分开来。
-ldflags '[pattern=]arg list' go tool link的调用参数。更多详细信息,请参阅 “编译器和链接器” 部分。
-linkshared 在执行-buildmode=shared之后,此标志用于链接新创建的共享库。
-mod 确定使用哪种模块下载模式。在撰写本文时,有两个选项:-readonly或vendor。
-pkgdir dir 使用指定的目录来安装和加载所有包。
-tags tag,list 构建过程中需要满足的构建标签列表。该列表以逗号分隔的形式传递。
-trimpath 在构建可执行文件时,生成的可执行文件将对文件系统路径使用不同的命名方案。具体如下:
- Go(用于标准库)
- path @version(用于Go模块)
- plain import path(在使用GOPATH时)
-toolexec 'cmd args' 调用工具链程序,如调试器或其他交互式程序。这用于诸如vet和asm等程序。

掌握了这些信息,您将能够有效地设置正确的链接器标志。

# 构建信息

为了更好地理解构建过程,让我们看一些构建示例,以便深入了解构建工具是如何协同工作的。

假设我们要构建一个带有Prometheus导出器的简单HTTP服务器。我们可以这样创建一个导出器:

package main

import (
    "fmt"
    "net/http"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    http.Handle("/", promhttp.Handler())
    port := ":2112"
    fmt.Println("Prometheus Handler listening on port ", port)
    http.ListenAndServe(port, nil)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

准备好包之后,我们可以使用以下命令构建包:

go build -p 4 -race -x prometheusExporterExample.go
1

在构建这个二进制文件时,我们会在标准输出中看到一些信息(因为我们传递了-x标志来查看构建过程中使用的命令)。让我们来看看:

  1. 为了便于阅读,我们将截断输出内容。如果您自己测试,会看到更详细的构建输出:
WORK=/tmp/go-build924967855
1

这为构建设置了一个临时工作目录。如前所述,除非另有指定,该目录通常位于/tmp/目录下:

mkdir -p $WORK/b001/
1
  1. 编译器还会创建一个子工作目录:
cat >$WORK/b001/importcfg.link << 'EOF' #  internal
1
  1. 创建并添加链接配置。这会向链接配置中添加各种不同的参数:
packagefile command-line-arguments=/home/bob/.cache/go-build/aa/aa63d73351c57a147871fde4964d74c9a39330b467c6d73640815775e6673084-d
1
  1. 从缓存中引用命令行参数的包:
packagefile fmt=/home/bob/.cache/go-build/74/749e110dc104578def1859fbd4ca5c5546f4032f02ffd5ea4d14c730fbd65b81-d
1

fmt是我们用于显示fmt.Println("Prometheus Handler listening on port ", port)的打印包。它的引用如下:

packagefile github.com/prometheus/client_golang/prometheus/promhttp=/home/bob/.cache/go-build/e9/e98940b17504e2f647dccc7832793448aa4e8a64047385341c94c1c4431d59cf-d
1
  1. 编译器还会添加Prometheus HTTP客户端库的包。在此之后,还有许多其他引用会添加到构建过程中。为简洁起见,这里进行了截断。文件末尾以EOF表示。
  2. 创建一个可执行目录:
mkdir -p $WORK/b001/exe/
1
  1. 然后,编译器使用之前创建的importcfg来构建二进制文件:
/usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -installsuffix race -buildmode=exe -buildid=bGYa4XecCYqWj3VjKraU/eHfXIjk2XJ_C2azyW4yU/8YHxpy5Xa69CGQ4FC9Kb/bGYa4XecCYqWj3VjKraU -race -extld=gcc /home/bob/.cache/go-build/aa/aa63d73351c57a147871fde4964d74c9a39330b467c6d73640815775e6673084-
1
  1. 然后添加一个构建ID:
/usr/lib/golang/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
1
  1. 接下来,将二进制文件重命名为我们的导出器示例的文件名(因为我们没有使用-o指定不同的二进制文件名):
cp $WORK/b001/exe/a.out prometheusExporterExample
1
  1. 最后,删除工作目录:
rm -r $WORK/b001/
1

这个程序的构建结果是一个Go二进制文件。在下一节中,我们将讨论编译器和链接器标志。

# 编译器和链接器标志

在构建Go二进制文件时,-gcflags标志允许您传递可选的编译器参数,而-ldflags标志允许您传递可选的链接器参数。通过调用以下命令,可以找到编译器和链接器标志的完整列表:

go tool compile -help
go tool link -help
1
2

让我们看一个使用编译器和链接器标志的示例。我们可以构建一个简单的程序,该程序返回一个未初始化的字符串变量的值。下面这个程序看起来没什么问题:

package main

import "fmt"

var linkerFlag string

func main() {
    fmt.Println(linkerFlag)
}
1
2
3
4
5
6
7
8
9

如果使用一些常见的编译器和链接器标志来构建此代码,我们将看到一些有用的输出:

我们这里传递的编译器标志实现了以下功能:

  • “-m -m”:打印关于编译器优化决策的信息。这就是我们在构建命令执行后,在上图中看到的输出内容。
  • “-N”:禁用Go二进制文件中的优化。
  • “-l”:禁用内联(inlining)。

我们传递的链接器标志的作用如下:

  • “-X main.linkerFlag=Hi_Gophers”:为main包中的linkerFlag变量设置一个值。在构建时能够添加变量很重要,因为许多开发者希望在编译时向代码中添加某种构建参数。我们可以使用date -u +.%Y%m%d%.H%M%S传递构建日期,或者使用git rev-list -1 HEAD传递Git提交版本。这些值在之后用于引用构建状态时会很有帮助。
  • “-s”:禁用符号表(symbol table),符号表是一种数据结构,它将源代码中的每个标识符与声明信息一起存储。对于生产环境的二进制文件,通常不需要这个符号表。
  • “-w”:禁用DWARF生成。由于Go二进制文件已经包含了基本类型信息、程序计数器到行数据(PC-to-linedata)和符号表,所以DWARF表通常无需保存。

如果我们先用标准方法构建二进制文件,然后再使用一些可用的编译器和链接器标志,就可以看到二进制文件大小的差异:

  • 未优化的构建:
$ go build -ldflags "-X main.linkerFlag=Hi_Gophers" -o nonOptimized
1
  • 优化后的构建:
$ go build -gcflags="-N -l" -ldflags "-X main.linkerFlag=Hi_Gophers -s -w" -o Optimized
1

可以看到,优化后的二进制文件比未优化的二进制文件小28.78%:

这两个二进制文件对最终用户来说功能相同,所以可以考虑使用编译器和链接器标志去除一些构建优化,以减小最终生成的二进制文件大小。这在存储和部署这些二进制文件时会很有帮助。

# 构建约束(Build constraints)

如果你想为Go构建添加构建约束,可以在文件开头添加注释行,注释行前面只能是空白行和其他注释。这种注释的格式是// +build darwin,amd64,!cgo,android,386,cgo。

这对应的布尔表达式是(darwin AND amd64 AND (NOT cgo)) OR (android AND 386 AND cgo)。

构建约束需要在包声明之前,且构建约束和包初始化之间要有一个换行符。其格式如下:

// +build [OPTIONS]
package main
1
2

完整的构建约束列表可以在 https://golang.org/pkg/go/build/#hdr-Build_Constraints 找到。这个列表包括以下构建约束:

  • GOOS
  • GOARCH
  • 编译器类型(gc或gccgo)
  • cgo
  • 所有1.x版本的Go(没有针对测试版或小版本发布的构建标签)
  • ctxt.BuildTags中列出的其他单词

如果你想在库中排除某个文件不参与构建,也可以添加如下形式的注释:

// +build ignore
1

相反,你可以使用如下形式的注释将文件构建限制在特定的GOOS、GOARCH和cgo条件下:

// +build windows,386,cgo
1

只有在使用cgo并且在Windows操作系统的386处理器上进行构建时,这个文件才会被构建。这是Go语言中一个强大的功能,因为你可以根据必要的构建参数来构建包。

# 文件名约定

如果一个文件在去掉任何扩展名和_test后缀(用于测试用例)后,匹配GOOS和GOARCH模式,那么这个文件将针对该特定的GOOS或GOARCH模式进行构建。常见的模式如下:

  • *_GOOS
  • *_GOARCH
  • *_GOOS_GOARCH

例如,如果你有一个名为example_linux_arm.go的文件,它只会作为Linux arm架构构建的一部分被构建。

在下一节中,我们将探索go clean命令。

# go clean - 清理你的构建目录

Go命令会在一个临时目录中构建二进制文件。go clean命令用于删除其他工具创建的或手动调用go build时生成的多余目标文件。go clean的使用格式为go clean [clean flags] [build flags] [packages]。

clean命令有以下可用标志:

  • -cache标志:删除整个Go构建缓存。如果你想在多个系统上比较全新的构建,或者想了解全新构建所需的时间,这个标志会很有用。
  • -i标志:删除go install创建的归档文件或二进制文件。
  • -n标志:不执行任何操作,仅打印删除命令但不执行它们。
  • -r标志:对导入路径的包的所有依赖项递归应用清理逻辑。
  • -x标志:打印并执行生成的删除命令。
  • -cache标志:删除整个Go构建缓存(重复列出,疑为原文重复)。
  • -testcache标志:删除构建缓存中的测试结果。
  • -modcache标志:删除模块下载缓存。

如果我们想在没有现有依赖项的情况下进行全新构建,可以使用一个命令来删除Go构建系统中许多重要缓存中的内容。下面来看一下具体步骤:

  1. 我们将构建prometheusExporterExample,以验证构建缓存大小的变化。我们可以使用Go环境变量GOCACHE来找到构建缓存的位置:

  1. 为了进行验证,我们将连续使用几个命令。首先,使用rm -rf ~/.cache/go-build/删除整个缓存目录。
  2. 接下来,运行go build prometheusExporterExample.go命令构建我们的Go二进制文件。
  3. 然后,使用du -sh ~/.cache/go-build/检查缓存大小,以验证缓存是否显著增大。
  4. 现在,我们可以使用go clean程序清理缓存,即go clean -cache -modcache -i -r 2&>/dev/null。

需要注意的是,一些缓存信息存储在主库中,普通用户无法删除这些内容。如果需要,我们可以以超级用户身份运行清理命令来解决这个问题,但通常不建议这样做 。

然后,我们可以验证缓存是否变小。清理后查看缓存目录,会发现其中只剩下三个文件:

  • 一个README文件,用于解释该目录。

  • 一个log.txt文件,记录缓存信息。

  • 一个trim.txt文件,记录上次完成缓存清理的时间。

验证构建过程中缓存的内容是否正确,有助于加快构建速度,并让开发过程更加轻松。

在下一节中,我们将介绍go get和go mod命令。

# 使用go get和go mod获取包依赖项

在构建Go程序时,你可能经常需要添加依赖项。go get命令用于下载并安装包及其依赖项。go get的调用格式为go get [-d] [-f] [-t] [-u] [-v] [-fix] [-insecure] [build flags] [packages]。

Go 1.11版本增加了对Go模块的初步支持。我们在第6章“编写可读性强的Go代码”的“Go模块”部分学习了如何使用Go模块。

Go模块供应商(Go mod vendor)通常是Go构建系统的一部分,因为我们可以在Go程序中使用供应商依赖项。在代码库中使用供应商依赖项有利有弊。在构建时所有所需的依赖项都在本地可用,可以加快构建速度。但是,如果用于构建依赖项的上游存储库发生更改或被删除,构建就会失败,因为你的程序无法满足其上游依赖项。

使用供应商依赖项的缺点还包括,更新依赖项的责任落在了程序员身上。如果使用了供应商依赖项但未进行更新,那么上游的更新,如安全更新、性能改进和稳定性增强等,都可能无法应用。

许多企业选择使用供应商依赖项的方式,因为他们认为存储所有所需依赖项的安全性,比在有新版本可用时更新供应商目录更为重要。

初始化Go模块后,我们可以将依赖项进行供应商管理,并使用供应商模块进行构建:

如上述输出所示,我们有满足项目构建约束所需的供应商依赖项(来自https://github.com/和https://golang.org/)。我们可以在构建过程中使用go mod tidy来验证go.mod文件是否包含存储库所需的所有元素。

go mod tidy命令会添加缺失的模块,并删除未使用的模块,以确保源代码与目录中的go.mod文件匹配。

在下一节中,我们将学习go list命令。

# go list

go list命令用于列出指定的包和模块,以及显示有关文件、导入和依赖项的重要构建信息。go list的调用格式为usage: go list [-f format] [-json] [-m] [list flags] [build flags] [packages]。

能够访问作为构建过程主要部分的数据结构非常有用。我们可以使用go list命令来深入了解正在构建的程序。例如,对于下面这个简单的程序,它会打印一条消息并为用户计算平方根:

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println("Hello Gophers")
    fmt.Println(math.Sqrt(64))
}
1
2
3
4
5
6
7
8
9
10
11

如果我们想了解特定项目的所有依赖项,可以调用如下命令:

 go list -f '{{.Deps}}' 
1

结果将是存储库中包含的所有依赖项的切片:

go list的数据结构可以在https://golang.org/cmd/go/#hdr-List_packages_or_modules找到。它有许多不同的参数。go list程序另一个常用的输出是JSON格式的输出。在下面的截图中,你可以看到对listExample.go执行go list -json的输出:

go list -m -u all命令也会显示项目的依赖项。如果有可用的升级版本,输出结果中还会在括号里列出第二个版本号。如果我们想通过go mod包密切监控依赖项及其升级情况,这个命令会很有帮助。

以Prometheus导出器(Prometheus exporter)为例,我们可以查看包的依赖项是否需要升级:

在这个例子中,可以看到有几个包有可用的升级版本。如果对其中一个依赖项执行go get命令,就能有效地进行升级。比如,使用go get github.com/pkg/errors@v0.8.1,可以将上图中列出的errors包从v0.8.0升级到v0.8.1。

完成升级后,我们可以运行go list -m -u github.com/pkg/errors来验证依赖项是否已升级。

下面的截图展示了输出结果:

从上述输出中可以看到,现在引用的errors包是v0.8.1,而不是之前输出中的v0.8.0。

接下来,让我们了解一下go run命令。

# go run - 运行你的包

go run命令用于运行并编译指定的Go程序。go run的调用格式为go run [构建标志] [-exec xprog] 包 [参数...]。

go run允许开发者通过一次操作快速编译并运行Go二进制文件。在这个过程中,go run会构建可执行文件,运行它,然后删除该可执行文件。这在开发环境中特别有用。在快速迭代Go程序时,go run可以作为一种快捷方式,用于验证正在修改的代码能否生成可接受的构建产物。正如我们在本章前面所学,这些工具的许多构建标志是一致的。

goRun.go是一个非常简单的Go程序,它没有参数,只有一个空的main()函数调用。我们以此为例,展示这个过程,且该示例没有额外的依赖项或开销:

package main

func main() {}
1
2
3

通过执行go run -x goRun.go命令,我们可以查看go run调用相关的工作输出。

执行此操作时,我们能够看到作为go run程序一部分被调用的构建参数:

这个输出看起来应该很熟悉,因为它与我们在go build示例中看到的输出非常相似。然后,我们可以看到我们的包被调用。

如果对Prometheus HTTP服务器执行相同的操作,会发现执行go run程序后,Prometheus HTTP服务器会启动并运行。在go run调用过程中终止进程后,会注意到本地目录中没有存储任何二进制文件。默认情况下,go run调用不会保存这些输出。

下一节要介绍的Go命令(go install)是本章的最后一个命令。让我们来了解一下它的作用。

# go install - 安装你的二进制文件

go install命令用于编译并安装指定的Go程序。go install的调用格式为go install [-i] [构建标志] [包]。

这些包会被导入到$GOPATH/pkg目录。如果缓存的项目没有被修改,下次编译时会使用它们。go install的结果是生成一个可执行文件,它与使用go build命令编译的文件相同,并安装在系统的$GOBIN路径下。例如,如果我们想在主机上安装Prometheus HTTP服务器,可以执行go install命令,即GOBIN=~/prod-binaries/ go install -i prometheusExporterExample.go。

设置GOBIN变量可以告诉编译器编译完成后将编译好的二进制文件安装到哪里。go install程序允许我们将二进制文件安装到GOBIN指定的位置。-i标志用于安装指定包的依赖项。下面的截图展示了这一过程:

完成上述操作后,可以看到在示例中定义的GOBIN位置有一个prometheusExporterExample二进制文件。

在本章的最后一节,我们将了解如何使用Docker构建Go二进制文件。

# 使用Docker构建Go二进制文件

根据目标架构的不同,你可能希望使用Docker构建Go二进制文件,以保持构建的可重复性、限制构建大小,并最小化服务的攻击面。使用多阶段Docker构建可以帮助我们完成这项任务。

要执行这些操作,你必须安装最新版本的Docker。我们将要使用的多阶段构建功能要求Docker守护进程和客户端的版本都在17.05或更高。你可以在https://docs.docker.com/install/上找到适合你操作系统的最新Docker版本以及安装说明。

考虑下面这个简单的包,它会在屏幕上打印一条调试消息:

package main

import "go.uber.org/zap"

func main() {
    zapLogger := zap.NewExample()
    defer zapLogger.Sync()
    zapLogger.Debug("Hi Gophers - from our Zap Logger")
}
1
2
3
4
5
6
7
8
9

如果我们想在最小化依赖项的情况下,在Docker容器中构建并运行这个包,可以使用多阶段Docker构建。为此,可以执行以下步骤:

  1. 通过执行以下命令,将当前目录初始化为模块的根目录:
go mod init github.com/bobstrecansky/HighPerformanceWithGo/11-deploying-go-code/multiStageDockerBuild
1
  1. 通过执行以下命令添加供应商仓库:
go mod vendor
1

现在,我们的仓库中就有了所有必需的供应商包(在这个例子中是Zap日志记录器)。下面的截图展示了这一情况:

  1. 构建zapLoggerExample的Docker容器。我们可以使用以下Dockerfile来构建容器:
# Builder - stage 1 of 2
FROM golang:alpine as builder
COPY . /src
WORKDIR /src
RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -o zapLoggerExample

# Executor - stage 2 of 2
FROM alpine:latest
WORKDIR /src/
COPY --from=builder /src/zapLoggerExample .
CMD ["./zapLoggerExample"]
1
2
3
4
5
6
7
8
9
10
11

请注意,我们使用golang:alpine镜像来构建Go二进制文件,因为它是包含成功构建Go二进制文件所需元素的最简单的Docker镜像之一。我们使用alpine:latest镜像来运行Go二进制文件,因为它是包含成功运行Go二进制文件所需元素的最简单的Docker镜像之一。

在这个Dockerfile示例中,我们使用多阶段Docker构建来构建和运行二进制文件。在第一阶段(构建阶段),我们以golang alpine镜像为基础。将当前目录中的所有文件复制到Docker容器的/src/目录中,将/src/设置为工作目录,然后构建Go二进制文件。禁用cgo、为Linux架构构建以及添加在第一步中创建的供应商目录,这些都有助于最小化构建大小和时间。

在第二阶段(执行阶段),我们使用基本的alpine Docker镜像,将/src/设置为工作目录,并将第一阶段构建的二进制文件复制到这个Docker容器中。然后,我们在这个Docker构建中执行日志记录器作为最后一个命令。 4. 准备好必要的依赖项后,我们可以通过执行以下命令来构建Docker容器:

docker build -t zaploggerexample .
1
  1. 构建好Docker容器后,我们可以通过执行以下命令来运行它:
docker run -it --rm zaploggerexample
1

在下面的截图中,你可以看到我们的构建和执行步骤都已完成:

在多阶段Docker容器中构建Go程序,有助于创建可复现的构建、限制二进制文件大小,并通过仅使用所需的部分内容来最小化服务的攻击面。

# 总结

在本章中,我们学习了如何构建Go二进制文件,了解了如何高效且稳定地进行构建。我们还学习了如何理解和管理依赖项,使用go run测试Go代码,以及使用go install将Go二进制文件安装到特定位置。理解这些二进制文件的工作原理,将帮助你更高效地迭代代码。

在下一章中,我们将探讨如何对Go代码进行性能分析,以找出功能瓶颈。

上次更新: 2025/04/08, 19:40:35
10 Go语言中的编译时评估
12 Go代码性能分析

← 10 Go语言中的编译时评估 12 Go代码性能分析→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式