包(package)
在工程化的Go语言开发项目中,Go语言的源码复用是建立在包(package)基础之上的。本文介绍了Go语言中如何定义包、如何导出包的内容及如何引入其他包。同时也将介绍如何在项目中使用go module管理依赖。
包介绍
Go语言中支持模块化的开发理念,在Go语言中使用包(package)来支持代码模块化和代码复用。一个包是由一个或多个Go源码文件(.go结尾的文件)组成,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、os、io等。
例如,在之前的章节中我们频繁使用了fmt这个内置包。
package main
import "fmt"
func main(){
fmt.Println("Hello world!")
}
上面短短的几行代码就涉及到了如何定义包以及如何引入其它包两个内容,接下来我们依次介绍一下它们。
定义包
我们可以根据自己的需要创建自定义包。一个包可以简单理解为一个存放.go文件的文件夹。该文件夹下面的所有.go文件都要在非注释的第一行添加如下声明,声明该文件归属的包。
package packagename
- package:声明包的关键字
- packagename:包名,可以不与文件夹的名称一致,不能包含-符号,最好与其实现的功能相对应。
另外需要注意一个文件夹下面直接包含的文件只能归属一个包,同一个包的文件不能在多个文件夹下。包名为main的包是应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。
标识符可见性
在同一个包内部声明的标识符都位于同一个命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部使用包内部的标识符就需要添加包名前缀,例如fmt.Println("Hello world!"),就是指调用fmt包中的Println函数。
如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。在Go语言中是通过标识符的首字母大/小写来控制标识符的对外可见(public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。
例如我们定义一个名为demo的包,在其中定义了若干标识符。在另外一个包中并不是所有的标识符都能通过demo.前缀访问到,因为只有那些首字母是大写的标识符才是对外可见的。
package demo
import "fmt"
// 包级别标识符的可见性
// num 定义一个全局整型变量
// 首字母小写,对外不可见(只能在当前包内使用)
var num = 100
// Mode 定义一个常量
// 首字母大写,对外可见(可在其它包中使用)
const Mode = 1
// person 定义一个代表人的结构体
// 首字母小写,对外不可见(只能在当前包内使用)
type person struct {
name string
Age int
}
// Add 返回两个整数和的函数
// 首字母大写,对外可见(可在其它包中使用)
func Add(x, y int) int {
return x + y
}
// sayHi 打招呼的函数
// 首字母小写,对外不可见(只能在当前包内使用)
func sayHi() {
var myName = "七米" // 函数局部变量,只能在当前函数内使用
fmt.Println(myName)
}
同样的规则也适用于结构体,结构体中可导出字段的字段名称必须首字母大写。
type Student struct {
Name string // 可在包外访问的方法
class string // 仅限包内访问的字段
}
包的引入
要在当前包中使用另外一个包的内容就需要使用import关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方。完整的引入声明语句格式如下:
import importname "path/to/package"
- importname:引入的包名,通常都省略。默认值为引入包的包名。
- path/to/package:引入包的路径名称,必须使用双引号包裹起来。
- Go语言中禁止循环导入包。
一个Go源码文件中可以同时引入多个包,例如:
import "fmt"
import "net/http"
import "os"
当然可以使用批量引入的方式。
import (
"fmt"
"net/http"
"os"
)
当引入的多个包中存在相同的包名或者想自行为某个引入的包设置一个新包名时,都需要通过importname指定一个在当前文件中使用的新包名。
例如,在引入fmt包时为其指定一个新包名f。这样在当前这个文件中就可以通过使用f来调用fmt包中的函数了。
import f "fmt"
f.Println("Hello world!")
如果引入一个包的时候为其设置了一个特殊_作为包名,那么这个包的引入方式就称为匿名引入。一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引入的包中的init函数将被执行并且仅执行一遍。
import _ "github.com/go-sql-driver/mysql"
匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。
需要注意的是,Go语言中不允许引入包却不在代码中使用这个包的内容,如果引入了未使用的包则会触发编译错误。
init初始化函数
在每一个Go源文件中,都可以定义任意个如下格式的特殊函数:
func init(){ // ... }
这种特殊的函数不接收任何参数也没有任何返回值,我们也不能在代码中主动调用它。当程序启动的时候,init函数会按照它们声明的顺序自动执行。
一个包的初始化过程是按照代码中引入的顺序来进行的,所有在该包中声明的init函数都将被串行调用并且仅调用执行一次。每一个包初始化的时候都是先执行依赖的包中声明的init函数再执行当前包中声明的init函数。确保在程序的main函数开始执行时所有的依赖包都已初始化完成。
每一个包的初始化是先从初始化包级别变量开始的。例如从下面的示例中我们就可以看出包级别变量的初始化会先于init初始化函数。
package main
import "fmt"
var x int8 = 10
const pi = 3.14
func init() {
fmt.Println("x:", x)
fmt.Println("pi:", pi)
sayHi()
}
func sayHi() {
fmt.Println("Hello World!")
}
func main() {
fmt.Println("你好,世界!")
}
输出结果:
x: 10
pi: 3.14
Hello World!
你好,世界!
在上面的代码中,我们了解了Go语言中包的定义及包的初始化过程,这让我们能够在开发时按照自己的需要定义包。同时我们还学到了如何在我们的代码中引入其它的包,不过在本小节的所有示例中我们都是引入Go内置的包。现代编程语言大多都允许开发者对外发布包/库,也支持开发者在自己的代码中引入第三方库。这样的设计能够让广大开发者一起参与到语言的生态环境建设当中,把生态建设的更加完善。