刚开始学 Go 的时候,很多人会卡在几个看似简单的问题上:
- 文件名必须和包名一样吗?
import后面的路径到底从哪里来?- 为什么
example.com/package-demo/tools里用的却是toolbox.Label()? - 为什么
Hello可以被别的包访问,defaultEnglishPrefix不可以?
这些问题的答案,其实可以压缩成四句话:
目录决定包的位置
package 声明决定包名
go.mod 决定导入路径前缀
首字母大小写决定跨包可见性
下面用一个真实可运行的示例把它们串起来。
示例工程
目录结构如下:
package-demo/
go.mod
main.go
greetings/
english.go
chinese.go
tools/
naming.go
internal/
mathutil/
add.go
go.mod:
module example.com/package-demo
go 1.26
main.go:
package main
import (
"fmt"
"example.com/package-demo/greetings"
"example.com/package-demo/internal/mathutil"
"example.com/package-demo/tools"
)
func main() {
fmt.Println(greetings.Hello("Gopher"))
fmt.Println(greetings.NiHao("beginner"))
fmt.Println(toolbox.Label())
fmt.Println("2 + 3 =", mathutil.Add(2, 3))
}
运行:
go run .
输出:
Hello, Gopher
Ni hao, beginner
import path ends with /tools, but package name is toolbox
2 + 3 = 5
package 是什么
Go 里的 package 是组织代码的基本单位。一个包通常对应一个目录,同一个目录下的多个 .go 文件会一起编译,组成同一个包。
比如:
greetings/
english.go
chinese.go
english.go:
package greetings
func Hello(name string) string {
return "Hello, " + name
}
chinese.go:
package greetings
func NiHao(name string) string {
return "Ni hao, " + name
}
这两个文件都在 greetings 目录下,并且都声明了:
package greetings
所以它们属于同一个包。english.go 里的函数、变量,chinese.go 也可以直接使用。
包是按目录算的,不是按文件算的
Go 导入包时,导入的是一个目录,而不是某个 .go 文件。
错误写法:
import "example.com/package-demo/greetings/english.go"
正确写法:
import "example.com/package-demo/greetings"
因为 greetings 目录整体就是一个包。
这也是为什么一个目录下可以放多个文件:
greetings/
english.go
chinese.go
format.go
只要它们都写:
package greetings
它们就会被当成一个整体编译。
文件名必须和包名一样吗
不需要。
比如文件可以叫:
english.go
里面写:
package greetings
完全没问题。
文件名主要是给人看的,用来表达这个文件大概放了什么代码。包名由文件里的 package xxx 决定。
实际开发中常见写法是:
user/
user.go
service.go
repository.go
这些文件都可以写:
package user
目录名必须和包名一样吗
也不强制。
比如示例里故意写了一个容易混淆的包:
tools/
naming.go
naming.go:
package toolbox
func Label() string {
return "import path ends with /tools, but package name is toolbox"
}
导入时写的是目录路径:
import "example.com/package-demo/tools"
但使用时写的是包名:
toolbox.Label()
所以要分清楚:
import 后面写的是导入路径
代码里调用时用的是 package 声明的包名
虽然 Go 允许目录名和包名不同,但一般不建议这样做。更清晰的做法是:
tools/
naming.go
里面也写:
package tools
这样别人看到 import ".../tools",自然就会想到 tools.xxx()。
导入路径怎么算
导入路径由 go.mod 决定。
示例里的 go.mod 是:
module example.com/package-demo
所以整个项目的导入路径前缀就是:
example.com/package-demo
公式是:
导入路径 = module 名 + 从 go.mod 所在目录开始算的子目录
对应关系如下:
| 目录 | 导入路径 | 包名 |
|---|---|---|
package-demo/ |
example.com/package-demo |
main |
package-demo/greetings |
example.com/package-demo/greetings |
greetings |
package-demo/tools |
example.com/package-demo/tools |
toolbox |
package-demo/internal/mathutil |
example.com/package-demo/internal/mathutil |
mathutil |
注意第三行:导入路径最后是 tools,但包名是 toolbox。这再次说明导入路径和包名是两个概念。
example.com 是什么
example.com 不是 Go 默认加上的东西,也不是必须的开发前缀。
它只是示例里写在 go.mod 里的模块名:
module example.com/package-demo
你写什么 module,导入路径就从什么开始。
如果写:
module mydemo
那么导入路径就是:
import "mydemo/greetings"
如果写:
module github.com/alice/shop
那么导入路径就是:
import "github.com/alice/shop/greetings"
教程里常用 example.com,是因为它是专门给示例保留的域名,看起来像真实路径,但不会和真实项目冲突。
大写开头才可以被别的包访问
Go 没有 public 和 private 关键字。它用名字首字母大小写控制可见性。
比如:
package greetings
const defaultEnglishPrefix = "Hello"
func Hello(name string) string {
return defaultEnglishPrefix + ", " + name
}
这里有两个名字:
Hello
defaultEnglishPrefix
Hello 首字母大写,所以它可以被其他包访问:
greetings.Hello("Gopher")
defaultEnglishPrefix 首字母小写,所以它只能在 greetings 包内部使用。别的包不能这样写:
greetings.defaultEnglishPrefix
简单记:
大写开头:导出,其他包可以访问
小写开头:不导出,只能当前包内部访问
这个规则适用于函数、变量、常量、类型、结构体字段。
func Hello() {} // 其他包可以访问
func hello() {} // 只能当前包访问
var Name string // 其他包可以访问
var name string // 只能当前包访问
const Version = 1 // 其他包可以访问
const version = 1 // 只能当前包访问
type User struct{} // 其他包可以访问
type user struct{} // 只能当前包访问
结构体字段也一样:
type User struct {
Name string // 其他包可以访问
age int // 其他包不能访问
}
package main 有什么特殊
package main 表示这是一个可执行程序。
一个可执行程序通常需要:
func main() {
}
完整示例:
package main
import "fmt"
func main() {
fmt.Println("hello")
}
运行:
go run .
注意:同一个 package main 里只能有一个 func main()。如果一个目录下有两个文件都定义了 func main(),会报重复定义错误。
internal 包有什么特殊
Go 里有一个特殊目录名:internal。
示例里有:
internal/
mathutil/
add.go
导入路径是:
import "example.com/package-demo/internal/mathutil"
internal 目录下的包只能被它父目录及子目录访问,外部项目不能随便导入。
它适合放项目内部工具代码,比如:
project/
internal/
config/
database/
mathutil/
这样可以明确告诉别人:这些包是项目内部实现,不是对外 API。
常见错误速查
| 错误 | 原因 | 解决办法 |
|---|---|---|
found packages main and user |
同一个目录下混了多个普通包名 | 保持同目录下 package 一致 |
main redeclared |
同一个 package main 里有多个 func main() |
一个可执行包只保留一个入口 |
imported and not used |
导入了包但没有使用 | 删除无用导入,或真正使用它 |
undefined: greetings.xxx |
访问了不存在或未导出的名字 | 检查名字是否拼对,首字母是否大写 |
import cycle not allowed |
包之间循环导入 | 抽出公共逻辑到第三个包 |
导入 .go 文件失败 |
Go 导入的是目录,不是文件 | 改成导入包所在目录 |
包命名建议
包名尽量短、小写、有意义。
推荐:
package user
package order
package config
package greetings
不推荐:
package userUtils
package commonFunction
package my_package
一般建议:
包名全小写
尽量不用下划线
不要为了省事到处叫 util
目录名和包名尽量保持一致
最后总结
Go 的包机制看起来规则很多,但主线非常清楚:
一个目录通常就是一个包
同目录下的普通 .go 文件 package 名要一致
文件名不需要和包名一样
导入路径由 go.mod 的 module 名加子目录决定
调用时使用的是 package 声明的包名
大写开头的名字才能被其他包访问
把这几条记住,Go 的包、模块、导入路径和可见性基本就打通了。后面再遇到 internal、replace、多模块工程时,也能顺着这套逻辑理解。