Go 语言程序结构
项目
使用 Go Modules 管理的项目,其结构可以相对简单且灵活,一切根据项目需求进行调整。
源码分类
Go 语言源代码根据用途可分为几类:
- 命令源文件:包含
main
函数,属于package main
的代码文件,是程序入口点。通常存放在项目根目录下或cmd
目录下子目录中,可用go build
或go run
命令编译为可执行程序。 - 库源文件:库文件指被设计为被外部导入重用的代码文件。库不包含
main
函数,而是提供函数、类型、变量等供其他程序使用。通常位于pkg
目录下。 - 测试源文件:测试代码与被测试代码位于同一个目录,文件名以
_test.go
结尾。测试函数以Test
开头,使用内置测试框架来编写测试用例。 - 内部源文件:内部源文件指放置在
internal
目录下的代码文件。作用和库文件一样,只不过作用域限于项目内部。 - 辅助工具:包括用于支持开发、构建、部署和维护的辅助脚本和工具。可能存到
tools
目录中。
源文件默认使用 UTF-8
编码。
基本文件
项目根目录下 go.mod
和 main.go
文件,是一个可执行项目的最基础配置:
-
go.mod
: 项目核心文件,记录项目模块路径以及依赖关系。由go mod init
命令自动生成,之后下载依赖包时会自动更新内容。 -
main.go
:程序入口文件,包含main
包和main
函数。
main.go
文件也可叫其他名字,但不推荐改名。
常用结构
关于项目必备目录,Go 语言没有强制要求和指导说明,下面是较常见的目录结构:
cmd
:用于存放项目可执行文件入口点。如果项目生成多个二进制文件,可以用子目录划分不同应用程序。例如cmd/api/main.go
和cmd/cli/main.go
,编译后会生成api.exe
和cli.exe
。pkg
:用于存放可导出的库代码。internal
:存放不可导出的应用代码和库代码,只能项目内使用。config
:存放配置文件模板或默认配置。assets
:存放项目所需的媒体文件。scripts
:存放外部脚本和工具。test
:存放外部测试文件。api
:存放开放 API 的定义,例如 OpenAPI/Swagger 规格。docs
:文档目录,存放用户手册、开发者指南和 API 文档。
规范参考来源于:https://github.com/golang-standards/project-layout
项目示范
下面通过新建一个简单项目,来演示模块和包。
首先新建目录 awkgo
,代表项目名称。在 awkgo
路径下中初始化项目:
PS D:\Software\Programming\Go\awkgo> go mod init awkgo
go: creating new go.mod: module awkgo
go: to add module requirements and sums:
go mod tidy
这里仅作演示,所以没有初始化成一个代码仓库路径。继续在 awkgo
目录下新建 echo
目录,代表有一个本地包。在 echo
目录内新建文件 text.go
,声明属于 echo
包,内容只有一个函数 ToTitle
,用来转换字符串:
/*
Package echo 包最顶部说明
调用扩展库中实验功能
*/
package echo
import (
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// ToTitle 用于转换英语单词为首字母大小
func ToTitle(s string) string {
c := cases.Title(language.English)
return c.String(s)
}
代码中导入了外部包 golang.org/x/text
,需要单独下载:
PS D:\Software\Programming\Go\awkgo> go get golang.org/x/text
go: downloading golang.org/x/text v0.16.0
go: added golang.org/x/text v0.16.0
下载完毕后,go.mod
文件内容会自动更新,不需要手动修改:
module awkgo
go 1.22.0
require golang.org/x/text v0.16.0 // indirect
然后在 awkgo
目录新建文件 main.go
作为程序入口,内容如下:
package main
import (
"awkgo/echo" // 导入本地包
"fmt"
)
func main() {
fmt.Println(echo.ToTitle("hello world = 世界你好"))
}
整个项目文件结构应该像下面这样,包内文件 text.go
命名没有规定,只要保证包名和目录名一致即可:
awkgo/
├── echo/
│ └── text.go
├── go.mod
├── main.go
最后在项目路径下,使用 go run
或 go build
命令来编译运行项目:
PS D:\Software\Programming\Go\awkgo> go run .
Hello World = 世界你好
PS D:\Software\Programming\Go\awkgo> go build
PS D:\Software\Programming\Go\awkgo> .\awkgo.exe
Hello World = 世界你好
模块
Go 模块(Module)在 Go 1.11 版本中引入,是 Go 语言包管理和依赖管理的基础。模块可以视为一组包的集合,通过 go.mod
文件来记录管理依赖:
- 模块:由一个根目录、一个
go.mod
文件以及多个 Go 包组成。 - go.mod 文件:在模块根目录中,描述了模块属性、依赖项及其版本。
简单来看,任何包含 go.mod
文件的代码目录都可以被称为模块。
初始化模块
在创建完项目目录后,第一件事就是使用 go mod init
命令来初始化新模块:
go mod init <module-name>
其中 module-name
为模块名字,一般用项目代码仓库路径,例如 :github.com/hxz393/projectname
,这样能保证模块名独一无二。
初始化完成后,会在项目根目录自动创建 go.mod
文件。
go.mod
go.mod
文件是 Go 模块的核心,一个示例文件内容如下:
module github.com/hxz393/myproject
go 1.22.0
require (
github.com/gin-gonic/gin v1.6.3
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
)
replace (
github.com/gin-gonic/gin v1.6.3 => github.com/gin-gonic/gin v1.8.0
github.com/mohae/deepcopy => ../myproject/deepcopy
)
exclude (
github.com/gin-gonic/gin v1.7.0
)
retract (
v0.0.1 // 不再支持
v0.2.3 // 有安全问题
)
其中必备字段会自动生成和更新:
module
:声明模块名,也就是使用go mod init
命令时输入的模块路径。go
:指定模块使用的 Go 语言版本,编译时不得低于指定版本。require
:列出依赖及其版本号。如果没有版本号,由仓库提交号代替。
可选字段需要手动添加,用于特殊目的:
replace
:替换依赖版本或替换为其他依赖。exclude
:指定排除特定依赖版本,避免代码缺陷或兼容问题。retract
:标记本模块的特定版本为弃用。
一般 go.sum
文件和go.mod
文件成对出现,用于记录依赖版本哈希值,确保依赖未被非法篡改或损坏。
添加依赖
从 Go 1.17 版本开始,go get
命令用于添加和更新外部依赖,不再用来安装工具:
go get <module-name>[@version]
module-name
:和初始化模块时一样,module-name
为依赖仓库路径,除了 Git 也支持 Mercurial 和 Subversion 等版本控制系统。version
:版本号类似于vX.Y.Z
格式。如果忽略则下载最新版本,否则会安装指定版本。也用于降级依赖版本。
依赖在本地存放路径由 GOMODCACHE
环境变量决定,如果本地已存在依赖缓存,则不会重新下载安装。
go get
命令支持一些选项和参数:
-d
:只下载不安装。-insecure
:允许使用 HTTP 协议下载,用于在内部仓库中下载依赖。-u
:更新依赖包。-v
:打印详细信息。
比较常用的是 -u
更新选项,新增和更新依赖都会自动更新 go.mod
和 go.sum
文件。但升级模块时需要注意,一般不同主版本号会使用不同路径,表示新版本包含不向后兼容的更改。例如 cloud.google.com/go
大版本升级后地址变为 cloud.google.com/go/v2
,在更新依赖和导入包时都需要调整地址。
管理模块
大多数时候,模块不需要手动管理,只需用 go get
命令来指定依赖就够了。少数情况下,可以使用 go mod
子命令来管理模块:
- 清理依赖:运行
go mod tidy
命令可以清理未使用的依赖。只会优化go.mod
文件内容,不会删除实际依赖文件。 - 下载依赖:运行
go mod download
来一键下载go.mod
中列出的所有依赖。 - 验证依赖:运行
go mod verify
验证当前依赖文件是否都符合哈希值。
如果要查看当前模块所有依赖,包括依赖的依赖,可以使用 go list
命令列出:
PS D:\Software\Programming\Go\new> go list -m all
new
cloud.google.com/go v0.26.0
filippo.io/edwards25519 v1.1.0
github.com/BurntSushi/toml v1.4.0
包
Go 语言以包(Package)来封装、组织和重用代码,一个包就是一个目录,里面包含属于这个包的 .go
源文件。一个包结构通常包括三部分:包声明、包引入和包内容。
Go 语言与大部分编译语言类似,当改动源文件时,必须重新编译该源文件及依赖包。但和其他语言比较,编译速度快得多,是以下特性在起作用:
- 每个源文件在开头显式地列出所有依赖包,编译器可以快速读取依赖包列表。
- 包之间禁止循环依赖,所以包可以被单独编译,也支持并行编译。
- 每个包在编译后会缓存结果,当代码没变化时,编译器能重用之前缓存。
包声明
所有源文件必须在开头显式声明所属包,包声明格式如下:
package <pkgName>
包名 pkgName
使用小写形式单词命名,每个 Go 文件都属于且仅属于一个包。一个包可以由多个 Go 文件组成,一个应用程序可以由多个包组成。
属于同个包的源文件必须被一起编译,所以每个目录只能代表一个包。也不能把同个包的文件拆分到多个目录中,这样做编译器会强制拆分包。例如在 config
和 hello
目录中都有 package hello
声明,那么在其他包中导入时,要作为不同包对待:
package main
import (
"fmt"
// 只当是同名包,导入需要用别名
hello1 "new/internal/config"
hello2 "new/internal/hello"
)
func main() {
hello2.Sum([]int{1, 3, 4})
fmt.Print(hello1.MaxConnections)
}
包导入
无论需要调用标准库包、本地包还是外部包,都要使用导入来使用。Go 语言中,使用关键字 import
来导入包中公开变量、常量和函数。导入包有三种模式:
- 正常模式:
import <pkgName>
。导入pkgName
之后,使用<pkgName>.<funcName>
形式对包中函数或类型进行调用。 - 别名模式:
import aliasName <pkgName>
。导入包时,可能遇到包名相同或相近的情况,此时可以拟定包别名来进行区别。注意,一旦定义包别名,就不能再使用包原名来调用。 - 简便模式:
import .<pkgName>
。在简便模式中,可直接使用包内函数名进行调用,而不用带上包名。通常不会使用此导入方式。
所有外部包导入路径以托管域名为前缀,例如:github.com/go-sql-driver/mysql
,包名匹配包导入路径的最后一段。本地包则通过 path/<pkgName>
来导入,计算相对路径要包括工作目录,路径分隔符统一使用斜杠 /
。
有多个导入包时,可以使用导入块来包装,并通过空行分隔进行分组。导入分组用来区分包来源是标准库、内部库还是第三方库:
package main
// 导入块形式
import (
// 标准库
"fmt"
"runtime"
// 项目内部模块
"myproject/models"
tools "myproject/utils"
// 第三方库
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
)
func main() {
}
Go 语言中不允许导入包而不用,但可以将下划线 _
作为包别名来绕过匿名导入检查。这样做的唯一目的是触发包内初始化函数,初始化函数可能用于注册驱动或环境检查:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq" // 注册 PostgreSQL 驱动
)
func main() {
db, _ := sql.Open("postgres", "192.168.2.1")
db.Close()
}
不同包之间允许存在同名函数,调用时通过带上不同包名来消除歧义:
package main
import (
"new/internal/config"
"new/internal/hello"
)
func main() {
// 同名函数,接受不同参数。调用带上包名
fmt.Print(hello.Sum([]int{1, 3, 4}))
fmt.Print(config.Sum(1,2))
}
包内容
在包导入之后就是包内容,包含实际功能和数据结构,通常是些变量、函数、类型和方法:
package cli
import (
"fmt"
"runtime"
)
// 全局变量最先计算
var MaxUser = runtime.NumCPU() * Multiple
var Multiple = 100
// 初始化函数依次执行
func init() { fmt.Println("可支持最大用户:\t", MaxUser) }
func init() { fmt.Println("操作系统:\t", runtime.GOOS) }
// Client 为可导出函数
func Client(max int) {
fmt.Println("注册用户数:\t", max)
if !check(max) {
fmt.Println("无法注册新用户")
return
}
fmt.Println("继续正常注册流程")
}
// 不可导出函数,注释不必以函数名开头
func check(max int) bool {
if max <= 0 || max > MaxUser {
fmt.Println("超出可支持最大用户数")
return false
}
return true
}