Golang通脉之包的管理
阅读原文时间:2021年10月21日阅读:22

在工程化的开发项目中,Go语言的源码复用是建立在包(package)基础之上的。

包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言提供了很多内置包,如fmtosio等。

src 目录是以代码包的形式组织并保存 Go 源码文件的。每个代码包都和 src 目录下的文件夹一一对应。每个子目录都是一个代码包。

代码包包名和文件目录名,不要求一致。比如文件目录叫 server,但是代码包包名可以声明为 “main”,但是同一个目录下的源码文件第一行声明的所属包,必须一致!

还可以根据自己的需要创建自己的包。一个包可以简单理解为一个存放.go文件的文件夹。 该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。

package 包名

注意事项:

  • 一个文件夹下面直接包含的文件只能归属一个package,同样一个package的文件不能在多个文件夹下。
  • 包名可以不和文件夹的名字一样,包名不能包含 - 符号。
  • 包名为main的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。
  • 在同一个包下面的文件属于同一个工程文件,不用import包,可以直接使用

如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了:

package pkg

import "fmt"

// 包变量可见性

var a = 100 // 首字母小写,外部包不可见,只能在当前包内使用

// 首字母大写外部包可见,可在其他包中使用
const Mode = 1

type person struct { // 首字母小写,外部包不可见,只能在当前包内使用
    name string
}

// 首字母大写,外部包可见,可在其他包中使用
func Add(x, y int) int {
    return x + y
}

func age() { // 首字母小写,外部包不可见,只能在当前包内使用
    var Age = 18 // 函数局部变量,外部包不可见,只能在当前函数内使用
    fmt.Println(Age)
}

结构体中的字段名和接口中的方法名如果首字母都是大写,外部包可以访问这些字段和方法:

type Student struct {
    Name  string //可在包外访问的方法
    class string //仅限包内访问的字段
}

type Payer interface {
    init() //仅限包内访问的方法
    Pay()  //可在包外访问的方法
}

Go 语言的入口 main() 函数所在的包(package)叫 main,main 包想要引用别的代码,需要import导入!

要在代码中引用其他包的内容,需要使用import关键字导入使用的包。具体语法如下:

A:通常导入

// 单个导入
import "package"
// 批量导入
import (
  "package1"
  "package2"
  )

B:点操作 有时候会看到如下的方式导入包

import(
    . "fmt"
)

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world")

注意事项:

  • import导入语句通常放在文件开头包声明语句的下面。
  • 导入的包名需要使用双引号包裹起来。
  • 包名是从$GOPATH/src/后开始计算的,使用/进行路径分隔。
  • Go语言中禁止循环导入包

自定义包名顾名思义可以把包命名成另一个用起来容易记忆的名字。导入时,可以为包定义别名,语法演示:

import (
  p1 "package1"
  p2 "package2"
  )
// 使用时:别名操作,调用包函数时前缀变成了自定义的前缀
p1.Method()

_ 操作 如果仅仅需要导入包时执行初始化操作,并不需要使用包内的其他函数,常量等资源。则可以在导入包时,匿名导入。

这个操作经常是让很多人费解的一个操作符:

import (
   "database/sql"
   _ "github.com/mysql/driver"
 )

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。也就是说,使用下划线作为包的别名,会仅仅执行init()

导入的包的路径名,可以是相对路径也可以是绝对路径,推荐使用绝对路径(起始于工程根目录)。

匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。

init()函数介绍

在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是: init()函数没有参数也没有返回值。 init()函数在程序运行时自动被调用执行,不能在代码中主动调用它,init() 中的代码会在 main() 函数执行前执行,用于初始化包所需要的特定资源。

包初始化执行的顺序如下图所示:

init()函数执行顺序

Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。

在运行时,被最后导入的包会最先初始化并调用其init()函数, 如下图示:

禁止循环引用包:A -> B -> C -> A

init函数与main函数的异同

相同点:

  • 两个函数在定义时不能有任何的参数和返回值。 都只能由 go 程序自动调用,不可以被引用。

不同点:

  • init 可以应用于任意包中,且可以重复定义多个。 main 函数只能用于 main 包中,且只能定义一个。

  • 两个函数的执行顺序:

    在 main 包中的 go 文件默认总是会被执行。对同一个 go 文件的 init( ) 调用顺序是从上到下的。

    对同一个 package 中的不同文件,将文件名按字符串进行“从小到大”排序,之后顺序调用各文件中的init()函数。

    对于不同的 package,如果不相互依赖的话,按照 main 包中 import 的顺序调用其包中的 init() 函数。

    如果 package 存在依赖,调用顺序为最后被依赖的最先被初始化,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main,一次执行对应的 init 方法。main 包总是被最后一个初始化,因为它总是依赖别的包