likes
comments
collection
share

重学Go语言 | Go包管理详解

作者站长头像
站长
· 阅读数 11

我正在参加「掘金·启航计划」

大部分编程语言都有其代码组织方式,以方便管理我们所开发的代码,比如PHP命名空间(namespace),JavapackageJavaScriptmodule

Go语言也有自己的代码组织方式:包(package)。

Go语言包管理历史

Go语言包管理历史主要有以下几个阶段:

  • GOPATH时代:项目放在GOPATH环境变量所配置的目录下的src目录中。
  • vendor与各种版本管理工具百花齐放
  • 官方Go module一统江湖。

包的定义

什么是包(package)?

在Go语言中,是一个或多个源码文件的集合,源码文件以.go为后缀,在源码文件中,我们可以声明常量、变量、函数、自定义类型与方法。

我们开发的程序(项目或者类库)是由一个或多个包构成的。

程序、包、源码文件之间的关系如下图所示:

包的声明

对于Go初学者来说,直接学习Go module这种包管理工具就可以了,为了后面更好地讲解,我们先用go mod init命令初始化一个项目:

//创建项目目录
mkdir demo
//进入目录
cd demo
//初始化项目
go mod init github.com/my/demo

go.mod

执行了go mod init命令后,这时候可以看到demo目录下有一个go.mod的文件,其内容如下:

module github.com/my/demo

go 1.18

go.mod文件主要记录项目的module路径、项目依赖包列表(当前未依赖其包,所以为空)和go的版本信息。

go.sum

如果我们要导入外部包,可以执行go get命令:

go get github.com/go-sql-driver/mysql

执行后,会生成go.sum文件,这个文件记录着当前项目每个依赖包的哈希值,在项目构建时,会计算依赖包的哈希值,并与go.sum中对应的哈希值比较,以防止依赖包被窜改。

执行go get命令后,go.mod文件也会新增了一条依赖记录:

module github.com/my/demo

go 1.18

require github.com/go-sql-driver/mysql v1.7.0 // indirect

main包

Go语言程序的入口main函数,该函数所在的包必须为main:

package main

func main(){
    //...
}

包的声明

包的声明位于源文件的第一行的package语句,后面跟着包名:

package 包名

除了main包外,其他包的名称必须与对应的目录同名,同一个目录下所有源码文件包名必须相同:

user/user.go

package user

type User struct {
    ID   int
    Name string
}

user/list.go

package user

func (u *User) List() []User {
    return []User{{ID: 1, Name: "test"}}
}

包的导入与使用

包声明之后,就可以在其他包中导入了,导入包名使用import语句。

导入路径

标准库包的导入路径一般就是标准库的名称:

import "fmt"
import "time"
import "net/http"

如果是我们自己声明的包,其导入路径为module路径加上包名,比如我们项目module路径为github.com/my/demo,那么要导入user包,其路径为:

import "github.com/my/demo/user"

对于有多重目录的包来说,比如controller目录下有一个order包,那么其路径需要包含完整的目录名:

import "github.com/my/demo/controller/order"

两种导入方式

Go的包有两种导入方式,一种是标准导入,一种是匿名导入。

标准导入

普通导入在关键字import后跟着导入路径即可,然后就可以通过包名调用包下的变量、函数、常量了:

package main

import "github.com/my/demo/user"

func main() {
  userList := user.List()
  fmt.Println(userList)
}

Go支持在导入的时候为包取一个别名(alias),这样可以避免因包名相同而无法导入的问题,比如下面我们声明一个名称为time的包:

package time

import "time"

func Now() int64 {
    return time.Now().Unix()
}

之后在main包中,调用自定义的time包,同时导入标准库time包,由于导入时包名相同,因此可以给其中一个包起一个别名来避免无法导入的问题:

package main

import t "github.com/my/demo/time"
import "time"

func main() {
  fmt.Println(time.Now().Unix())
  fmt.Println(t.Now())
}

如果别名是一个点号,那么此时导入的所有变量、函数、常量都不需要通过包名来访问了:

package main

import . "github.com/my/demo/time"
import "time"

func main() {
  fmt.Println(time.Now().Unix())
  fmt.Println(Now())
}

如果导入多个包,也可将多个导入写在一个import语句中,用小括号括起来:

package main

import (
    "time"

    t "github.com/my/demo/time"
)

func main() {
  fmt.Println(time.Now().Unix())
  fmt.Println(Now())
}

匿名导入

通过import语句导入的包后必须使用,否则无法通过编译。

但有时候我们导入包仅仅只是为了导入时自动执行包中的init函数而已,在后续代码中并不会使用该包,此时可以通过在包路径前面加上空白符_将导入声明为匿名导入:

import (
    "database/sql"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

可见性

对于定义在包里的变量、常量、函数等代码,如果不想被其他包调用,则其首字母应该设置为小写,小写字母开头的函数、变量、常量为可见性为private,而首字母大写的话,可见性为public,在其他包中被调用:

package user

//private
func eat(){
    //....
}

//public
func Say(){
    //....
}

内部包

我们开发的库可能会分享(比如通过Github或者Gitlab)给其他开发者使用,当我们改动到其他开发者依赖的函数或变量时,就会对他们造成影响,破坏到他们的代码,因此在开发时,要对外暴露通用的,不经常改动的代码,对于会变动的逻辑,可以放在internal目录中,Go语言不允许导入internal包。

比如下面的目录中,internal包中所有的代码,只能在本项目中使用,其他开发者无法导入:

.
├── cmd
│   └── main.go
├── go.mod
├── go.sum
├── internal
│   └── util
│       └── util.go
├── time
│   └── time.go
└── user
    ├── list.go
    └── user.go

小结

包是Go语言代码的组织单位,通过包,我们可以管理我们的代码,尤其是当开发大型项目时,可以通过包进行模块划分,另外,我们也可以封装自己的包以供其他开发者使用,或者在自己的项目中导入其他开发者的包。

最后,总结一下在这篇文章中的要点:

  • Go包管理的历史
  • 包的声明,对go.mod和go.sum文件有清楚的认识。
  • 包的导入,包括导入路径,导入方式,可见性以及内部包。