重学Go语言 | Go包管理详解
我正在参加「掘金·启航计划」
大部分编程语言都有其代码组织方式,以方便管理我们所开发的代码,比如PHP
的命名空间
(namespace
),Java
的package
,JavaScript
的module
;
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文件有清楚的认识。
- 包的导入,包括导入路径,导入方式,可见性以及内部包。
转载自:https://juejin.cn/post/7247518785834041403