Go 语言 package 入门:从目录、包名到导入路径

刚开始学 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 没有 publicprivate 关键字。它用名字首字母大小写控制可见性。

比如:

复制代码
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 的包、模块、导入路径和可见性基本就打通了。后面再遇到 internalreplace、多模块工程时,也能顺着这套逻辑理解。