Menu

Go 包与导入:导出名、多文件包与 import

Go 的包是如何工作的:package 声明、导入标准库和模块中的包、导出与未导出的名字、把一个包拆分到多个文件、internal 包、导入别名、空白导入,以及 go run main.go 报出的 "undefined" 错误。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

每个 Go 文件都属于一个包,包在文件的第一行声明。一个包中的代码要使用另一个包,就导入它,并在名字前加上包名作为前缀。只有首字母大写的名字才能在包外使用。

fmt、math 和 strings 都是标准库中的包。它们随 Go 一起提供,所以“添加”一个包只需要在 import 块里写上它的名字,不会下载任何东西。fmt.Println、math.Sqrt 和 strings.Repeat 首字母大写,因为它们是导出的。

package 声明

每个 .go 文件第一行非注释的代码都是 package name。同一个目录下的所有文件必须使用同一个名字,它们合起来构成一个包。一个目录里出现两个不同的包名,就无法编译:

found packages greeting (b.go) and greet (greet.go) in /home/ana/shop/greet

(唯一的例外是测试文件,它可以使用 package name_test。)

package main 很特殊:它生成可执行文件,程序从它的 func main() 开始执行。其他所有包都是用来被导入的库。

按照惯例,包名与目录路径的最后一段相同:shop/greet 中的代码是 package greet。包名应简短、全小写、只用一个单词,不用下划线或大小写混写:strconv、httptest、greet,而不是 string_utils 或 greetHelpers。

导入包

导入路径是一个字符串。标准库的导入路径是包在 Go 源码树中的路径;其他包的导入路径都以模块路径开头:

import (
	"fmt"           // standard library
	"net/http"      // standard library, nested path
	"math/rand/v2"  // standard library, version 2 of math/rand

	"github.com/google/uuid"   // another module (needs go get)
	"example.com/shop/greet"   // a package in your own module
)

路径是包所在的位置;名字是你在代码中写的东西。net/http 的名字是 http,math/rand/v2 的名字是 rand。你写的是 http.Get,永远不会写 net/http.Get。

gofmt 和 goimports 会对导入块排序,并且按惯例用一个空行把标准库导入和其他导入分开。

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

./main.go:5:2: "os" imported and not used

支持 Go 的编辑器会在保存时运行 goimports,自动帮你添加缺少的标准库导入,并删除未使用的导入。

导出与未导出的名字

Go 中的可见性只由一条规则决定:名字以大写字母开头,就是导出的。这适用于函数、类型、变量、常量、struct 字段和方法。Go 没有 public、private 或 protected 关键字。

名字包外可见?
func Hello()是
func hello()否
type User struct是
User.Name 字段是
User.email 字段否
const MaxSize是
var defaultTimeout否

在另一个包中使用未导出的名字,会得到 undefined 错误:

./main.go:11:20: undefined: greet.prefix

即使只有一个文件,这条规则也很重要,因为其他包会检查你的类型。encoding/json 只能看到导出的字段:

token 被悄无声息地排除在 JSON 之外。如果这个字段是私有数据,这是个特性;如果你只是忘了把想要的字段首字母大写,那就是个意外。

把一个包拆分到多个文件

一个包可以由任意多个文件组成。目录中 package 行相同的每个文件,都能看到其他文件中声明的所有名字,无论是否导出,而且不需要导入任何东西:

shop/
├── go.mod        (module example.com/shop)
├── main.go
└── math.go
// main.go
package main

import "fmt"

func main() {
	fmt.Println(total(2, 3))
}
// math.go
package main

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

main.go 直接调用 total。同一个包的文件之间没有 #include,也不需要导入。

go run main.go 报出的 "undefined" 错误

这是 Go 中被搜索最多的包问题,而它只有一个原因。只运行包含 main 的那个文件,就只会编译那一个文件:

go run main.go
# command-line-arguments
./main.go:6:14: undefined: total

command-line-arguments 是 Go 给由文件列表构建出的包起的名字,而 math.go 不在这个列表里。改为运行整个包:

go run .
5

go build main.go 也是同样的道理。使用 go run . 和 go build(或 go build ./cmd/app),这个问题就不会出现。如果用 go run . 仍然报错,检查两个文件是否都写着 package main,以及是否有某个文件因为构建约束(//go:build)或 _test.go 后缀而被排除。

你自己的包

模块中的子目录就是一个单独的包。用 go.mod 中的模块路径加上目录来导入它:

shop/
├── go.mod          (module example.com/shop)
├── main.go
└── greet/
    └── greet.go
// greet/greet.go
package greet

const prefix = "Hi, "

// Hello returns a greeting for name.
func Hello(name string) string {
	return prefix + name
}
// main.go
package main

import (
	"fmt"

	"example.com/shop/greet"
)

func main() {
	fmt.Println(greet.Hello("Ana"))
}
Hi, Ana

这里有两个容易出错的地方。导入写的是模块路径加目录,而不是相对路径:在模块模式下 "./greet" 行不通。另外,prefix 是小写的,所以对 main 不可见;只有 Hello 属于这个包的 API。

internal 包

名为 internal 的目录会限制谁可以导入其中的内容。shop/internal/store 中的代码可以被以 shop/ 为根的任何包导入,但不能被这之外的任何代码导入。另一个模块如果尝试导入,会得到:

use of internal package example.com/shop/internal/store not allowed

当你想在自己的多个包之间共享代码,又不想对外承诺它的 API 时,就用 internal/。你可以随意修改它的 API,因为外部代码无法依赖它。

导入别名

在路径前写一个名字,就能给导入的包换一个本地名字。最常见的原因是两个包同名:

这两个包都叫 rand,所以至少有一个需要别名。别名用于解决冲突和处理含义不清的名字;不要只是为了写得短而重命名包,因为读代码的人认识 strconv,却不认识你的 sc。

空白导入与点导入

空白导入(_)。 只为了副作用而导入一个包:它的包级变量和 init 函数会执行,但你不使用它的任何名字。数据库驱动和图片解码器就是这样注册自己的:

import (
	"database/sql"

	_ "github.com/lib/pq" // registers the "postgres" driver with database/sql
)

如果没有 _,这个导入就会触发“未使用的导入”错误。

点导入(.)。 把包中导出的名字直接放进当前文件的作用域,这样你可以写 Println 而不是 fmt.Println。它会让人看不出名字来自哪里,除了少数测试辅助的写法之外,不建议使用。

循环导入

Go 不允许两个包直接或通过一条链互相导入:

package example.com/shop
	imports example.com/shop/greet from main.go
	imports example.com/shop/other from c.go
	imports example.com/shop/greet from o.go: import cycle not allowed

错误信息会列出这条导入链。下面两种修复方法几乎能覆盖所有情况:

  1. 把两个包都需要的代码移到第三个更底层的包中,让两者都导入它。
  2. 如果包 A 只是需要调用 B 中的某些东西,就在 A 中定义一个小接口,让 B 的类型去实现它。这样 A 就不再导入 B。

出现循环导入,通常说明包的边界划在了本该放在一起的代码层之间,所以这也是一个重新考虑拆分方式的信号。

常见错误

  • 只运行单个文件。 在多文件的包中使用 go run main.go:应该用 go run .。
  • 相对导入。 import "./utils" 在模块模式下会失败。应使用 "example.com/yourmodule/utils"。
  • 本想导出的名字写成了小写。 其他包需要用到的函数、字段或方法,必须以大写字母开头。
  • 包名与目录名不一致。 这样写合法,但容易让人困惑:导入路径写的是 utils,代码里却是 helpers.X。让两者保持一致。
  • 大杂烩式的包名。 util、common 和 misc 说明不了包里装的是什么,还容易变成循环导入的重灾区。按包提供的功能来命名:money、auth、slug。

常见问题

为什么 Go 说同一个包里的函数 undefined?

你很可能运行的是 go run main.go。指定文件名时只会编译那一个文件,所以同一个包里其他文件定义的函数就找不到了:undefined: total。改用 go run .,它会编译目录中的每个 .go 文件。同时检查每个文件的 package 行是否相同。

Go 中如何添加 fmt 包?

fmt 属于标准库,随 Go 一起安装,所以不需要另外安装任何东西。在 package 行之后加上 import "fmt",然后用 fmt. 前缀调用它的函数,例如 fmt.Println("hi")。strings、os、time、net/http 以及其他所有标准包都是一样的。

Go 中如何导入自己写的包?

按模块路径加目录来导入。在名为 example.com/shop 的模块中,greet 文件夹里的包这样导入:import "example.com/shop/greet",用法是 greet.Hello()。只有首字母大写的名字对导入方可见。

Go 中导出和未导出有什么区别?

以大写字母开头的名字(Hello、Total、User.Name)是导出的,可以在其他包中使用。小写的名字(hello、total、user.name)是未导出的,只在自己的包内可见。Go 没有 public 或 private 关键字。

Go 中如何解决 "import cycle not allowed"?

两个包直接或通过一条链互相导入,而 Go 禁止这样做。把共用的代码移到第三个包里,让两者都导入它;或者在需要该行为的包中定义一个小接口,这样它就不再需要导入另一个包。

Coddy 编程语言插图

和 Coddy 一起学编程

开始