15.包管理

15.1 模块化

    用任何语言开发,如果软件规模扩大,会编写大量的函数、结构体、接口等。而这些代码不可能全部写在同一个文件中,因此就会产生大量的文件。如果这些文件是杂乱无章,则会造成名称冲突、重复定义、难以检索、无法引用、共享不便、版本管理、代码如何复用等一系列问题。因此需要有方案来解决类似问题,Go里面的方案是采用模块化管理来解决前述问题。

15.2 包

    模块,有时也称为包,它是某个功能或某个框架的基本单位。任何编程语言都会有一些内置包,它将一些基本功能封装为包形式提供给开发者使用,Go语言内置包可以在安装目录src文件夹查看。其主要特点如下所示:

  • 包由多个文件目录组成
  • 使用关键字package 包名来定义包名
  • 包名一般使用小写且符合标识符要求
  • 当前目录名和package 包名中的包名不需要一致,但根据约定俗成原则,则建议保持一致
  • 同级文件归属一个包,即每个包目录的当前目录中,只能统一使用同一个包名,否则则会编译出错

    一般来说开发项目时,可以把功能相关的代码集中放置一个包里面。同一个目录就是同一个包,该包里面的变量、函数、结构体均互相可见,也可以直接使用。而跨目录则是跨包,使用时需要导入相应的包。

15.3 包管理

15.3.1 GOPATH

    Go 1.11版本之前,项目依赖包存于GOPATH。GOPATH是一个环境变量,它指向一个目录,用于存放项目依赖包的源码。其默认值为$HOME/go。开发的代码在GOPATH/src目录中,编译这个目录的代码,生成的二进制文件放置GOPATH/bin目录中。但存在以下问题:

  • GOPATH不区分项目,代码中任何导入的路径均从GOPATH作为根目录开始。如果存在多个项目,不同项目依赖不同的库的不同版本,则无法解决
  • 所有项目的依赖都放在GOPATH中,则很难知道当前项目的依赖项有哪些

15.3.2 GOPATH+vendor机制

    Go 1.5 引入Vendor机制。Vendor是将项目依赖包复制到项目中的vendor目录,在编译时使用项目中vendor目录的包进行编译,但依然不能解决不同项目依赖不同包版本问题。其中包的搜索顺序如下所示:

  • 在当前包vendor目录中查找
  • 向上级目录查找,直到GOPATH/src/vendor目录
  • GOPATH目录查找
  • 在GOROOT目录查找标准库

15.3.3 Go Modules

    Go Modules 是从Go 1.11版本之后引入,到1.13版本之后已经成熟,因此Go Modules已经成为官方的依赖包管理解决方案。其优势如下所示:

  • 不受GOPATH的限制,代码可放在任意目录
  • 自动管理和下载依赖,且可以限制使用版本
  • 不允许使用相对导入

    我们通过GO111MODULE配置来控制Go Module模式是否开启,有以下三个选项

  • on:开启Go Module功能,Go会忽略GOPATH和vendor目录,仅根据go.mod下载依赖项,在GOPATH/pkg/mod目录中搜索依赖包

Go 1.13版本之后,默认开启。

  • off:关闭Go Moudule功能,Go会从GOPATH和vendor目录中搜索依赖包
  • auto:在GOPATH/src外面构建项目且时,若根目录中存在go.mod文件时,则开启支持Go Module功能,否则使用GOPATH和vendor机制

15.4 常用内置包

    Go语言为我们提供了很多内置包,以下为一些日常开发常用的包,如下所示:

  • fmt包实现了格式化的标准输入输出,与C语言的printf和scanf类似。其中fmt.Printf()fmt.Println()是开发中使用最频繁的函数。
  • io包提供了原始的I/O操作,定义了4个基本接口ReaderWriterCloserSeeker用于表示二进制流的读、写、关闭和寻址操作。这些接口封装底层操作,如果没有特殊说明,接口不能被视为线程安全
  • bufio包通过对io包的封装提供了数据缓冲功能,一定程度上减少了大数据读写带来的资源开销
  • sort包为切片或自定义Map提供了排序功能,实现了4种排序算法插入排序归并排序堆排序快速排序,依据数据结构自动选择最优的排序算法。
  • strconv包实现基本数据类型的转换,如字符串与整型相互转换功能。
  • os包实现了操作系统的访问功能,包括文件操作、进程管理、信号等功能
  • sync包实现了多线程的锁机制以及同步互斥功能。
  • flag包提供命令行参数定义和参数解析的功能
  • encoding包提供将某些数据转换为特定数据的功能,例如将数据转换为JSON、CSV、XML、base64等
  • html/template包为WEB开发提供了模板语法功能,通过模板语法将Go语言某类数据类型转换为相应的HTML代码
  • net/http包提供了HTTP服务,包括HTTP请求、响应和URL解析、HTTP客户端和服务端。
  • strings包提供了字符串操作处理,包括字符串合并、查找、分割、比较、扩展名检查、索引、大小写等功能
  • bytes包与strings包功能相同,仅仅是bytes包用于处理字节类型的切片数据
  • reflect包提供了Go语言中的反射功能
  • log包提供程序的日志功能,常用日志输出接口为PrintFatalPanic
  • time包提供时间和日期功能
  • testing包为Go语言提供了单元测试功能
  • regexp包提供了正则表达式功能
  • math包提供了基本的数学常量和运算函数

15.5 包命名

    在Go语言中,包是以文件夹形式表现的。其语法如下所示:

package 包名

    包命名规则如下所示:

  • package:是Go语言的关键字,用于指定当前文件所属包
  • 包名:代表包名
  • 一个文件夹里面的所有go文件都属于同一个包

在Go语言中有一个非常特殊的包main包,它不能被其他包导入,且里面必须存在main函数,否则程序没有入口,则无法运行。

15.6 包导入

    在Go语言中使用import导入包。导入的包只能是该包里面的导出标识符。其中导出标识符可以是变量、常量、类型、函数或方法、接口等。

    每个包在项目中都有唯一的导入路径,导入路径是告诉Go语言从哪里找到包,导入路径和包名称之间没有必然联系。如果一个程序需要导入多个包,每个包可以单独使用关键字importimport + 小括号 实现,包与包之间必须处于不同的行,示例如下所示:

// 导入包示例一:
import "fmt"
import "math"

// 导入包示例二:
import (
	"fmt"
	"math"
)

一般推荐方式二,代码比较简洁

    下面来演示一下,目录结构如下所示:

1501-导入包目录结构示例.png

    示例代码如下所示:

  • calc.go
package calc

import "fmt"

func Add[T int | float32 | float64 | string](x, y T) T {
	return x + y
}

func Div[T int | float32 | float64](x, y T) T {
	if y == 0 {
		fmt.Println("除数为0")
	}
	return x / y
}
  • log.go
package log

import "log"

func PrintInfo(message any) {
	log.Print(message)
}

func PrintError(message any) {
	log.Fatal(message)
}

15.6.1 绝对导入

package main

import (
	"fmt"

	"surpass.net/tools/calc"
)

func main() {
	fmt.Println(calc.Add(10, 20))
}

15.6.2 别名导入

    如果在导入的包里面存在同名的包,则可以使用别名导入来避免冲突。在对包进行命名别名时,新名字必须在包前面,且两者之间使用空格间隔,示例使用方法如下所示:

package main

import (
	"fmt"

	c "surpass.net/tools/calc" // 将导入的包重新命名为 c
)

func main() {
	fmt.Println(c.Add("100", "200")) // 通过别名调用包里面的导出标识符
}

注意事项: 别名导入的包仅在当前go文件中有效,其他go文件在导入时还是以原有名称导入

15.6.3 相对导入

    这种方式不推荐使用

package main

import (
	"fmt"

	c "./tools/calc"
)

func main() {
	fmt.Println(c.Add("100", "200"))
}

如果是在启用了go.mod的环境使用相对导入,会提示main.go:6:2: "./tools/calc" is relative, but relative import paths are not supported in module mode

15.6.4 点导入

    这种方式适用于将包里面所有导出成员直接导入到本地,即在调用包里面的导出标识符时无须通过包名调用,但有可能会导致导入的标识符存在冲突,且同一个包在一行时不能同时使用别名导入和点导入。仍然不推荐使用

package main

import (
	"fmt"

	. "surpass.net/calc"
)

func main() {
	fmt.Println(Add("100", "200"))
	fmt.Println(Add(12, 13))
	fmt.Println(Div(12, 6))
}

15.6.5 匿名导入

    这种方式类似于别名导入,区别就是该别名为_,通过这种方式导入的包,则意味着该包无法使用。通常适用于在导入的包仅执行导入包里面的init()函数,其主要作用是做包的初始化。示例如下所示:

import (
	"fmt"

	_ "surpass.net/tools/calc"
)

15.6.6 导入本地其他项目

    如果将calc包放到其他项目时,如何导入呢?例如将calc放置到x:\calc,同时在calc目录使用go mod init surpass.net/calc。其go.mod内容如下所示:

  • calc 目录的go.mod内容
module surpass.net/calc

go 1.22.5
  • main.go中的go.mod需要按以下进行修改
module surpass.net

go 1.22.5


require (
    surpass.net/calc v1.0.0 // 后面的版本号可以随便定义,满足格式要求即可
)

replace surpass.net/calc => "F:\\calc" // replace 指令指定包的搜索路径,而不是去GOPATH/pkg/mod搜索

    go.mod一些参数的详细说明如下所示:

  • require:用于设置一个特定的模块版本
  • indirect:表示该模块为间接依赖,即在当前应用程序中的import语句中,并没有发现这个模块的明确调用,可能是事先通过go get下载,也可能是依赖的模块所依赖的
  • exclude:用于排除使用中的一个特定的模块版本
  • replace:用于将一个模块版本替换为另外一个模块版本

参考文档:https://golang.google.cn/ref/mod#go-mod-file-require

    在main.go中用法如下所示:

package main

import (
	"fmt"

	"surpass.net/calc"
)

func main() {
	fmt.Println(calc.Add("100", "200"))
	fmt.Println(calc.Add(12, 13))
	fmt.Println(calc.Div(12, 6))
}

    代码运行结果如下所示:

$ go run main.go
100200
25
2

15.6.7 导入第三方包

    日常开发过程中,除了自己写的包,也还有可能用到其他第三方的包,可以通过 https://pkg.go.dev/ 进行搜索和下载(例如:go get -u github.com/gin-gonic/gin),导入第三方包的示例代码如下所示:

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

    在下载第三方包之后,会生成一个go.sum文件,在里面详细列举当前项目直接或间接依赖的所有模块版本,同时也带有详细的SHA-256值,以确保Go在售后的操作中保证项目所依赖的模块版本不会被修改。

15.7 init函数

    Go语言中有一个特殊的函数init(),它的执行优先级比main()函数要高,主要实现包的初始化。其具备以下特征:

  • init()函数,既无参数也无返回值,不能被其他函数调用
  • 包中的init()函数在main()函数之前执行
  • 每个包中init()函数可以存在多个,且可以位于不同的文件中
  • 一个文件中最多拥有一个init()函数
  • 同一个包中的init()函数没有明确的执行顺序
  • 不同包中的init()函数的执行顺序由导入顺序决定

    由于init()函数主要功能是初始化一些操作,但由于同一个包里面init()函数执行顺序是无序的。因此,除非有必要,不要在同一个包里面定义多个init()函数。另外,init()函数和main()函数也不一定在同一个文件夹里面。

  • import _ "package": 仅执行该包里面的init()函数,无法使用包里面的导出标识符
  • import "package": 既执行该包里面的init()函数,也可以使用包里面的导出标识符

    示例代码如下所示:

  • calc.go
package calc

import "fmt"

func init() {
	fmt.Println("package calc 中的 init()函数")
}

func Add[T int | float32 | float64 | string](x, y T) T {
	return x + y
}

func Div[T int | float32 | float64](x, y T) T {
	if y == 0 {
		fmt.Println("除数为0")
	}
	return x / y
}
  • log.go
package log

import (
	"fmt"
	"log"
)

func init() {
	fmt.Println("package log 中的 init()函数")
}

func PrintInfo(message any) {
	log.Print(message)
}

func PrintError(message any) {
	log.Fatal(message)
}
  • main.go
package main

import (
	"fmt"

	_ "surpass.net/tools/calc"
	_ "surpass.net/tools/log"
)

func main() {
	fmt.Println("main - 测试init()函数")
}

    代码运行结果如下所示:

package calc 中的 init()函数
package log 中的 init()函数
main - 测试init()函数

本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:

posted @ 2025-09-07 17:10  Surpassme  阅读(10)  评论(0)    收藏  举报