章节

18.3 练习

本篇通过若干分级练习巩固 go build、交叉编译与部署产物整理,并能把真实 Go 程序打包成可发布版本。

练习

学完本章你应该掌握

  • 能把一个真正可运行的 main 包用 go build 编译成可执行文件,并根据场景使用 -o 指定清晰的产物名。
  • 能根据目标系统和架构写出正确的 GOOSGOARCH 组合,产出 Windows、Linux、macOS 等不同平台版本。
  • 能识别哪些项目当前不能直接打包,例如目录不是 main 包、没有入口函数、打包前测试没过等问题。
  • 能在部署时区分“二进制文件”和“运行时依赖”,知道配置文件、数据文件、模板和静态资源不会自动跟着普通打包一起进入二进制。
  • 能把前面学过的函数、切片、map、文件操作、测试、HTTP、TCP、错误处理等知识自然放进部署场景,而不是只会机械背命令。

简单

第 1 题:打包一个成绩摘要工具

第 1 题 简单

编写一个 Go 程序,完成下面要求:

  • 编写函数 calcAvg(scores []int) float64
  • main 中准备切片:
1
scores := []int{78, 92, 85}
  • 输出:
1
2
count=3
avg=85.0
  • 再把当前程序打包成一个名为 score-tool 的可执行文件
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func calcAvg(scores []int) float64 {
	total := 0
	for _, score := range scores {
		total += score
	}

	return float64(total) / float64(len(scores))
}

func main() {
	scores := []int{78, 92, 85}

	fmt.Printf("count=%d\n", len(scores))
	fmt.Printf("avg=%.1f\n", calcAvg(scores))
}

打包命令:

1
go build -o score-tool .

说明:

  • 这道题真正要练的是:先有一个能运行的 main 包,再执行 go build -o ... 生成可执行文件。
  • 如果你当前在 Windows 上练习,也可以把输出名写成 score-tool.exe,这样更直观。

第 2 题:修正无法生成可执行文件的程序入口

第 2 题 简单

下面这段代码的目标是统计成绩总分和平均分,但它当前不能直接打包成可执行文件。请改正后让它可以正常打包,并输出:

1
summary=count=3 avg=80.0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package report

import "fmt"

func buildSummary(scores []int) string {
	total := 0
	for _, score := range scores {
		total += score
	}

	avg := float64(total) / float64(len(scores))
	return fmt.Sprintf("summary=count=%d avg=%.1f", len(scores), avg)
}

额外要求:

  • 让当前目录成为一个可执行程序
  • 写出 Windows 下的打包命令,输出文件名为 report.exe
查看参考答案

修正后的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func buildSummary(scores []int) string {
	total := 0
	for _, score := range scores {
		total += score
	}

	avg := float64(total) / float64(len(scores))
	return fmt.Sprintf("summary=count=%d avg=%.1f", len(scores), avg)
}

func main() {
	scores := []int{72, 81, 87}
	fmt.Println(buildSummary(scores))
}

打包命令:

1
go build -o report.exe .

关键点:

  • 只有 main 包并且存在 main() 入口函数时,当前目录才适合直接打包成可执行文件。
  • 原代码更像一个普通库包,能被别人调用,但不能直接当程序入口运行。

第 3 题:为同一个程序生成 Windows 和 Linux 版本

第 3 题 简单

假设你当前目录已经有一个可正常打包的 main 程序。请使用 PowerShell 写出下面两条打包命令:

  • 生成 windows/amd64 可执行文件 summary.exe
  • 生成 linux/amd64 可执行文件 summary-linux
查看参考答案
1
2
3
4
5
6
7
$env:GOOS = "windows"
$env:GOARCH = "amd64"
go build -o summary.exe .

$env:GOOS = "linux"
$env:GOARCH = "amd64"
go build -o summary-linux .

说明:

  • GOOS 表示目标操作系统,GOARCH 表示目标 CPU 架构。
  • Windows 目标文件通常带 .exe,Linux 目标文件一般不带。
  • 在同一个终端里反复切换平台时,要记得每次都确认当前 GOOSGOARCH,不要把上一次的目标平台误带到下一次构建里。

第 4 题:打包一个 HTTP 健康检查服务

第 4 题 简单

编写一个 Go 程序,完成下面要求:

  • 使用 net/http 启动一个 HTTP 服务
  • 注册路由 /health
  • 访问 /health 时返回:
1
ok
  • 服务监听 :8080
  • 再把当前程序打包成名为 health-server 的可执行文件
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"net/http"
)

func healthHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok")
}

func main() {
	http.HandleFunc("/health", healthHandler)
	fmt.Println("listen on :8080")

	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

打包命令:

1
go build -o health-server .

说明:这道题虽然顺手复习了上一单元的 HTTP 服务端,但主考点仍然是“先有一个可运行服务,再把它编译成部署产物”。

第 5 题:交叉编译一个 TCP 客户端

第 5 题 简单

编写一个 Go 程序,完成下面要求:

  • 使用 net.Dial 连接 127.0.0.1:9000
  • 向服务端发送 ping
  • 读取服务端响应并输出
  • 最后把这个程序交叉编译成 linux/amd64 版本
  • 输出文件名为 tcp-client-linux
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"fmt"
	"net"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:9000")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	_, err = conn.Write([]byte("ping"))
	if err != nil {
		panic(err)
	}

	buffer := make([]byte, 1024)
	n, err := conn.Read(buffer)
	if err != nil {
		panic(err)
	}

	fmt.Println(string(buffer[:n]))
}

PowerShell 交叉编译命令:

1
2
3
$env:GOOS = "linux"
$env:GOARCH = "amd64"
go build -o tcp-client-linux .

说明:

  • 这道题让你在打包时顺手复习 TCP 客户端的最小读写流程。
  • 真正运行客户端前,仍然需要先准备好对应的服务端;但“能不能运行”和“能不能编译出目标平台的产物”是两个不同层面的检查。

一般

第 6 题:先测试再打包成绩等级工具

第 6 题 一般

编写一个 Go 小工具,完成下面要求:

  • grade.go 中编写函数 Grade(score int) string
  • 规则如下:
1
2
3
4
5
score < 0 或 score > 100 -> 非法
90 ~ 100 -> A
80 ~ 89 -> B
60 ~ 79 -> C
其他 -> D
  • main 中输出:
1
85 -> B
  • 再在 grade_test.go 中使用表格驱动子测试,至少覆盖下面 4 个场景: 59 -> D 60 -> C 90 -> A 101 -> 非法
  • 最后先运行测试,再打包成 grade-tool
查看参考答案

grade.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func Grade(score int) string {
	switch {
	case score < 0 || score > 100:
		return "非法"
	case score >= 90:
		return "A"
	case score >= 80:
		return "B"
	case score >= 60:
		return "C"
	default:
		return "D"
	}
}

func main() {
	fmt.Printf("85 -> %s\n", Grade(85))
}

grade_test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "testing"

func TestGrade(t *testing.T) {
	cases := []struct {
		name  string
		score int
		want  string
	}{
		{name: "d_upper", score: 59, want: "D"},
		{name: "c_lower", score: 60, want: "C"},
		{name: "a_lower", score: 90, want: "A"},
		{name: "too_large", score: 101, want: "非法"},
	}

	for _, item := range cases {
		item := item
		t.Run(item.name, func(t *testing.T) {
			got := Grade(item.score)
			if got != item.want {
				t.Fatalf("Grade(%d) = %s, want %s", item.score, got, item.want)
			}
		})
	}
}

建议命令:

1
2
go test ./...
go build -o grade-tool .

说明:

  • 这道题的部署主线是“打包前先做基本验证”,不是测试章节的重复。
  • 读者在这一步会自然复习函数、switch、子测试和 go test,但最终目标仍然是得到一个更可靠的可执行文件。

第 7 题:部署一个依赖配置文件的程序

第 7 题 一般

编写一个 Go 程序,完成下面要求:

  • 编写函数 loadMode(path string) (string, error)
  • app.conf 中读取一行配置:
1
mode=dev
  • main 中输出:
1
mode=dev
  • 把程序打包成 mode-reader
  • 最后回答:部署到目标机器时,除了二进制文件,还需要一起带上什么文件
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"fmt"
	"os"
	"strings"
)

func loadMode(path string) (string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}

	lines := strings.Split(string(data), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			continue
		}

		key := strings.TrimSpace(parts[0])
		value := strings.TrimSpace(parts[1])
		if key == "mode" {
			return value, nil
		}
	}

	return "", fmt.Errorf("mode not found")
}

func main() {
	mode, err := loadMode("app.conf")
	if err != nil {
		fmt.Println("load config error:", err)
		return
	}

	fmt.Printf("mode=%s\n", mode)
}

打包命令:

1
go build -o mode-reader .

部署时还需要一起带上的文件:

1
app.conf

说明:

  • 运行这个程序前,要先确保当前目录里已经准备好了 app.conf,内容例如 mode=dev
  • 普通 go build 只会生成二进制文件,不会自动把运行时要读取的配置文件一起打进去。
  • 这正是部署章节里非常容易忽略的一点:程序能编译成功,不等于目标机器上运行时一定能找到依赖文件。

第 8 题:一次产出三个平台版本

第 8 题 一般

假设你当前目录里已经有上一题的 mode-reader 程序,并且你准备在打包前先跑测试。请使用 PowerShell 写出下面这组命令:

  • 先运行所有测试
  • 再生成 windows/amd64 版本:mode-reader.exe
  • 再生成 linux/amd64 版本:mode-reader-linux-amd64
  • 再生成 darwin/arm64 版本:mode-reader-darwin-arm64
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
go test ./...

$env:GOOS = "windows"
$env:GOARCH = "amd64"
go build -o mode-reader.exe .

$env:GOOS = "linux"
$env:GOARCH = "amd64"
go build -o mode-reader-linux-amd64 .

$env:GOOS = "darwin"
$env:GOARCH = "arm64"
go build -o mode-reader-darwin-arm64 .

说明:

  • 三个平台的核心差别就在 GOOSGOARCH 和产物命名上。
  • Windows 版本带 .exe,Linux 和 macOS 版本一般不带。
  • 如果你后面还要继续打本机版本,记得重新检查当前终端里的目标平台变量,不要把前面的交叉编译设置误带到后续构建中。

进阶

第 9 题:部署一个成绩查询 HTTP 服务

第 9 题 进阶

请完成一个稍微完整一点的小场景:

  • 编写函数 loadScores(path string) (map[string]int, error)
  • scores.txt 中按行读取学生成绩,文件内容格式如下:
1
2
3
小李 82
小王 59
小张 91
  • 程序启动时先加载这份成绩表
  • 启动一个 HTTP 服务,监听 :8080
  • 注册路由 /score
  • 访问方式示例:
1
/score?name=小李
  • 如果找到了,返回:
1
小李=82
  • 如果没找到,返回:
1
not found
  • 最后写出:
    1. 本机打包命令,输出 score-server
    2. linux/amd64 交叉编译命令,输出 score-server-linux
    3. 部署到 Linux 机器时需要一起上传的文件
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
	"bufio"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"strings"
)

func loadScores(path string) (map[string]int, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	scores := make(map[string]int)
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}

		parts := strings.Fields(line)
		if len(parts) != 2 {
			return nil, fmt.Errorf("bad line: %s", line)
		}

		score, err := strconv.Atoi(parts[1])
		if err != nil {
			return nil, fmt.Errorf("parse score: %w", err)
		}

		scores[parts[0]] = score
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return scores, nil
}

func main() {
	scores, err := loadScores("scores.txt")
	if err != nil {
		fmt.Println("load scores error:", err)
		return
	}

	http.HandleFunc("/score", func(w http.ResponseWriter, r *http.Request) {
		name := strings.TrimSpace(r.URL.Query().Get("name"))
		if name == "" {
			fmt.Fprintln(w, "name required")
			return
		}

		score, ok := scores[name]
		if !ok {
			fmt.Fprintln(w, "not found")
			return
		}

		fmt.Fprintf(w, "%s=%d\n", name, score)
	})

	fmt.Println("listen on :8080")
	err = http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

本机打包命令:

1
go build -o score-server .

PowerShell 交叉编译 Linux:

1
2
3
$env:GOOS = "linux"
$env:GOARCH = "amd64"
go build -o score-server-linux .

部署到 Linux 机器时需要一起上传的文件:

1
2
score-server-linux
scores.txt

说明:

  • r.URL.Query().Get("name") 用来读取查询参数,这是在 HTTP 服务里很常见的取参方式。
  • 这道题把文件读取、map 查询、错误处理和 HTTP 服务端串起来了,但整道题的核心仍然是:把真实服务程序编译成部署产物,并且别忘了它运行时依赖的数据文件。

第 10 题:在同一仓库里分别打包 server 和 client

第 10 题 进阶

请完成一个稍微扩展一点的部署练习:

  • 在同一个仓库中准备两个可执行程序:
    1. server/main.go
    2. client/main.go
  • server/main.go 的要求: 启动 HTTP 服务 注册 /hello 访问时返回:
1
hello deploy
  • client/main.go 的要求: 请求 http://127.0.0.1:8080/hello 读取响应体并输出

  • 最后写出下面三条打包命令:

    1. 打包服务端,输出 hello-server
    2. 打包客户端,输出 hello-client
    3. 交叉编译 linux/amd64 的服务端版本,输出 hello-server-linux

提示:这题比正文多走了一步。正文主要演示的是“打包当前目录”,这里额外练的是“在一个仓库里打包不同子目录下的 main 包”。

查看参考答案

server/main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "hello deploy")
	})

	fmt.Println("server listen on :8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

client/main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	resp, err := http.Get("http://127.0.0.1:8080/hello")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}

	fmt.Print(string(data))
}

打包命令:

1
2
go build -o hello-server ./server
go build -o hello-client ./client

PowerShell 交叉编译 Linux 服务端:

1
2
3
$env:GOOS = "linux"
$env:GOARCH = "amd64"
go build -o hello-server-linux ./server

说明:

  • ./server./client 表示要构建的是哪个子目录下的包,而不是当前根目录。
  • 当一个仓库里同时存在多个 main 程序时,这种按包路径打包的写法很常见,也很接近真实项目里的发布流程。
本文禁止转载
使用 Hugo 构建
主题 StackJimmy 设计 由 Hobin 魔改
最近构建时间:2026-04-17 19:07:48 CST
载入天数...载入时分秒...
发表了 1 篇文章 · 发表了 152 篇笔记 · 总计 18 万 0 千字