21 How to Write Packages in Go

如何在 Go 中编写包 #

一个包由同一目录下的 Go 文件组成的,并且在文件开头有相同的包声明。你可以从包中加入额外的功能,使你的程序更加复杂。有些包可以通过 Go 标准库获得,因此在安装 Go 时就已经安装了。其他的可以用 Go 的go get命令来安装。你也可以通过在同一目录下创建 Go 文件来建立你自己的 Go 包,你可以通过使用必要的包声明来分享代码。

本教程将指导你如何编写 Go 包,以便在其他编程文件中使用。

前提条件 #

  • 按照如何安装和设置 Go 的本地编程环境系列教程中的一个教程设置 Go 编程环境。按照本地编程环境教程中的步骤5创建你的 Go 工作区。要遵循本文的例子和命名规则,请阅读第一节「编写和导入软件包」。
  • 为了加深你对 GOPATH 的了解,请阅读文章了解 GOPATH

编写和导入软件包 #

编写包就像编写任何其他 Go 文件一样,包可以包含函数、类型变量的定义,然后可以在其他 Go 程序中使用。

在我们创建一个新的包之前,我们需要进入我们的 Go 工作区。这通常是在我们的gopath下。对于这个例子,本教程中我们将把包称为greet。为了做到这一点,在我们的项目空间下的gopath中创建了一个名为greet的目录。当使用 Github 作为代码库,组织名称为gopherguides,想在此组织下创建greet包,那么我们的目录会是这样的:

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides

greet目录在gopherguides目录中:

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides
                └── greet

最后,我们可以添加我们目录中的第一个文件。通常的做法是,包中的 主要入口 文件是以目录名来命名的。在这种情况下,将在greet目录下创建一个名为greet.go的文件:

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides
                └── greet
                    └── greet.go

创建了文件后,我们就可以开始编写我们想要重复使用或在不同项目中共享的代码。在本例中,我们将创建一个打印出 Hello WorldHello 的函数。

在文本编辑器中打开 greet.go 文件,增加如下代码:

package greet

import "fmt"

func Hello() {
	fmt.Println("Hello, World!")
}

让我们把这个文件分解一下,每个文件中第一行需要是所处的名称。因为你在greet包里,所以通过使用package关键字,后面加包的名称:

package greet

这将告诉编译器把文件中的所有内容作为greet包的一部分。

接下来,你用 import 语句声明你需要使用的任何其他包。在这个文件中你只使用一个包,fmt包:

import "fmt"

最后,你创建函数Hello,它将使用fmt包来打印出Hello, World!

func Hello() {
	fmt.Println("Hello, World!")
}

现在已经编写了greet包,可以在你创建的任何其他包中使用它。让我们创建一个新的包,在其中使用greet包。

接下来创建一个名为example的包,这意味着需要一个名为example的目录。在gopherguides中创建这个包,所以目录结构看起来像这样:

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides
                    └── example

现在你有了新包的目录,可以创建入口文件。因为这将是一个可执行的程序,最好的做法是将入口文件命名为main.go

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides
                └── example
                    └── main.go

在文本编辑器中,打开main.go,添加以下代码来调用greet包:

package main

import "github.com/gopherguides/greet"

func main() {
	greet.Hello()
}

因为正在导入一个包,通过用点符号来调用指定包的函数。点符号是指在使用的包的名称和想使用的包中资源之间加一个句号.。例如,在greet包中,有Hello函数作为一个资源。如果想调用该资源,可以使用 greet.Hello() 的形式。

现在,可以打开终端,在命令行上运行该程序:

go run main.go

完成后,你将收到以下输出:

Hello, World!

为了解如何在包中使用变量,让我们在greet.go文件中添加一个变量定义:

package greet

import "fmt"

var Shark = "Sammy"

func Hello() {
	fmt.Println("Hello, World!")
}

接下来,打开main.go文件,添加以下高亮行,在fmt.Println()函数中调用greet.go中的变量:

package main

import (
	"fmt"

	"github.com/gopherguides/greet"
)

func main() {
	greet.Hello()

	fmt.Println(greet.Shark)
}

再次运行此程序:

go run main.go

你会收到以下输出:

Hello, World!
Sammy

最后,让我们也在greet.go文件中定义一个类型。创建一个带有 namecolor字段的 Octopus 类型,以及一个在调用时将打印出字段的函数:

package greet

import "fmt"

var Shark = "Sammy"

type Octopus struct {
	Name  string
	Color string
}

func (o Octopus) String() string {
	return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
}

func Hello() {
	fmt.Println("Hello, World!")
}

打开main.go,在文件的末尾创建一个该类型的实例:

package main

import (
	"fmt"

	"github.com/gopherguides/greet"
)

func main() {
	greet.Hello()

	fmt.Println(greet.Shark)

	oct := greet.Octopus{
		Name:  "Jesse",
		Color: "orange",
	}

	fmt.Println(oct.String())
}

一旦你用oct := greet.Octopus创建了一个Octopus类型的实例,就可以在main.go文件的命名空间中访问该类型的函数和字段。这使得在最后一行直接写oct.String(),而不用调用greet。同样的,也可以在不引用greet包的名字的情况下调用oct.Color等类型字段。

Octopus类型上的String方法使用fmt.Sprintf函数来输出一段文本,并将结果即一个字符串,返回给调用者(在这里是指主程序)。

当你运行该程序时,你会收到以下输出:

go run main.go
Hello, World!
Sammy
The octopus's name is "Jesse" and is the color orange.

通过在Octopus上创建String方法,你现在有一个可重复使用的方法来打印出自定义类型的信息。如果想在将来改变这个方法的行为,只需要编辑这一个方法。

可导出代码 #

你可能已经注意到,调用的greet.go文件中所有的声明都是大写的。Go 没有像其他语言那样有publicprivateprotected修饰符的概念。外部可见性是由大写字母控制的。以大写字母开头的类型、变量、函数等等,在当前包之外是可以公开使用的。一个在其包外可见的符号被认为是 可导出 的。

如果你给Octopus添加了一个名为reset的新方法,可以在greet包内调用它,但是不能在main.go文件中调用,因为调用者在greet包之外:

package greet

import "fmt"

var Shark = "Sammy"

type Octopus struct {
	Name  string
	Color string
}

func (o Octopus) String() string {
	return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
}

func (o *Octopus) reset() {
	o.Name = ""
	o.Color = ""
}

func Hello() {
	fmt.Println("Hello, World!")
}

如果你试图从main.go文件中调用reset

package main

import (
	"fmt"

	"github.com/gopherguides/greet"
)

func main() {
	greet.Hello()

	fmt.Println(greet.Shark)

	oct := greet.Octopus{
		Name:  "Jesse",
		Color: "orange",
	}

	fmt.Println(oct.String())

	oct.reset()
}

你会收到以下编译错误:

oct.reset undefined (cannot refer to unexported field or method greet.Octopus.reset)

要从 Octopus导出 reset 功能,请将reset 中的R 大写:

package greet

import "fmt"

var Shark = "Sammy"

type Octopus struct {
	Name  string
	Color string
}

func (o Octopus) String() string {
	return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
}

func (o *Octopus) Reset() {
	o.Name = ""
	o.Color = ""
}

func Hello() {
	fmt.Println("Hello, World!")
}

如此一来,可以从其他包中调用`Reset’而不会得到错误:

package main

import (
	"fmt"

	"github.com/gopherguides/greet"
)

func main() {
	greet.Hello()

	fmt.Println(greet.Shark)

	oct := greet.Octopus{
		Name:  "Jesse",
		Color: "orange",
	}

	fmt.Println(oct.String())

	oct.Reset()

	fmt.Println(oct.String())
}

现在,如果你运行这个程序:

go run main.go

你将收到以下输出:

Hello, World!
Sammy
The octopus's name is "Jesse" and is the color orange
The octopus's name is "" and is the color .

通过调用Reset,清除了NameColor字段中的所有信息。当调用String方法时,NameColor打印为空,因为这些字段现在是空的。

总结 #

编写 Go 包与编写其他 Go 文件是一样的,但把它放在另一个目录中可以隔离代码,以便在其他地方重复使用。本教程介绍了如何在包中编写定义,演示了如何在另一个 Go 文件中使用这些定义,并解释了控制包是否可访问的选项。