《Go 语言编程入门》1.2 第一个程序与 go run/build

本节让 TaskAPI 说出第一句话:写出最小可运行的 main.go,讲清 package main、import 与 func main 三件套;随后用实测对比 go run、go build、go install 三种执行路径的差别与产物位置,再用 os.Args 为后续 add/list/toggle 子命令打底,最后用 go vet 与退出码收尾。

1.2 第一个程序与 go run/build

任何一个 Go 可执行程序都长着同一副骨架:package main 声明这是一个可独立运行的程序,import 引入要用的包,func main() 是入口。三件套缺一不可,也没有 public static void main(String[]) 那样冗长的修饰。本节先把这副骨架写出来,再逐一解释每一行的含义,最后把「写完怎么跑」这条路彻底走通。

本节把 TaskAPI 推进到「能跑出第一行输出」:建立 main.go,让它打印项目名与所用 Go 版本,并跑通 go run、go build、go install 三种执行方式。这是 TaskAPI 的第一次编译,虽然还没有任何业务逻辑。

1.2.1 最小可运行程序

先建一个目录,把代码放进去:

mkdir taskapi && cd taskapi

新建 main.go,内容如下:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println("TaskAPI", version)
	fmt.Println("built with", runtime.Version())
}

const version = "v0.1.0"

逐块读一遍:

  • package main:包声明。名字叫 main 的包是可执行程序的约定;其他任何包名都会编译成库,无法直接运行。
  • import (...):引入标准库。这里引入了 fmt(格式化输出)和 runtime(运行时信息)。Go 不允许导入未使用的包,编译会直接报错。
  • func main():程序入口。它没有参数、没有返回值,由运行时自动调用。
  • const version = "v0.1.0":包级常量。声明顺序在 Go 里不敏感,常量写在 main 后面完全合法。

直接跑起来:

GOTOOLCHAIN=go1.27.0 go run .
TaskAPI v0.1.0
built with go1.27.0

runtime.Version() 返回的是编译这份二进制所用工具链的版本,所以输出的 go1.27.0 正好印证了上一节的 GOTOOLCHAIN 协商确实生效了。

1.2.2 package 与 import 的规则

Go 的包系统有几条硬规则,第一次写代码时最容易在这里踩坑:

规则说明反例
包名与目录名解耦目录叫 foo,包名可以是 bar,但惯例保持一致目录 utils,包名 helpers
入口必须叫 mainpackage main + func main()package taskapi 无法 go run
导入必须被使用未使用的 import 是编译错误,不是警告引了 os 却没用
导入路径是模块路径标准库写 "fmt",本模块内写 "taskapi/internal/task"写相对路径 "./task"

第二条尤其值得强调:很多语言的编译器只对未使用变量给个警告,Go 则直接拒绝编译。这看起来严苛,但换来的是「能编译过 = 没有半成品代码」这一强保证。

还有一条常被忽略的细节:导入的是包,不是文件。一个目录下的所有 .go 文件同属一个包,它们共享包级标识符,不需要互相 import。所以如果你把 version 常量挪到同目录的 version.go 里,main.go 依然能直接用它。

1.2.3 go run:编译并执行

go run 是最快的试错方式,它把「编译」和「运行」两步合并,产物写进临时目录,运行完就丢弃:

GOTOOLCHAIN=go1.27.0 go run .

传参也支持,-- 之后的内容会原样交给程序:

GOTOOLCHAIN=go1.27.0 go run . add "写第一章"

关于 go run 的参数有个细节要留意:go run 自己也有 flag,所以给程序的参数最好放在包路径之后。写成 go run ./cmd/taskapi add 写第一章 是最稳的形式。

go run 的缺点是每次都要重新编译,大项目里会有明显延迟。它适合开发期快速验证,不适合当作分发方式。

1.2.4 go build:产出可执行文件

go build 只编译不运行,产物落在当前目录(或 -o 指定的位置):

GOTOOLCHAIN=go1.27.0 go build -o taskapi .
./taskapi
TaskAPI v0.1.0
built with go1.27.0

编译出来的 taskapi 是一个单文件二进制(在 Linux 上通常是静态链接,macOS 上则会动态链接系统自带的 libSystem),不依赖 Go 运行时——直接拷到另一台同架构的机器上就能跑,不需要装 Go。这正是 Go 部署体验极简的根源。

想确认二进制里刻进了哪些信息,用 go version -m:

GOTOOLCHAIN=go1.27.0 go version -m ./taskapi
./taskapi: go1.27.0
	path	taskapi
	mod	taskapi	(devel)
	build	-buildmode=exe
	build	-compiler=gc
	build	GOARCH=arm64
	build	GOOS=darwin

mod 一行显示 (devel),因为此时还没建模块(下一节才做 go mod init)。等模块建好、打了 tag,这里就会显示出具体版本号——这也是排查线上二进制「到底是哪次构建」的第一手线索。

生产构建时通常会加几个 flag:

GOTOOLCHAIN=go1.27.0 go build -trimpath -o taskapi .

-trimpath 会去掉二进制里的本地绝对路径,让构建可复现,也避免把开发机的目录结构泄露出去。更完整的交叉编译与镜像打包放到第 17 章。

1.2.5 go install:装进 GOBIN

go install 是第三种方式,它编译后把可执行文件复制到 $GOBIN(默认 $GOPATH/bin):

GOTOOLCHAIN=go1.27.0 go install .
(无输出即成功)

产物落在 $(go env GOPATH)/bin/taskapi,之后可以在任何目录直接调用 taskapi(前提是 ~/go/bin 在 PATH 里)。三种方式的区别总结如下:

方式产物位置适合场景
go run临时目录,运行后删除开发期快速验证
go build当前目录或 -o 指定处构建交付物、CI 产出
go install$GOBIN安装自己写的命令行工具

注意 go install 也能装远程包,比如 go install golang.org/x/tools/gopls@latest。这是 Go 生态分发命令行工具的标准方式。

1.2.6 os.Args:为子命令铺路

TaskAPI 最终要支持 add、list、toggle 三个子命令,而读取命令行的入口就是 os.Args。它是一个字符串切片,第 0 个元素是程序自身的路径,其后是用户输入的参数:

package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("args:", os.Args)
}
GOTOOLCHAIN=go1.27.0 go run . add "写第一章"
args: [/var/folders/.../exe/taskapi add 写第一章]

可以看到 os.Args[0] 是临时编译产物的路径(go run 的特性,go build 后就是 ./taskapi),os.Args[1] 是 add,os.Args[2] 是中文标题 写第一章——中文参数原样传递,不会乱码,这是 UTF-8 环境的默认行为。

有了 os.Args,一个最简单的命令分发器就能写出来:

if len(os.Args) < 2 {
	fmt.Println("用法: taskapi <add|list|toggle> [参数]")
	os.Exit(2)
}

switch os.Args[1] {
case "add":
	fmt.Println("新增任务:", os.Args[2])
case "list":
	fmt.Println("列出所有任务")
case "toggle":
	fmt.Println("切换任务状态")
default:
	fmt.Fprintf(os.Stderr, "未知命令: %s\n", os.Args[1])
	os.Exit(1)
}

os.Exit(2) 表示「用法错误」,os.Exit(1) 表示「运行时错误」——这是 Unix 的惯例,脚本和 CI 会据此判断失败类型。这段分发逻辑的完整版(含 for、switch 的更多形态)会在 2.2 控制流与循环 里正式展开。

1.2.7 go vet:编译之外的第二道闸

编译器只保证语法和类型正确,管不了「逻辑上可疑」的代码。go vet 补上这一层,它能抓出 Printf 占位符与参数不匹配、结构体标签写错、nil 切片误用等常见问题:

GOTOOLCHAIN=go1.27.0 go vet ./...
(无输出 = 通过)

./... 表示「当前目录及其所有子目录下的包」,这是 Go 里最常用的包通配写法,后面会反复出现。建议把 go vet 加进提交前的检查清单——它跑得飞快,却常常能拦住一整类低级 bug。

小结

  • 可执行程序的骨架是 package main + import + func main(),三件套缺一不可。
  • 未使用的 import 和变量是编译错误,Go 用这一点保证「能编译过就没有半成品」。
  • go run 编译后立即运行、产物丢弃,适合开发期;go build 产出可执行文件;go install 装进 $GOBIN。
  • go build 的产物是静态链接的单文件二进制,拷到同架构机器即可直接运行。
  • go version -m ./taskapi 能读出二进制里刻进的模块路径、构建模式与 GOOS/GOARCH。
  • os.Args[0] 是程序路径,其后是用户参数;中文参数原样传递。退出码 2 表示用法错误、1 表示运行时错误。
  • go vet ./... 是编译之外的第二道闸,./... 通配当前目录及所有子包。

程序能跑起来只是开始。现在 go build 的产物里还写着 (devel),因为我们甚至还没有模块——下一节 1.3 Go Modules 与项目骨架 会正式执行 go mod init taskapi,把 cmd/、internal/ 目录骨架搭起来,让 TaskAPI 从「一个文件」变成「一个项目」。如果你想回头确认 GOTOOLCHAIN 的行为,见 1.1 安装与工具链 。

阅读导航:上一节:1.1 安装与工具链 · 下一节:1.3 Go Modules 与项目骨架 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练