刚接触 Go 时,最有用的做法不是先背语法表,而是完整走一遍“写文件、运行、构建、读懂报错”的闭环。等这个闭环跑通,package、import、func main 这些词就不再是散落的规则,而是一套能够解释程序如何组织、如何启动的结构。
这一节会从一个只有几行的程序出发,把源码的结构、Go 工具链、包与导入、入口函数、分号规则和格式化串成一条线。示例都可以复制到本地运行;即便你今天第一次打开终端,也能跟着完成。
先确认终端能够找到 Go 工具链:
go version正常情况下会看到类似 go version go1.x.y ... 的信息。具体版本号和操作系统会不同,关键是命令能够成功执行。如果终端提示找不到 go,先完成 Go 的安装,并重新打开终端,让环境变量生效。
接着建立一个独立目录。下面的命令适用于 macOS、Linux 和 PowerShell;Windows 的传统命令提示符也可以手动创建同名文件夹。
mkdir hello-go
cd hello-go
go mod init example/hello-gogo mod init 会创建 go.mod。你可以把模块理解成一个可管理、可构建的 Go 项目边界。go.mod 记录模块路径、Go 版本以及之后可能加入的外部依赖。模块里可以有多个包,而一个包通常对应一个目录里的若干 .go 文件。
这里使用 example/hello-go 只是本地练习名称。如果代码准备发布,模块路径通常会写成代码仓库的地址。现在先不用处理依赖版本,第一步只要得到下面这样的结构:
hello-go/
└── go.mod直接运行单个 .go 文件时,有些场景并不强制要求先建模块;不过,从一开始使用模块目录更接近真实项目,也让后面的 go run .、多文件编译和依赖管理保持一致。
在目录中创建 main.go,写入以下内容:
package main
import "fmt"
func main() {
fmt.Println("你好,Go!")
}这几行代码已经构成一个完整的命令程序。Go 源文件的顶层结构有固定顺序:先是包声明,然后是导入声明,最后是函数、类型、变量和常量等顶层声明。

package main:说明这份代码属于谁每个 Go 源文件都要以包声明开头。package main 中,package 是关键字,main 是包名。同一目录里一起参与构建的普通源码文件,通常应声明同一个包名。
名为 main 的包具有特殊用途:它用来生成可执行命令。普通包则更像可复用的功能单元,供别的包导入。仅仅把函数命名为 main 还不够;想得到可直接启动的程序,包也必须是 main。
import "fmt":声明当前文件使用的依赖import 把包引入当前文件。fmt 是标准库中的格式化输入输出包。导入以后,可以通过“包名.标识符”的形式访问它导出的内容,例如 fmt.Println。
Go 对导入很严格:使用了 fmt.Println 却没有导入 fmt,编译器找不到 fmt;导入 fmt 后完全不用它,编译器又会报告未使用的导入。这个约束会及时清理已经失效的依赖。
func main():声明程序入口func 开始一条函数声明,main 是函数名,空括号表示它不接收参数,后面也没有返回值列表。花括号中的语句是函数体。
完整程序完成包初始化后,会调用 main 包中的 main 函数。main 返回时,进程结束。入口函数必须写成 func main();给它添加参数或返回值都不符合入口签名。
fmt.Println:调用另一个包中的函数Println 会把传入的值写到标准输出,并在末尾补一个换行。名称首字母大写表示它从 fmt 包导出,所以其他包能够调用它。fmt.println 并不是同一个名称,而且标准库也没有提供这个小写函数。
字符串字面量可以直接写中文。Go 源码使用 Unicode,因此这里不需要额外转义“你好”。文件仍应保存为 UTF-8,避免编辑器用其他编码破坏文本。
在 main.go 所在目录运行:
go run .终端会输出:
你好,Go!go run 会先编译指定的 main 包,再立即运行临时生成的程序。点号 . 表示当前目录中的包。这种写法会把当前包需要的源码一起纳入构建,比只写 go run main.go 更适合逐渐长大的多文件项目。
如果只想快速试一个独立文件,也可以运行:
go run main.go但要留意差异:显式写文件名时,go 命令只把列出的同目录文件合成一个临时包。假如 main.go 调用了 helper.go 中的函数,却只运行 go run main.go,就会得到“未定义”错误;go run . 会按包处理当前目录,更不容易漏文件。

go build 会留下可以重复运行的产物执行:
go build当目标是单个 main 包时,命令会编译它及其依赖,并在当前目录写出可执行文件。macOS 和 Linux 通常可以这样运行:
./hello-goWindows 会生成带 .exe 后缀的程序,可运行:
.\hello-go.exe如果想明确指定产物名,使用 -o:
go build -o hello-app .此时“构建”和“执行”是两步。源码没有变化时,可以反复运行 hello-app,不需要每次先输入 go run。发布命令行程序、放入容器或交给别人运行时,go build 的产物更合适。
go build 不会把构建结果安装到系统目录。它也会忽略以 _test.go 结尾的测试文件。对于非 main 包,构建可以用于验证代码能否通过编译,但不会在当前目录生成可执行程序。
当程序从一个文件长到多个文件时,“包”会成为最重要的组织单位。一个包由同一目录中的一个或多个 Go 源文件共同实现。包级声明在包内共享:helper.go 里声明的函数,可以被同包的 main.go 直接调用,不需要把 helper.go 当作包导入。
例如:
hello-go/
├── go.mod
├── main.go
└── greeting.gogreeting.go:
package main
func greeting(name string) string {
return "你好," + name + "!"
}main.go:
package main
import "fmt"
func main() {
fmt.Println(greeting("小林"))
}两个文件都属于 main 包,所以 main.go 能直接调用 greeting。运行 go run . 会输出 你好,小林!。

导入声明中的字符串是导入路径,例如 "fmt" 或 "example.com/tools/textutil"。代码里点号左侧通常使用包声明给出的短包名,例如 fmt.Println、textutil.Trim。
按照惯例,包名会与导入路径最后一段一致,而且使用简短的小写单词。但导入路径需要在整个依赖图里定位包,包名只负责当前源码中的引用,因此两者承担的职责不同。
同一包的声明可以跨文件共享,导入却不能。main.go 使用 fmt.Println,就必须在 main.go 自己的导入声明中包含 fmt;即使同包的 greeting.go 已经导入 fmt,也不能替它提供这个文件级名字。
多个导入通常写成分组形式:
import (
"fmt"
"strings"
)导入顺序不影响语义,gofmt 会整理它们。调用导出名称时要保留包限定,例如 strings.TrimSpace(text)。首字母小写的包级名称只在定义它的包内部可见;首字母大写的名称可以被其他包通过限定标识符使用。
不要为了暂时消除“未使用导入”而习惯性写点导入。import . 会把另一个包的导出名称直接带入当前文件,让名称归属变得模糊;import _ 只为执行包的初始化副作用,属于明确但较特殊的用法。入门阶段优先使用普通导入和清晰的包限定名。
把 main 称为“第一行执行的代码”并不完全准确。更精确的顺序是:先初始化依赖包,再初始化当前包的包级变量并执行 init 函数,最后调用 main.main。
package main
import "fmt"
var message = prepareMessage()
func prepareMessage() string {
fmt.Println("1. 初始化包级变量")
return "3. 进入 main"
}
func init() {
fmt.Println("2. 执行 init")
}
func main() {
fmt.Println
输出顺序是:
1. 初始化包级变量
2. 执行 init
3. 进入 main如果当前包导入了其他包,被导入的包要先完成初始化。同一个被多个包依赖的包只会初始化一次。每个源文件可以声明 init,一个包也可能有多个 init;它们由运行时自动调用,不能由普通代码显式调用。
入门阶段不必急着使用 init。显式初始化函数通常更容易测试,也更容易看出依赖关系。这里理解执行顺序的目的,是避免看到“main 还没开始,为什么已经打印或报错”的现象时无从判断。
main 返回以后发生什么main 函数没有返回值。它执行到右花括号,进程就结束。即使其他 goroutine 仍在运行,运行时也不会因为它们尚未完成而自动等待。这一点会在并发章节中详细展开;当前只需记住:程序生命周期由 main 控制。
Go 代码看起来很少写分号,但语法并非完全没有分号。词法分析时,编译器会在某些行尾自动插入分号。最常见的触发条件是行末为标识符、字面量、break、continue、fallthrough、return,或 ++、--、)、]、}。
这解释了为什么换行有时只影响排版,有时会改变语法。

下面的写法正确:
func main() {
fmt.Println("你好")
}下面的写法错误:
func main()
{
fmt.Println("你好")
}func main() 这一行以 ) 结束,编译器会在行尾插入分号,于是函数头被意外截断,下一行单独出现的 { 无法接在函数声明后。Go 的左花括号通常与控制结构或函数签名放在同一行,不只是风格偏好,也是分号插入规则的结果。
运算符附近也要留意换行。这样写可以:
total := first +
second因为第一行以 + 结束,不会自动插入分号。反过来写会把第一行截断:
total := first
+ second实际写代码时不需要逐条模拟词法规则。更有效的习惯是遵循常见排版,让 gofmt 接管格式;遇到意外语法错误时,再用“行尾是否会插入分号”解释它。
Go 把统一格式交给工具,而不是让每个项目维护一套缩进争论。gofmt 会按标准规则重写源码,使用制表符缩进并整理对齐。对一个文件直接写回:
gofmt -w main.go如果只想预览格式化结果,不覆盖文件,可以省略 -w:
gofmt main.go在模块或包目录中,更常用的是:
go fmt ./...go fmt 会对指定包运行格式化;./... 表示当前模块目录及其子目录中的包。只处理当前包时,运行 go fmt 或 go fmt . 即可。

假设源码被随手写成:
package main
import("strings";"fmt")
func main(){name:=strings.TrimSpace(" 小林 ");fmt.Println("你好,",name)}gofmt 会整理成:
package main
import (
"fmt"
"strings"
)
func main() {
name := strings.TrimSpace(" 小林 ")
fmt.Println("你好,", name)
}它处理缩进、空格、换行和导入排序,但不会替你判断业务逻辑是否正确。标准库里的 gofmt 也不会自动新增或删除导入;编辑器常用的“组织导入”通常由 gopls 或 goimports 完成。
Go 的报错通常会给出文件、行号和问题摘要。不要一看到英文就从头重写,先读第一条错误,再定位到对应行。后面的错误有时只是第一处语法问题引起的连锁反应。

expected 'package', found ...检查文件开头。除注释和空白外,源文件应先出现 package 声明。不要把 import 或 func main 放在包声明前。
undefined: fmt代码写了 fmt.Println,但当前文件没有 import "fmt",或者把包名拼错了。导入是文件级声明,不能依赖同包的另一个文件替你导入。
"fmt" imported and not used当前文件导入了 fmt,却没有通过 fmt. 使用任何内容。删除导入,或者恢复真正需要它的代码。不要用无意义调用掩盖问题。
function main is undeclared in the main package正在构建 main 包,但包里没有合法的 func main()。检查函数名大小写、参数、返回值和文件是否真的参与当前构建。
found packages ... in the same directory同一目录的普通构建文件声明了不同包名。决定这个目录究竟实现哪个包,然后统一包声明;如果它们本就属于不同包,应移动到不同目录。
syntax error: unexpected newline, expected {常见原因是把左花括号放到了下一行。把它移到函数签名、if、for 或 switch 所在行末,再运行 gofmt。
go run . 中的点号就是“当前目录”。如果终端没有进入包含 go.mod 和源码的目录,go 命令自然找不到预期的包。可以先运行 pwd(Windows PowerShell 可运行 Get-Location)和 ls(Windows 也可用 dir)确认位置。
一个稳定的排错循环是:保存文件 → 运行 gofmt → 执行 go run . → 只处理第一条报错 → 再运行。这样每次只改变一个变量,错误会比同时修改多处更容易定位。
现在把知识收束成一条真正可执行的路径。目标是写一个程序,清理用户输入中的首尾空白,再输出问候语。
新建空目录并进入目录,运行 go mod init example/greeting。确认生成了 go.mod,再创建 main.go。
在 main.go 中声明 package main,分组导入 fmt 和 strings,再声明无参数、无返回值的 func main()。
在函数体中写 name := strings.TrimSpace(" 小林 "),然后用 fmt.Println("你好," + name + "!") 输出结果。
你可以先独立完成,再展开参考实现。
到这里,我们已经不只是“打印了一句话”,而是跑通了 Go 项目的最小闭环:模块确定项目边界,包组织源码,导入连接依赖,main 提供入口,工具链负责格式化、运行和构建。下一步学习声明、变量和控制流程时,这套骨架会一直保留。
运行 gofmt -w main.go,再执行 go run .。确认输出是 你好,小林!,并且姓名两侧没有多余空格。
最后运行 go build -o greeting .。macOS 或 Linux 用 ./greeting 运行;Windows 用 .\greeting.exe。两次输出应一致。