第一个 Go 程序
Hello World 程序
让我们从一个最经典的 Go 程序开始:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}运行这段代码:
# 方法一:使用 go run 直接运行
go run main.go
# 输出:
# Hello, World!这短短 5 行代码已经包含了 Go 程序的几个核心要素。让我们逐行解析。
程序结构详解
第 1 行:package main
Go 程序由包(package)组成。每个 Go 文件必须属于一个包,包名声明在文件的第一行。package main 是一个特殊的包名,表示这是一个可执行程序。Go 编译器会将 package main 中的 main() 函数作为程序的入口点。
package main // 声明当前文件属于 main 包关于包的要点:
- 每个目录下的所有
.go文件必须属于同一个包 package main是可执行程序的入口包,必须有main()函数- 其他包名(如
package utils)是库包,不能直接运行 - 包名通常使用小写字母,不使用下划线或驼峰命名
第 2-3 行:import "fmt"
import "fmt" // 导入标准库中的 fmt 包import 语句用于导入其他包,使其功能可以在当前文件中使用。fmt 是 Go 标准库中提供格式化 I/O 功能的包,包含 Println、Printf、Sprintf 等常用函数。
导入多个包的方式:
// 方式一:逐行导入
import "fmt"
import "os"
import "strings"
// 方式二:分组导入(推荐)
import (
"fmt"
"os"
"strings"
)
// 方式三:带别名的导入
import (
"fmt"
f "fmt" // 给 fmt 起别名 f
_ "image/png" // 只执行 init 函数,不直接使用
. "math" // 不需要包名前缀,直接使用 Sin、Cos 等
)Go 的导入规则
Go 语言有一个严格的规则:导入的包必须被使用,否则编译器会报错。这不是建议,而是强制要求。这个设计是为了保持代码的整洁性,避免无用的依赖堆积。
第 4-6 行:func main()
func main() { // 定义 main 函数,程序入口
fmt.Println("Hello, World!") // 调用 fmt 包的 Println 函数
}main() 函数是 Go 可执行程序的入口点。它必须定义在 package main 中,不接受任何参数,也没有返回值。当程序运行时,main() 函数是第一个被执行的代码。
main 函数的特点:
- 无参数、无返回值
- 必须在
package main中定义 - 一个可执行程序只能有一个
main()函数 main()函数执行完毕后,程序退出
fmt 包常用函数
fmt 是 Go 中最常用的标准库包之一,提供了丰富的格式化输出功能。
Println、Print、Printf
package main
import "fmt"
func main() {
// Println:打印内容后换行,多个参数之间自动添加空格
fmt.Println("Hello", "World", 2024)
// 输出:Hello World 2024
// Print:打印内容,不自动换行
fmt.Print("Hello ")
fmt.Print("World\n")
// 输出:Hello World
// Printf:格式化输出(类似 C 语言的 printf)
name := "Go"
version := 1.23
fmt.Printf("Language: %s, Version: %.2f\n", name, version)
// 输出:Language: Go, Version: 1.23
// Sprint / Sprintf:返回字符串而不是打印
s := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)
fmt.Println(s)
// 输出:Name: Alice, Age: 30
}常用格式化动词
| 动词 | 说明 | 示例 | 输出 |
|---|---|---|---|
%s | 字符串 | fmt.Printf("%s", "hello") | hello |
%d | 十进制整数 | fmt.Printf("%d", 42) | 42 |
%f | 浮点数 | fmt.Printf("%f", 3.14) | 3.140000 |
%.2f | 保留 2 位小数 | fmt.Printf("%.2f", 3.14) | 3.14 |
%t | 布尔值 | fmt.Printf("%t", true) | true |
%v | 默认格式 | fmt.Printf("%v", []int{1, 2}) | [1 2] |
%T | 类型 | fmt.Printf("%T", 42) | int |
%p | 指针地址 | fmt.Printf("%p", &x) | 0x... |
%q | 带引号的字符串 | fmt.Printf("%q", "hi") | "hi" |
\n | 换行 | fmt.Printf("a\nb") | a(换行)b |
\t | 制表符 | fmt.Printf("a\tb") | a b |
go run 与 go build
go run:编译并运行
go run 命令会在临时目录中编译你的程序,然后立即执行,执行完毕后删除临时文件。
go run main.go特点:
- 适合开发调试阶段快速测试
- 不会生成可执行文件
- 编译和运行一步完成
- 可以同时指定多个文件:
go run file1.go file2.go - 也可以运行整个目录:
go run .
go build:编译生成可执行文件
go build 命令编译你的程序,生成一个可执行文件(但不运行它)。
# 基本编译(在当前目录生成可执行文件)
go build
# 指定输出文件名
go build -o myapp
# 指定要编译的文件
go build main.go
# 编译并优化(减小文件大小)
go build -ldflags="-s -w"
# 交叉编译
GOOS=linux GOARCH=amd64 go build -o myapp-linux特点:
- 生成可执行文件,方便分发和部署
- 默认输出文件名与目录名相同(或使用
-o指定) - 可以通过
-ldflags参数注入版本信息、减小文件大小 - 适合生产环境部署
对比总结
| 特性 | go run | go build |
|---|---|---|
| 是否编译 | 是 | 是 |
| 是否运行 | 是 | 否 |
| 是否生成可执行文件 | 否(临时编译后删除) | 是 |
| 适用场景 | 开发调试 | 生产部署 |
| 可指定输出文件名 | 不适用 | -o 参数 |
| 速度 | 每次都要编译 | 编译一次,多次运行 |
进阶命令
go install 类似于 go build,但会将编译后的可执行文件安装到 $GOPATH/bin(或 $GOBIN)目录,这样你可以在任何地方直接运行该命令。
gofmt 与代码格式化
gofmt 是 Go 语言自带的代码格式化工具,它会自动将 Go 代码格式化为统一的风格。go fmt 命令是 gofmt 的包装,提供了更简洁的用法。
为什么需要格式化?
Go 团队认为,代码风格的一致性比个人偏好更重要。通过强制统一的代码风格:
- 减少团队中关于代码格式的争论
- 提高代码可读性
- 让 code review 更专注于逻辑而非格式
使用 gofmt
# 格式化单个文件(直接输出到终端)
gofmt main.go
# 格式化并覆盖原文件
gofmt -w main.go
# 格式化整个项目
gofmt -w .
# 显示哪些文件需要格式化(但不实际格式化)
gofmt -l .使用 go fmt
# 格式化当前包下的所有 .go 文件
go fmt
# 格式化指定文件
go fmt main.go
# 格式化所有子包
go fmt ./...最佳实践
在 VS Code 中配置 "editor.formatOnSave": true,保存文件时自动格式化代码。这是 Go 开发的标准做法,确保代码始终保持统一风格。
Go 代码风格要点
以下是一些 Go 代码的风格约定,gofmt 会自动帮你处理:
package main
import (
"fmt"
"math"
)
// 使用 Tab 缩进(不是空格)
// 左大括号不换行
func add(a, b int) int {
return a + b
}
// 多行导入使用分组形式
// 导入的包按顺序排列:标准库、第三方库、本地包
import (
"fmt" // 标准库
"github.com/gin-gonic/gin" // 第三方库
"myproject/internal/utils" // 本地包
)
// 变量命名使用驼峰命名法
var userName string
var maxCount int
var isReady bool
// 导出标识符(首字母大写)可以被外部包访问
// 未导出标识符(首字母小写)只能在包内访问
// 错误处理不使用异常,而是返回 error
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// 字符串使用双引号
s := "hello world"
// 字符使用单引号
c := 'A'一个更完整的示例
让我们写一个稍微复杂一点的 Go 程序,综合运用前面学到的知识:
package main
import (
"fmt"
"os"
"strings"
)
// 程序版本信息
const version = "1.0.0"
func main() {
// 获取命令行参数
args := os.Args
// 显示帮助信息
if len(args) < 2 {
printUsage()
return
}
// 根据参数执行不同操作
switch strings.ToLower(args[1]) {
case "hello":
name := "World"
if len(args) > 2 {
name = args[2]
}
fmt.Printf("Hello, %s! 👋\n", name)
case "version":
fmt.Printf("App version: %s\n", version)
default:
fmt.Printf("Unknown command: %s\n", args[1])
printUsage()
}
}
func printUsage() {
fmt.Println("Usage:")
fmt.Println(" myapp hello [name] - Say hello")
fmt.Println(" myapp version - Show version")
}运行示例:
# 编译
go build -o myapp
# 运行
./myapp
# Usage:
# myapp hello [name] - Say hello
# myapp version - Show version
./myapp hello
# Hello, World! 👋
./myapp hello Go
# Hello, Go! 👋
./myapp version
# App version: 1.0.0注释
Go 支持两种注释方式:
package main
import "fmt"
// 这是单行注释
// 注释不会被执行
/*
这是多行注释(块注释)
可以跨越多行
通常用于函数或包的文档说明
*/
/*
Package main 演示了 Go 中注释的用法。
在 Go 中,包级别的注释(紧跟 package 声明之前)
会被 godoc 工具提取为包文档。
*/
func main() {
// 行内注释:解释代码的功能
fmt.Println("Comments in Go") // 行尾注释
}godoc 文档
Go 的文档工具 godoc 会提取包声明和顶层声明前的注释作为文档。因此,每个导出的函数、类型、变量都应该有注释说明。注释应该以被注释对象的名称开头:
// Add 返回两个整数的和。
func Add(a, b int) int {
return a + b
}
// User 表示一个系统用户。
type User struct {
Name string
Age int
}练习题
练习 1
编写一个 Go 程序,使用 fmt.Printf 打印你的名字、年龄和爱好,使用不同的格式化动词。
package main
import "fmt"
func main() {
name := "张三"
age := 25
hobby := "编程"
// 使用不同的格式化动词
fmt.Printf("姓名: %s\n", name)
fmt.Printf("年龄: %d\n", age)
fmt.Printf("爱好: %s\n", hobby)
// 使用 Printf 一次打印
fmt.Printf("\n我是 %s,今年 %d 岁,爱好是 %s。\n", name, age, hobby)
// 使用 %T 查看类型
fmt.Printf("\nname 的类型: %T\n", name)
fmt.Printf("age 的类型: %T\n", age)
}运行结果:
姓名: 张三
年龄: 25
爱好: 编程
我是 张三,今年 25 岁,爱好是 编程。
name 的类型: string
age 的类型: int练习 2
以下代码有什么问题?请找出并修正:
package main
import "fmt"
import "os"
func main() {
fmt.Println(os.Args)
}这段代码本身没有语法错误,但有两个风格问题:
- 导入语句应该使用分组形式:
import (
"fmt"
"os"
)- 修正后的完整代码:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println(os.Args)
}gofmt 工具会自动将逐行导入的语句格式化为分组形式。
练习 3
请解释 go run 和 go build 的区别。在什么情况下你会使用 go run?在什么情况下使用 go build?
| 方面 | go run | go build |
|---|---|---|
| 编译 | 在临时目录编译 | 在当前目录编译 |
| 运行 | 编译后自动运行 | 只编译不运行 |
| 输出文件 | 不生成可执行文件 | 生成可执行文件 |
使用场景:
- 使用
go run:开发调试阶段,快速验证代码逻辑,不需要保留编译产物。 - 使用
go build:需要生成可执行文件进行部署或分发时;需要通过-ldflags注入版本信息时;需要交叉编译生成其他平台的二进制文件时。
练习 4
编写一个 Go 程序,实现一个简易的计算器:接收两个整数和一个运算符(+、-、*、/),输出运算结果。使用 fmt.Printf 进行格式化输出。
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
// 检查参数数量
if len(os.Args) != 4 {
fmt.Println("Usage: calculator <num1> <operator> <num2>")
fmt.Println("Example: calculator 10 + 3")
return
}
// 解析参数
num1, err1 := strconv.Atoi(os.Args[1])
num2, err2 := strconv.Atoi(os.Args[3])
operator := os.Args[2]
if err1 != nil || err2 != nil {
fmt.Println("Error: 请输入有效的整数")
return
}
// 执行运算
switch operator {
case "+":
fmt.Printf("%d + %d = %d\n", num1, num2, num1+num2)
case "-":
fmt.Printf("%d - %d = %d\n", num1, num2, num1-num2)
case "*":
fmt.Printf("%d * %d = %d\n", num1, num2, num1*num2)
case "/":
if num2 == 0 {
fmt.Println("Error: 除数不能为零")
return
}
fmt.Printf("%d / %d = %d\n", num1, num2, num1/num2)
default:
fmt.Printf("Error: 不支持的运算符 '%s'\n", operator)
}
}运行示例:
go run calculator.go 10 + 3
# 10 + 3 = 13
go run calculator.go 20 / 4
# 20 / 4 = 5
go run calculator.go 7 * 8
# 7 * 8 = 56练习 5
以下 Go 代码能编译通过吗?为什么?
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println("Hello")
}不能编译通过。 编译器会报类似以下错误:
imported and not used: "math"Go 语言有一个严格的规定:导入的包必须被使用。 math 包虽然被导入了,但在代码中没有任何地方引用它。这是一个编译错误,不是警告。
修复方法: 删除未使用的导入:
package main
import "fmt"
func main() {
fmt.Println("Hello")
}