目录

  • 包的声明
  • 包的导入和调用
  • 导入形式
  • 包的成员可见性
  • main包
  • init函数和调用链
  • 包管理之vendor
  • 包管理之mod
  • internal包
  • 如何将包组织成工程

包的基本概念

包是什么

  • go里面的包是用来组织源码,是多个源文件的集合

包的声明/定义

  • 在源文件中加上package xxx就可以声明xxx的包

定义一个包

  • 新建一个pk1文件夹,和内部的func1.go
  1. package pk1
  2. import "fmt"
  3. var PK1Var1 = "pk1var1"
  4. func PK1Func1() {
  5. fmt.Println("pk1的方法")
  6. }
  • 在外侧调用
  1. package main
  2. import (
  3. "fmt"
  4. "maday06/pk1"
  5. )
  6. func main() {
  7. fmt.Println(pk1.PK1Var1)
  8. pk1.PK1Func1()
  9. }

路径、目录名、包名、文件名的关系

  • 看下面这个例子
  • 创建目录 src/pk1/pk2
  • 创建文件 src/pk1/pk2/func2.go,包名和目录不相同
  1. package pk3
  2. import (
  3. "log"
  4. )
  5. func Func2_test() {
  6. log.Printf("[pk1/pk2/func2.go里面的方法]")
  7. }
  • 创建文件 src/pk1/pk2/func3.go ,包名和目录不相同
  1. package pk3
  2. import (
  3. "log"
  4. )
  5. func Func3_test() {
  6. log.Printf("[pk1/pk2/func3.go里面的方法]")
  7. }
  • 创建文件 src/pk1/func1.go ,包名和目录不相同
  1. package pk4
  2. import pk3 "maday06/src/pk1/pk2"
  3. func Func1_test() {
  4. pk3.Func2_test()
  5. pk3.Func3_test()
  6. }
  • 创建文件 src/index.go
  1. package main
  2. import (
  3. "maday06/src/pk1"
  4. "maday06/src/pk1/pk2"
  5. )
  6. func main() {
  7. pk4.Func1_test()
  8. pk3.Func2_test()
  9. pk3.Func3_test()
  10. }
  • 执行index.go
  1. 2021/07/10 16:40:16 [pk1/pk2/func2.go里面的方法]
  2. 2021/07/10 16:40:16 [pk1/pk2/func3.go里面的方法]
  3. 2021/07/10 16:40:16 [pk1/pk2/func2.go里面的方法]
  4. 2021/07/10 16:40:16 [pk1/pk2/func3.go里面的方法]

关系说明

  • import 导入的是路径,而非包名
  • 包名和目录名不强制一致,但推荐一致
  • 在代码中引用包的成员变量或者函数时,使用的包名不是目录名
  • 在同一目录下,所有的源文件必须使用相同的包名
    • Multiple packages in directory: pk2, pk3
  • 文件名不限制,但不能有中文

包名要求

  • 包名一般小写,使用一个简短且有意义的名字
  • 名字中可以含有- 等特殊符号

不使用go mod 同包目录找到对象的问题

但是在gopath路径下 +go mod就可以

包导入形式

常规形式 根据路径导入

  • import xxx/xx

别名导入目的为了 名字重复的包

  • 创建src/log/a.go
  1. package log
  2. import "fmt"
  3. func LogPrint() {
  4. fmt.Println("自定义的log")
  5. }
  • 使用
  1. package main
  2. import (
  3. "log"
  4. mylog "maday06/src/log"
  5. "maday06/src/pk1"
  6. "maday06/src/pk1/pk2"
  7. )
  8. func main() {
  9. mylog.LogPrint()
  10. log.Printf("内置的log")
  11. pk4.Func1_test()
  12. pk3.Func2_test()
  13. pk3.Func3_test()
  14. }

使用.导入

  • 目的是省略包名
  • 方法的的名字容易冲突
  1. package main
  2. import (
  3. . "maday06/src/log"
  4. "maday06/src/pk1"
  5. "maday06/src/pk1/pk2"
  6. )
  7. func main() {
  8. LogPrint()
  9. pk4.Func1_test()
  10. pk3.Func2_test()
  11. pk3.Func3_test()
  12. }

使用下划线导入

  • 目的:有的时候并非真的需要这些包,仅仅是希望的它init()函数被执行而已
  • 举例mysql [https://github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)
  1. package main
  2. import (
  3. "database/sql"
  4. "time"
  5. _ "github.com/go-sql-driver/mysql"
  6. )
  7. func main() {
  8. // ...
  9. db, err := sql.Open("mysql", "user:password@tcp(localhost)/dbname")
  10. if err != nil {
  11. panic(err)
  12. }
  13. // See "Important settings" section.
  14. db.SetConnMaxLifetime(time.Minute * 3)
  15. db.SetMaxOpenConns(10)
  16. db.SetMaxIdleConns(10)
  17. }

如果不导入会怎么样

  • panic: sql: unknown driver "mysql" (forgotten import?)
  • 上述报错来自与C:\Program Files\Go\src\database\sql\sql.go+767
  1. func Open(driverName, dataSourceName string) (*DB, error) {
  2. driversMu.RLock()
  3. driveri, ok := drivers[driverName]
  4. driversMu.RUnlock()
  5. if !ok {
  6. return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
  7. }
  • 继续看代码发现 C:\Program Files\Go\src\database\sql\sql.go +44有个注册的方法
  • 会向drivers map中写入 driver
  1. func Register(name string, driver driver.Driver) {
  2. driversMu.Lock()
  3. defer driversMu.Unlock()
  4. if driver == nil {
  5. panic("sql: Register driver is nil")
  6. }
  7. if _, dup := drivers[name]; dup {
  8. panic("sql: Register called twice for driver " + name)
  9. }
  10. drivers[name] = driver
  11. }
  • 在 C:\Program Files\Go\src\database\sql\sql.go +83 包中发现有init函数
  1. func init() {
  2. sql.Register("mysql", &MySQLDriver{})
  3. }
  • init函数在调用sql.Register向driver map中注册 mysql类型的驱动

main包

  • package main 下面可以有多个文件,但所有文件只能有一个main方法,代表程序的入口
  • package main 每个go应用有一个名为main的包

包导入过程和调用链

包管理之vendor

  • 最开始的时候

govendor 测试

包管理之mod

  • go modules 是 golang 1.11 新加的特性

设置 go mod 和 go proxy

GO111MODULE 有三个值:off, on和auto(默认值)。

  • =off 代表go命令将不会支持module功能,按照原来的那种vendor目录或者GOPATH模式来查找
  • =on go命令会使用module功能,一点也不会去GOPATH目录查找
  • =auto ,默认值,go命令行会根据当前目录来决定是否启用module功能。
    • 当前目录在GOPATH/src之外的,而且该目录还有go.mod 开启
    • 处于GOPATH没有go.mod 等于off
  • 如果不使用 go mod,go get会去master分支拉代码
  • 使用go mod,可以选tag

go mod使用 go mod 有以下命令:

命令 说明
download download modules to local cache(下载依赖包)
edit edit go.mod from tools or scripts(编辑go.mod)
graph print module requirement graph (打印模块依赖图)
init initialize new module in current directory(在当前目录初始化mod)
tidy add missing and remove unused modules(拉取缺少的模块,移除不用的模块)
vendor make vendored copy of dependencies(将依赖复制到vendor下)
verify verify dependencies have expected content (验证依赖是否正确)
why explain why packages or modules are needed(解释为什么需要依赖)
  • 常用tidy init edit

使用go mod管理新的项目

  • 创建一个新的项目 go mod init
  1. package main
  2. import "github.com/gin-gonic/gin"
  3. func main() {
  4. r := gin.Default()
  5. r.GET("/ping", func(c *gin.Context) {
  6. c.JSON(200, gin.H{
  7. "message": "pong",
  8. })
  9. })
  10. r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
  11. }
  • go.mod
    • module 语句指定报的名字
    • require 语句代码指定依赖的模块
    • replace 可以替换
    • exclude 可以忽略
  • go.sum 文件来记录 dependency tree
  • mod包的缓存位置在 $GOPATH/pkg/mod/xxx
    • D:\nyy_work\go_path\pkg\mod\github.com\gin-gonic\gin@v1.7.2
    • 可以修改go.mod中的模块版本,再sync就可以

go get 拉取包的规则

  • go get -u 将会升级到最新的次要版本或者修订版本(x.y.z,x是主版本号, z是修订版本号, y是次要版本号)
  • go get xxx@version 升级到指定版本

使用go mod 发布一个我们自己的库

01 新建一个项目 叫common-tools

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net"
  6. "os"
  7. "strings"
  8. "time"
  9. )
  10. func GetNowTimeStr() string {
  11. return time.Unix(time.Now().Unix(), 0).Format("2006-01-02 15:04:05")
  12. }
  13. func GetHostName() string {
  14. name, _ := os.Hostname()
  15. return name
  16. }
  17. func GetLocalIp() string {
  18. conn, err := net.Dial("udp", "8.8.8.8:53")
  19. if err != nil {
  20. log.Printf("get local addr err:%v", err)
  21. return ""
  22. } else {
  23. localIp := strings.Split(conn.LocalAddr().String(), ":")[0]
  24. conn.Close()
  25. return localIp
  26. }
  27. }
  28. func main() {
  29. h := GetHostName()
  30. ip := GetLocalIp()
  31. t := GetNowTimeStr()
  32. fmt.Println(h, ip, t)
  33. }
  • 新建一个funcs.go ,塞进去一些常见的函数
  • 先将报名改为main ,加个main方法调用一下函数,看看好不好使,然后将报名改回common-tools

02 创建git,发布到github

  • 项目目录下 go mod init github.com/ning1875/common-tools
  • git init
  • 添加个.gitiiignore 文件去掉一些和代码无关的文件/文件夹
  • git add . && git commit -m “first”
  • github上新建一个仓库
  • 推送到远程
  1. git remote add origin https://github.com/ning1875/common-tools.git
  2. git branch -M main
  3. git push -u origin main

03 新创建一个项目,导入我们发布的mod

  • go get github.com/ning1875/common-tools
  • main.go
  1. package main
  2. import (
  3. "fmt"
  4. "github.com/ning1875/common-tools"
  5. )
  6. func main() {
  7. hostname := common_tools.GetHostName()
  8. ip := common_tools.GetLocalIp()
  9. ts := common_tools.GetNowTimeStr()
  10. fmt.Println(hostname, ip, ts)
  11. }
  • 注意项目中的go.mod 版本信息描述的是
  1. module new-pkg02
  2. go 1.16
  3. require github.com/ning1875/common-tools v1.0.1 // indirect

05 修改我们的代码 ,发布v1.0.2版本

  1. func GetHostName() string {
  2. name, _ := os.Hostname()
  3. return name +"v1.0.2"
  4. }
  • 本地新建一个分支
  1. git checkout -b v1
  2. git add .
  3. git commit -m "xxx"
  4. git tag v1.0.2
  5. git push --tags orgin v1
  • 到远程仓库检查 v1.0.2
  • 本地调用方 修改 go.mod 版本信息 改为v1.0.2
  • 调用方GetHostName就会变为v1.0.2版本
  1. SC-202003191609v1.0.2

06 主版本号变更

  • 版本号 v1.0.2 1代表主版本号 主版本号可能会不兼容
  • 0次版本号 2代表修正版本号
  • 把common-tools中的GetLocalIp由一个返回值改为 两个返回值
  • 库函数
  1. func GetLocalIp() (string, error) {
  2. conn, err := net.Dial("udp", "8.8.8.8:53")
  3. if err != nil {
  4. log.Printf("get local addr err:%v", err)
  5. return "", err
  6. } else {
  7. localIp := strings.Split(conn.LocalAddr().String(), ":")[0]
  8. conn.Close()
  9. return localIp, nil
  10. }
  11. }
  • 主版本变更的时候 go get不会去拉了
  • 手动get go get github.com/ning1875/common-tools/v2
  • path 改为了 module/v2 ,但是使用的时候还使用模块的名字
  • 调用方的go.mod
  1. module new-pkg02
  2. go 1.16
  3. require (
  4. github.com/ning1875/common-tools v1.0.2
  5. github.com/ning1875/common-tools/v2 v2.0.0 // indirect
  6. )
  • main.go
  1. package main
  2. import (
  3. "fmt"
  4. "github.com/ning1875/common-tools/v2"
  5. )
  6. func main() {
  7. //hostname := common_tools.GetHostName()
  8. ip, err := common_tools.GetLocalIp()
  9. //ts := common_tools.GetNowTimeStr()
  10. fmt.Println(ip, err)
  11. }

07 两个版本一起用,使用import 别名

  • 使用导入别名
  1. package main
  2. import (
  3. "fmt"
  4. cv1 "github.com/ning1875/common-tools"
  5. cv2 "github.com/ning1875/common-tools/v2"
  6. )
  7. func main() {
  8. //hostname := common_tools.GetHostName()
  9. ip, err := cv2.GetLocalIp()
  10. //ts := common_tools.GetNowTimeStr()
  11. fmt.Println(ip, err)
  12. ip = cv1.GetLocalIp()
  13. fmt.Println(ip)
  14. }

interal 包

  • go 1.5 及后续版本中,可以通过创建 internal 代码包让一些程序实体仅仅能被当前模块中的其他代码引用

如何组织项目

看源码

用 net/http/httptrace 写个http耗时探测的项目 simple-http-probe