目录

Go 程序结构

Go 程序有着清晰、规范的组织结构。理解程序的基本构成单元——包声明、导入、函数、变量作用域等——是编写 Go 代码的基础。本篇将详细讲解 Go 程序的各个组成部分,帮助你建立良好的代码组织习惯。

包声明

每个 Go 源文件都必须以 package 声明开头,表明该文件属于哪个包。

package main

包的分类

  • main:可执行程序的入口包,必须包含 main() 函数。
  • 库包:用于提供可复用的功能,包名通常与目录名一致。
// 文件: mathutil/add.go
package mathutil

func Add(a, b int) int {
    return a + b
}

一个包可以包含多个源文件,它们共享同一个包命名空间:

// 文件: mathutil/subtract.go
package mathutil

func Subtract(a, b int) int {
    return a - b
}

导入包

单导入

导入单个包时使用圆括号或直接导入:

import "fmt"

分组导入

导入多个包时,推荐使用分组导入的写法:

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

Go 官方工具 goimports 会自动将导入分为三组:标准库、第三方包、项目内部包。

匿名导入 _

有时候我们只需要某个包的初始化副作用(init 函数),而不直接使用它。这时可以使用匿名导入:

import (
    "fmt"
    _ "image/png" // 注册 PNG 解码器,不使用 png 包中的任何标识符
)

func main() {
    fmt.Println("PNG 解码器已注册")
}

导入别名

当两个包的名称冲突时,可以为导入指定别名:

import (
    crand "crypto/rand"
    mrand "math/rand"
)

func main() {
    _ = crand.Reader
    _ = mrand.Int
}

未使用的导入会导致编译错误

Go 编译器严格要求:导入但未使用的包会导致编译失败。这是 Go 的设计哲学之一——保持代码整洁。

package main

import (
    "fmt"
    "os" // 编译错误:os 包已导入但未使用
)

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

函数

main 函数

main 函数是程序的入口,它没有参数也没有返回值:

package main

import "fmt"

func main() {
    fmt.Println("程序从这里开始执行")
}

导出与非导出

Go 通过首字母大小写来控制标识符的可见性:

  • 首字母大写:导出(相当于 public),其他包可以访问。
  • 首字母小写:非导出(相当于 private),仅限本包内使用。
package calculator

// Add 是导出函数,其他包可以调用
func Add(a, b int) int {
    return a + b
}

// multiply 是非导出函数,仅限本包使用
func multiply(a, b int) int {
    return a * b
}

同样的规则适用于变量、常量、结构体、接口等所有标识符。

变量作用域

包级变量

在函数外部声明的变量是包级变量,作用域为整个包:

package main

import "fmt"

// 包级变量,所有函数都可以访问
var version = "1.0.0"
var debug = false

func main() {
    fmt.Println("版本:", version)
    fmt.Println("调试模式:", debug)
}

函数内变量

在函数内部声明的变量只能在函数内使用:

package main

import "fmt"

var globalVar = "我是全局变量"

func main() {
    localVar := "我是局部变量"
    fmt.Println(globalVar) // 可以访问
    fmt.Println(localVar)  // 可以访问
    helper()
}

func helper() {
    fmt.Println(globalVar) // 可以访问全局变量
    // fmt.Println(localVar) // 编译错误:localVar 未定义
}

代码块作用域

iffor 等代码块中声明的变量,仅在该代码块内可见:

package main

import "fmt"

func main() {
    x := 10

    if x > 5 {
        y := 20 // y 仅在 if 代码块内可见
        fmt.Println("if 内部:", x, y)
    }

    fmt.Println("if 外部:", x)
    // fmt.Println(y) // 编译错误:y 未定义
}

语句与表达式

Go 的语句有一些独特的规则:

不需要分号

Go 编译器会自动在行尾添加分号,开发者不需要手动写分号:

// 不需要写分号
x := 10
fmt.Println(x)

左大括号不能换行

Go 使用分号自动插入规则,左大括号 { 必须和语句在同一行:

// 正确写法
func main() {
    if x > 5 {
        fmt.Println("大于5")
    }
}

// 错误写法 —— 编译失败
// func main()
// {
//     if x > 5
//     {
//         fmt.Println("大于5")
//     }
// }

代码组织最佳实践

一个文件一个主要概念

每个源文件应聚焦于一个功能模块,文件名应反映其内容:

myproject/
├── main.go          # 程序入口
├── handler.go       # HTTP 处理函数
├── model.go         # 数据模型定义
├── service.go       # 业务逻辑
└── utils.go         # 工具函数

init 函数

每个 Go 源文件可以包含一个或多个 init 函数,它在 main 函数之前自动执行,常用于初始化:

package main

import "fmt"

var config map[string]string

func init() {
    config = make(map[string]string)
    config["db_host"] = "localhost"
    config["db_port"] = "3306"
    fmt.Println("配置初始化完成")
}

func main() {
    fmt.Println("数据库地址:", config["db_host"])
    fmt.Println("数据库端口:", config["db_port"])
}

执行顺序:包级变量初始化 → init() 函数 → main() 函数。

完整小程序示例

下面是一个完整的命令行计算器程序,展示了包声明、导入、函数定义和变量作用域的综合运用:

package main

import (
    "fmt"
    "os"
    "strconv"
)

// 版本信息(包级常量)
const version = "1.0.0"

func main() {
    if len(os.Args) != 4 {
        fmt.Println("用法: calculator <数字1> <运算符> <数字2>")
        fmt.Println("支持的运算符: + - * /")
        return
    }

    a, err1 := strconv.ParseFloat(os.Args[1], 64)
    op := os.Args[2]
    b, err2 := strconv.ParseFloat(os.Args[3], 64)

    if err1 != nil || err2 != nil {
        fmt.Println("错误: 请输入有效的数字")
        return
    }

    result, err := calculate(a, op, b)
    if err != nil {
        fmt.Println("错误:", err)
        return
    }

    fmt.Printf("%.2f %s %.2f = %.2f\n", a, op, b, result)
}

func calculate(a float64, op string, b float64) (float64, error) {
    switch op {
    case "+":
        return a + b, nil
    case "-":
        return a - b, nil
    case "*":
        return a * b, nil
    case "/":
        if b == 0 {
            return 0, fmt.Errorf("除数不能为零")
        }
        return a / b, nil
    default:
        return 0, fmt.Errorf("不支持的运算符: %s", op)
    }
}

运行示例:

$ go run main.go 10 + 3
10.00 + 3.00 = 13.00

$ go run main.go 10 / 0
错误: 除数不能为零

总结

本篇详细介绍了 Go 程序的基本结构:包声明决定了代码的归属,导入语句管理依赖关系,函数是代码的执行单元,而命名规则(首字母大小写)控制了标识符的可见性。我们还学习了变量作用域的层级关系、Go 独特的语句规则(自动分号、大括号位置),以及 init 函数的初始化机制。良好的代码组织习惯是编写可维护 Go 程序的第一步。