Go 语言入门:从安装环境到写出第一个可维护程序

这篇 Go 入门教程从安装、命令行、main 包、go run、go build 和代码格式化讲起,帮助初学者写出第一个真正清楚、可维护的小程序。

为什么很多人把 Go 当作第二门后端语言

学习一门新语言时,最容易被问到的问题是:它到底能帮我解决什么?如果你已经写过 PHP、JavaScript、Python 或 Java,Go 初看起来会有一点朴素。它没有复杂的继承体系,没有花哨的语法糖,也不鼓励把一个表达式写成谜语。可是这种朴素恰好是它的特点:代码容易读,程序容易部署,标准库足够扎实,并发模型也比许多传统语言轻。

Go 最常见的使用场景是后端服务、命令行工具、基础设施组件、网关、任务处理器和云原生系统。你不需要先理解一大堆框架才能运行第一个程序,也不需要为了发布一个小工具准备复杂运行时。很多 Go 程序最后只是一个二进制文件,复制到目标机器就能运行。这种直接感,对做工程的人很有吸引力。

但“简单”不等于“随便”。Go 的简单来自一些明确取舍:统一格式化、显式错误处理、组合优于继承、通过接口表达能力、用 goroutine 和 channel 处理并发。初学阶段先把这些基础动作练清楚,后面学 Web、数据库和微服务会轻松很多。

这篇文章不追求把所有语法一口气讲完,而是带你完成第一件更重要的事:把 Go 环境装好,理解一个 Go 程序如何组织、如何运行、如何构建,以及如何用 Go 的方式写出一个干净的小程序。

安装 Go 并确认环境

最稳妥的方式是从 Go 官方网站下载安装包。安装完成后,在终端执行:

go version

如果看到类似输出,就说明 Go 已经安装成功:

go version go1.20.5 darwin/arm64

版本号不必和这里完全一致。入门阶段只要使用较新的稳定版本即可。Go 的向后兼容做得比较好,大多数基础教程不会因为小版本不同而失效。

接着看两个常用环境信息:

go env GOPATH
go env GOMODCACHE

早期 Go 项目强依赖 GOPATH,现在更推荐使用 Go Modules,也就是每个项目自己有 go.mod 文件来描述模块名和依赖。你不需要把所有代码都放在某个固定目录里,只要在项目目录里初始化模块即可。

创建一个练习目录:

mkdir hello-go
cd hello-go
go mod init example.com/hello-go

example.com/hello-go 是模块名。真实项目里,它通常会写成仓库地址,例如 github.com/yourname/hello-go。练习时用 example.com 没问题,因为暂时不会发布给别人引用。

执行后会生成一个 go.mod 文件:

module example.com/hello-go

go 1.20

这就是 Go 项目的起点。它比许多语言的项目配置简单得多,但含义很重要:当前目录是一整个模块,模块里的包都属于这个项目。

第一个 main 程序

在目录里创建 main.go

package main

import "fmt"

func main() {
	fmt.Println("Hello, Go")
}

然后运行:

go run .

你会看到:

Hello, Go

这段代码很短,但已经包含 Go 程序最核心的三个概念。

第一行 package main 表示当前文件属于 main 包。Go 的代码以包为单位组织。每个 .go 文件都必须声明自己属于哪个包。包名不是装饰,它决定这段代码如何被编译和引用。

import "fmt" 表示引入标准库里的 fmt 包。fmt 负责格式化输入输出。我们用 fmt.Println 打印一行文本。

func main() 是程序入口。只有 package main 中的 main 函数才会被编译成可执行程序的入口。如果你写的是库代码,就不需要 main 函数。

Go 的入口非常明确,没有隐藏启动过程。程序从哪里开始,读者一眼能看到。

go run 和 go build 的区别

入门时经常用:

go run .

它会编译并立即运行当前模块里的 main 包。适合开发调试。

如果要生成可执行文件,使用:

go build .

当前目录会出现一个可执行文件,通常文件名与目录名相同。你可以直接运行:

./hello-go

这一步很能体现 Go 的工程风格:编译结果是一个独立二进制。部署时不需要把源码一起搬过去,也不需要目标机器安装一整套语言运行时。对命令行工具和后端服务来说,这一点非常省心。

如果想指定输出文件名:

go build -o app .

运行:

./app

当你后来写 HTTP 服务、定时任务或内部工具时,开发阶段用 go run,发布阶段用 go build,这是很自然的流程。

Go 的格式化不是建议,而是习惯

Go 有一个非常重要的工具:

gofmt -w main.go

它会自动格式化代码。Go 社区基本不会争论花括号放哪里、缩进用几个空格、导入顺序怎么排。大家把这些问题交给工具。

比如你写成这样:

package main
import "fmt"
func main(){fmt.Println("Hello, Go")}

执行 gofmt -w main.go 后,会变成:

package main

import "fmt"

func main() {
	fmt.Println("Hello, Go")
}

这不是小事。一个团队长期维护代码时,统一格式会降低阅读成本,也减少很多无意义的代码审查争论。你刚开始学 Go,就应该养成保存文件时自动格式化的习惯。

更常用的是:

go fmt ./...

它会格式化当前模块下所有 Go 包。./... 在 Go 命令里表示当前目录及所有子目录。后面你会经常看到它,比如 go test ./...

给程序加一点真实逻辑

只打印 Hello, Go 太单薄。我们写一个稍微像业务代码的版本:根据用户名生成欢迎语。

package main

import "fmt"

func greeting(name string) string {
	if name == "" {
		return "你好,陌生的 Go 学习者"
	}
	return fmt.Sprintf("你好,%s,欢迎学习 Go", name)
}

func main() {
	message := greeting("小林")
	fmt.Println(message)
}

这里出现了几个新点。

func greeting(name string) string 表示定义一个函数。它接收一个 string 类型参数 name,返回一个 string。Go 把类型写在变量名后面,这一点和 C、Java 不同。初看可能不习惯,但读多了会发现它对复杂函数签名更友好。

if name == "" 判断空字符串。Go 的 if 不需要括号,条件后面直接跟花括号。花括号必须和 if 在同一行,这是格式化工具和语言语法共同决定的。

message := greeting("小林") 使用短变量声明。:= 会根据右侧表达式推断变量类型。这里 message 的类型就是 string。短变量声明只能在函数内部使用,包级变量不能这样写。

fmt.Sprintf 不直接打印,而是返回格式化后的字符串。真实业务里,构造字符串和输出字符串通常是两件事。把它们分开,代码更容易测试和复用。

main 函数应该尽量薄一点

很多初学者会把所有逻辑都写进 main

func main() {
	name := "小林"
	if name == "" {
		fmt.Println("你好,陌生的 Go 学习者")
		return
	}
	fmt.Printf("你好,%s,欢迎学习 Go\n", name)
}

这当然能运行,但如果程序继续变大,main 很快会变成一团。更好的习惯是:main 负责启动和连接,具体逻辑放到函数里。

func buildMessage(name string) string {
	if name == "" {
		return "你好,陌生的 Go 学习者"
	}
	return fmt.Sprintf("你好,%s,欢迎学习 Go", name)
}

func main() {
	fmt.Println(buildMessage("小林"))
}

这样做的好处不是“看起来高级”,而是非常实际:函数可以单独测试,可以被其他代码调用,逻辑边界也更清楚。Go 鼓励小函数,但不鼓励把代码拆成没有意义的碎片。一个函数最好表达一个明确动作。

用测试保护第一个函数

Go 内置测试工具。创建 main_test.go

package main

import "testing"

func TestBuildMessage(t *testing.T) {
	got := buildMessage("小林")
	want := "你好,小林,欢迎学习 Go"

	if got != want {
		t.Fatalf("buildMessage() = %q, want %q", got, want)
	}
}

func TestBuildMessageWithEmptyName(t *testing.T) {
	got := buildMessage("")
	want := "你好,陌生的 Go 学习者"

	if got != want {
		t.Fatalf("buildMessage() = %q, want %q", got, want)
	}
}

运行:

go test ./...

测试函数必须以 Test 开头,参数是 *testing.T。如果结果不符合预期,可以调用 t.Fatalf 让测试失败并输出信息。

注意测试文件也写 package main,因为它要测试同一个包里的未导出函数。Go 里首字母大写的名字可以被包外访问,首字母小写的名字只能在包内使用。buildMessage 是小写开头,所以它是包内函数。

入门阶段就写测试,可能看起来有点“正式”。但 Go 的测试成本很低,工具链自带,不需要额外安装测试框架。越早习惯,越容易写出稳定程序。

常见问题:为什么我的程序找不到包

如果你在没有 go.mod 的目录里运行现代 Go 项目,可能会遇到模块相关错误。解决方式通常是回到项目根目录执行:

go mod init example.com/hello-go

另一个常见问题是文件不在同一个目录。Go 同一个目录里的 .go 文件必须属于同一个包。你不能在一个目录里同时放 package mainpackage utils。如果要拆包,需要创建子目录。

比如:

hello-go/
├── go.mod
├── main.go
└── message/
    └── message.go

message/message.go 可以这样写:

package message

import "fmt"

func Build(name string) string {
	if name == "" {
		return "你好,陌生的 Go 学习者"
	}
	return fmt.Sprintf("你好,%s,欢迎学习 Go", name)
}

main.go 引用它:

package main

import (
	"fmt"

	"example.com/hello-go/message"
)

func main() {
	fmt.Println(message.Build("小林"))
}

包名对应目录,导入路径从模块名开始。Build 首字母大写,表示它可以被其他包使用。

一个入门项目该保持什么样子

对刚开始学习 Go 的人,我建议第一个项目不要急着引入框架,也不要上来就做复杂 Web 系统。一个清楚的小项目就够了:

hello-go/
├── go.mod
├── main.go
├── main_test.go
└── message/
    ├── message.go
    └── message_test.go

你可以先练这些动作:创建模块、写函数、运行程序、格式化代码、写测试、构建二进制。它们看起来基础,却是 Go 日常开发最常用的动作。

后面你会接触变量、切片、结构体、接口、错误处理、并发和 HTTP 服务。那些内容都建立在今天这些基础之上。如果你现在能清楚回答“go rungo build 有什么区别”“为什么要有 go.mod”“package main 有什么特殊”,后面就不会迷路。

学习 Go 不需要一开始就记住所有语法。更好的路线是:写一个能运行的小程序,观察工具链如何工作,再逐步把程序变得真实。Go 的优势正在于此,它允许你从一小段清楚代码开始,一步一步长成可维护的工程。

继续阅读

探索更多技术文章

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

全部文章 返回首页