Golang 00_Golang环境及包管理

一、Go 语言编译环境

1.1 Go 语言跨平台编译

1. 跨平台编译相关环境变量

  • GOOS: Go的编译目标操作系统
  • GOARCH: GO的编译目标CPU架构

src/go/build/syslist.go 中可查看支持的 OS 和 Arch 列表。

  • GOOS 常见列表:
  • linux
  • darwin
  • windows
  • freebsd
  • GOARCH 常见列表:
  • amd64
  • 386
  • arm # 只支持linux OS

2. 常见跨平台编译设置

  • Windows平台下为MacOS、Linux平台编译
1
2
3
4
SET CGO_ENABLED=0           #  禁用CGO
SET GOOS=darwin / linux     # 目标平台是 MacOS / linux
SET GOARCH=amd64            # 目标处理器架构是amd64
go build -o app             # 编译
  • Linux平台为Windows、MacOS平台编译
1
2
3
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o app

CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o app
  • MacOS平台为Windows、Linux平台编译
1
2
3
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o app

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app

1.2 Go 编译器指令简介

Golang 的编译器指令是以注释的形式存在的

1、//line filename:line 指令指定源代码中的文件名及行号

//line filename:line 是一个特殊的注释,用于指定源代码中的文件名及行号,它通常用于生成代码或者调试时帮助开发者更好地定位问题

例如,在调试一个使用了代码生成器的程序时遇到了问题,可以使用 //line 指令(注释)来查看生成的代码,示例:

1
2
3
4
5
6
7
package main

//line xxxx.go:10
var h1 notInHeap1

func main() {
}

编译输出:

1
2
192:~ mac$ go run .
xxxx.go:10: undefined: notInHeap1

//line 只是一个注释,不会影响代码的执行,它只是用于指定文件名和行号,以便在编译错误或调试时更好地定位问题。

2、//go:noinline 指令禁止当前函数内联

//go:noinline 的作用是禁止当前函数内联。inline是编译器优化代码的一种手段,将函数调用的地方替换为函数调用实际代码。

函数内联的优势如下:

  • 减少函数调用的开销,提升性能,如果要进入函数需要存储函数入栈和出栈;
  • 消除分支,充分利用CPU的cache的空间局部性和指令的顺序性;

同时也带来一些问题,如下:

  • 导致同一个函数存在多个调用替换,增加code size;
  • 如果代码过多,导致一个函数膨胀或者由于分支过多,可能降低缓存的命中率;

//go:noinline 的使用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

//go:noinline
func add(a, b int) int {
    var c = 1000
    return a + b + c
}

func main() {
    _ = add(1, 2)
}

汇编输出:

1
2
3
4
5
6
7
MOVD  $1, R0
MOVD  $2, R1
PCDATA  $1, $0
CALL  <unlinkable>.add(SB) // 如果没有加go:noinline,默认不会直接调用add函数,而是直接嵌入
LDP -8(RSP), (R29, R30)
ADD $32, RSP
RET (R30)

3、//go:noescape 禁止变量逃逸

//go:noescape 的作用是禁止变量逃逸。

逃逸 是指编译器自动地将超出自身生命周期的变量,从函数栈转移到堆中的行为。 Golang 自动将函数外部引用的变量转移到堆中,从而保障其它地方使用不会为空或者异常。

禁止逃逸优势:如果变量不逃逸,GC压力就会很小,提升性能; 禁止逃逸也带来一些问题:如果函数外部使用已经销毁的变量,就会异常或者core;

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 执行文件:main.go
import (
    "fmt"
)

//go:noescape
func noescape(d []byte) (b []byte)

func escape(d []byte) (b []byte)

// 汇编文件:main_amd64.s
// +build amd64

#include "textflag.h"

// func noescape(d []byte) (b []byte)
TEXT ·noescape(SB),NOSPLIT,$0-48
    MOVQ    d_base+0(FP),   AX
    MOVQ    AX,     b_base+24(FP)
    MOVQ    d_len+8(FP),    AX
    MOVQ    AX,     b_len+32(FP)
    MOVQ    d_cap+16(FP),AX
    MOVQ    AX,     b_cap+40(FP)
    RET

// func escape(d []byte) (b []byte)
TEXT ·escape(SB),NOSPLIT,$0-48
    MOVQ    d_base+0(FP),   AX
    MOVQ    AX,     b_base+24(FP)
    MOVQ    d_len+8(FP),    AX
    MOVQ    AX,     b_len+32(FP)
    MOVQ    d_cap+16(FP),AX
    MOVQ    AX,     b_cap+40(FP)
    RET

// 测试文件:main_test.go
package main

import (
    "testing"
)

func BenchmarkNoescape(b *testing.B) {
    buf := "hello world"
    for n := 0; n < b.N; n++ {
        noescape([]byte(buf))
    }
}

func BenchmarkEscape(b *testing.B) {
    buf := "hello world"
    for n := 0; n < b.N; n++ {
        escape([]byte(buf))
    }
}

noescape 函数 加上 go:noescapeescape 函数什么都不加,执行输出:

1
2
3
192:~ mac$ GOARCH=amd64 go test -bench .
BenchmarkNoescape-10     166318081          7.042 ns/op
BenchmarkEscape-10       40982848         27.66 ns/op

可以看出没有内存逃逸的noescape的性能要好很多。

4、//go:norace 跳过竞态检测

//go:norace 的作用是跳过竞态检测。

优势:由于Goroutine很方便写并行逻辑,但是往往我们再代码中需要通过go run -race xxx.go测试是否有竞态,会导致编译慢,如果加上 norace 就可以跳过;

同时也带来一些问题:需要开发者自己控制是否变量竞争的问题; 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

var sum int
//go:norace
func sumAdd() {
    sum++
}
func main() {
    go sumAdd()
    sumAdd()
}

没有加go:norace,执行会提示输出:

1
2
3
4
5
192:~ mac$ GOARCH=amd64 go run -race .
==================
WARNING: DATA RACE
Read at 0x000001166748 by goroutine 6:
...

加了go:norace,执行则直接不提示。

5、//go:nosplit 跳过栈溢出检测

//go:nosplit 的作用是跳过栈溢出检测。

栈溢出:Goroutine的起始栈大小是有限制的,且比较小的,可以做到支持并发很多Goroutine,并高效调度,如果当前栈不够用,则会自动扩展栈空间。

栈溢出也会引入性能问题,因为需要检查栈是否需要动态扩展,如果加上//go:nosplit,则跳过这个检查。

跳过栈溢出检测的优势:不执行栈溢出检查,提升性能; 同时也带来一些问题:可能导致stack overflow;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

//go:nosplit
func split() int {
    return split()
}
func main() {
    go sumAdd()
    sumAdd()
}

加上go:nosplit,执行go run main.go执行输出:

1
2
3
4
main.split: nosplit stack over 792 byte limit
main.split<1>
    grows 16 bytes, calls main.split<1>
    infinite cycle

6、 //go:linkname localname importpath.name

//go:linkname 的作用是编译器使用importpath.name作为源代码中声明为localname的变量或函数的目标文件符号名称。但是由于这个伪指令,可以破坏类型系统和包模块化,只有引用了 unsafe 包才可以使用。

简单来讲,就是 importpath.namelocalname 的符号别名,编译器实际上会调用 localname。使用的前提是使用了 unsafe 包才能使用。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// time/time.go
...
// Provided by package runtime.
func now() (sec int64, nsec int32, mono int64)

// runtimeNano returns the current value of the runtime clock in nanoseconds.
//
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64
...

在这个案例中可以看到 time.now函数 并没有具体的实现。通过全局搜索一下源码,就会发现其实现在 runtime 中 有具体实现:

1
2
3
4
192:T1 mac$ grep -r " time.now" /usr/local/go/src/ | grep "linkname" 
/usr/local/go/src//runtime/timeasm.go://go:linkname time_now time.now
/usr/local/go/src//runtime/time_fake.go://go:linkname time_now time.now
/usr/local/go/src//runtime/timestub.go://go:linkname time_now time.now

其中在 runtime/timestub.go 中的实现如下

1
2
3
4
5
6
7
8
9
...
import _ "unsafe" // for go:linkname 
...
//go:linkname time_now time.now 
func time_now() (sec int64, nsec int32, mono int64) { 
 sec, nsec = walltime() 
 return sec, nsec, nanotime() - startNano 
} 
...

配合//go:linkname 的用法解释,可得知在 runtime 包中,声明了 time_now 方法是 time.now 的符号别名。并且在文件头引入了 unsafe 达成前提条件。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
192:demo mac$ tree .
.
├── go.mod
├── hello
│   ├── hello.go
│   └── hello.s
├── link
│   └── link.go
├── main
└── main.go

2 directories, 6 files
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// main.go 
package main

import (
   "demo/hello"
)

//go:linkname nanotime1 runtime.nanotime1
func nanotime1() int64

func main() {
   fmt.Println("nanotime1: ", nanotime1())
   hello.Hello()
}

// hello/hello.go 
package hello

import (
    _ "demo/link"
)

func Hello()

// link/link.go 
package link

import(
    "fmt"
    _ "unsafe"
    // _ "demo/hello"
)

//go:linkname helloWorld demo/hello.Hello
func helloWorld() {
    fmt.Println("hello world!")
}

编译时报错:

1
2
3
192:demo mac$ go run main.go 
# demo/hello
hello/hello.go:7:6: missing function body

这是因为 go build 添加了 -complete 参数来检查完整性。提供.s文件后, 以便编译器绕过 -complete 的检查,允许不完整的函数声明。

在 hello 文件夹下添加任意的 .s 的汇编文件(例如 hello.s 或 xx.s)便可以通过编译执行:

1
2
3
4
192:demo mac$ touch hello/a.s
192:demo mac$ go run main.go 
nanotime1:  19825274663333
hello world!

注意:需要使用import _ "unsafe"引入unsafe包。

7、//go:notinheap 类型声明

//go:notinheap 的作用是类型声明,不允许当前类型在GC堆上申请内存,主要为了提升性能。

示例:

1
2
3
4
5
6
7
8
//go:notinheap
type mcache struct {
    // The following members are accessed on every malloc,
    // so they are grouped here for better caching.
    nextSample uintptr // trigger heap sample after allocating this many bytes
    scanAlloc  uintptr // bytes of scannable heap allocated
    ...
}

这个是mcache代码 //go:notinheap

8、//go:build //go:build是一个编译器指令,用于根据条件编译代码。语法:

1
//go:build condition

如果是支持linux并且是amd64,可以写 //go:build linux && amd64

1
2
3
4
//go:build amd64 !wasm
func main() {
    fmt.Println("nanotime1: ", nanotime1())
}

如果执行GOARCH=amd32 go run .后输出:

1
go: unsupported GOOS/GOARCH pair darwin/amd32

表示不支持当前类型,需要执行GOARCH=amd64 go run .才能编译通过。

9、其它编译指令

  • //go:systemstack 一个函数必须在系统栈上运行,这个会通过一个特殊的函数前引(prologue)动态地验证。
  • //go:nowritebarrier 告知编译器如果以下函数包含了写屏障,触发一个错误。
  • //go:yeswritebarrierrec 告知编译器如果以下函数以及它调用的函数(递归下去),直到一个go:yeswritebarrierrec为止,包含了一个写屏障的话,触发一个错误。

1.3 Go 条件编译

Go没有预处理,没有宏定义系统,不可以像c语言那样使用#define来控制是否包含平台相关的特定代码。作为替代,Go使用go/build包中定义的标签系统(system of tags)和命名约定(naming convention)以及go tool中的相应支持来允许Go包编译特定代码,即 条件编译

在某些场景下需要对进行编译的 Go 文件做限制,例如:

  • 在 Debug 时,希望编译的是 a.go 文件,而生产环境下希望编译的是 b.go 文件;
  • 在 linux 系统中,希望编译的是 a.go 文件,而 darwin 系统中个,系统编译的是 b.go 文件;

Golang支持两种 条件编译 的实现方式:编译标签(build tags)文件后缀(file postfix)

方式一:编译标签(build tags)

在 Go 文件中可以用类似注释的方式设置编译限制(条件编译指令),使用 // +build 编译器指令在源文件中的代码之前 设置 编译标签(build tags)。

例如文件 a.go 的源代码之前有如下内容:

1
// +build debug

则运行该文件必须带上 debug 标签,eg:

1
go run -tags debug main.go

需要注意的是该类标签只能写在文件中的源代码之前,并且其后需要跟一个空行才能生效。 以下是一个包含了许可证,构建标签,包声明的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// head headspin.go
// Copyright 2013 Way out enterprises. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build someos someotheros thirdos,!amd64

// Package headspin implements calculates numbers so large
// they will make your head spin.
package headspin

编译标签(build tags)遵循以下三个原则:

  • 每个选项由字母和数字组成,如果前面加上 ! 是非(NOT)的关系;

  • 逗号隔开的选项之间是与(AND)的关系;

  • 空格隔开的选项之间是或(OR)的关系;

  • ! 非(NOT):非标签标示的是编译或者运行时不可携带 debug 标签;

1
// +build !debug
  • , 与(AND):多个标签中的逗号代表 “与” 的含义,下述标签代表需要 taga 和 tagb 同时存在;
1
// +build taga,tagb

编译或运行时命令:

1
go build/run -tags="taga tagb" main.go
  • space 或(OR):多个标签中使用空格代表 “或” 的语义,下述标签表示需要 taga 或者 tagb 二者其中之一即可;
1
// +build taga tagb

标签可以写成多行,表示的仍然所示 “与” 的含义,如下:

1
2
// +build taga
// +build tagb

标签可以进行"与或非"语义的组合,如下:

1
// +build taga,tagb tagc,!tagd

该语义表示 (taga && tagb) || (tagc && !tagd)

方式二:文件名后缀(file postfix)

文件名后缀(file postfix) 的方式限定了运行和编译文件的系统和架构。这个方法通过改变文件名的后缀来提供条件编译,这种方案比编译标签要简单,go/build可以在不读取源文件的情况下就可以决定哪些文件不需要参与编译。

文件命名约定可以在go/build 包里找到详细的说明,简单来说如果源文件名包含后缀:_GOOS.go,那么这个源文件只会在这个平台下编译,_GOARCH.go也是如此。这两个后缀可以结合在一起使用,但是要注意顺序为:_GOOS_GOARCH.go,不能反过来用:_GOARCH_GOOS.go。

Go 源文件中,这三类特殊的模式会在编译时进行识别,分别是:

  • *_GOOS:例如 source_linux.go 表示该文件需要在 linux 系统才能够运行;
  • *_GOARCH:例如 source_amd64.go 表示该文件需要在 amd64 架构下才能够运行;
  • *_GOOS_GOARCH:例如 source_linux_amd64.go 表示该文件需要在 amd64 架构的 linux 系统中才能够运行。

条件编译小结

编译标签(build tags)文件名后缀(file postfix) 可作用于任何go tool可以编译的源码文件,包括.c和.s文件。Go标准库中,尤其是runtime,syscall,os,net包中包含了大量这种例子。

测试文件也支持 编译标签(build tags)文件名后缀(file postfix),它们的表现和Go源码文件表现一致。从而允许我们为特定平台指定特定测试用例。

1.4 go list 命令

go list 允许访问包内部的数据结构从而驱动编译过程(build process)。

go list 的大部分参数和 go buildgo testgo install 相同,但是它不会执行编译。使用 -f 格式化参数,可以填入一段 text/template的模板代码,它会在一个包含go/build.Package结构的上下文环境被执行。 如下例子,可以得到所有将被编译的源码文件名:

1
2
3
4
5
192:~ mac$ go list -f '{{.GoFiles}}' os/exec
[exec.go lp_unix.go]

192:~ mac$ go list -f '{{.GoFiles}}' fmt
[doc.go errors.go format.go print.go scan.go]

这个例子查询了在当前执行环境 linux/arm 下 os/exec包 和 fmt 中将会被编译的文件。结果文件有两类:

  • exec.go、doc.go 等无 _GOOS.go、_GOARCH.go 和 _GOOS_GOARCH.go 后缀名的源代码文件包含了在所有平台共享的通用代码;
  • lp_unix.go 是有 _GOOS.go、_GOARCH.go 或 _GOOS_GOARCH.go 后缀名的源代码文件包含了一份在unix-like系统独有的exec.LookPath实现;

如果在Windows系统执行上面那个命令,结果如下:

1
2
C:\go> go list -f '{{.GoFiles}}' os/exec
[exec.go lp_windows.go]

二、Go语言包

2.1 Go语言包简介

Go语言是使用包(package)来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io 等。

Go语言的包(package)借助了目录树的组织形式,一般包的名称就是其源文件所在目录的名称,虽然Go语言没有强制要求包名必须和其所在的目录名同名,但还是建议包名和所在目录同名,这样结构更清晰。

包(package)可以定义在很深的目录中,包名的定义是不包括目录路径的,但是包在引用时一般使用全路径引用。

Go语言的每一个代码文件都是属于一个包(package)的,go是以包的形式来管理的,一个源代码文件目录(不包括子目录)就是一个 包(package),一般目录名就是 包名(package name)

包的习惯用法:

  • 包名一般是小写的,使用一个简短且有意义的名称。
  • 包名一般要和所在的目录同名,也可以不同,包名中不能包含 - 等特殊符号。
    • 包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。
  • 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。

包(package)的作用:

1、区分同名字的函数、变量等标识符 2、当程序较多时,可以很好的管理项目 3、控制函数、变量的访问范围,即作用域

打包 和 导入包:

1
2
package 包名   // 通常是当前目录的名称
import 包名    // 通常会跟项目目录名称/包名

一般在每一个源代码文件的第一行使用 package 包的路径/包名 指令声明

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// cat utils/utils.go
package utils   // 将当前目录utils打包,其它包的文件通过该包名调用此包中的可见函数或变量等函数名

// 包里的函数名称必须首字母大写开才可以允许其它包中调用该函数
func Add(n1, n2 int) int{
    return n1 + n2
}

// cat main/main.go
package main
 
import (
    "fmt"
    "test/utils"  // 导入utils包, 本地导入要带上项目名称,我项目名称叫test
)
func main()  {
    res := utils.Add(12, 23)  // 调用utils包下的 Add 函数,格式(包名.函数名) —— utils.Add()
    fmt.Println(res)
}

2.2 包的引用格式

包的引用有四种格式,下面以 fmt 包为例来分别演示一下这四种格式。

1、标准引用格式

1
import "fmt"

此时可以用fmt.作为前缀来使用 fmt 包中的方法,这是常用的一种方式。

1
2
3
4
5
package main
import "fmt"
func main() {
    fmt.Println("ms的go教程")
}

2、自定义别名引用格式

在导入包的时候,我们还可以为导入的包设置别名,如下所示:

1
import F "fmt"

其中 F 就是 fmt 包的别名,使用时可以使用 F. 来代替标准引用格式的 fmt. 来作为前缀使用 fmt 包中的方法

1
2
3
4
5
package main
import F "fmt"
func main() {
    F.Println("ms的go教程")
}

3、省略引用格式

1
import . "fmt"

这种格式相当于把 fmt 包直接合并到当前程序中,在使用 fmt 包内的方法是可以不用加前缀 fmt.,直接引用。

1
2
3
4
5
package main
import F "fmt"
func main() {
    Println("ms的go教程")
}

4、匿名引用格式

在引用某个包时,如果只是希望执行包初始化的 init 函数,而不使用包内部的数据时,可以使用匿名引用格式,如下所示:

1
import . "fmt"

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

使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过 import _ "包的路径" 这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。

1
2
3
4
5
6
7
8
package main
import (
    _ "database/sql"
    "fmt"
)
func main() {
    fmt.Println("ms的go教程")
}

注意:

  • 一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init 函数,将需要初始化的逻辑放到一个 init 函数里面。
  • 包不能出现环形引用的情况,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
  • 包的重复引用是允许的,比如包 a 引用了包 b 和包 c,包 b 和包 c 都引用了包 d。这种场景相当于重复引用了 d,这种情况是允许的,并且 Go 编译器保证包 d 的 init 函数只会执行一次。

2.3 包使用细节

  1. 包名通常和文件所在的文件夹(目录)名一致,一般为 小写字母 比如 utils 文件夹,对应的包名就是 utils

  2. 当一个文件要使用其它包函数或变量时,需要先引入对应的包,再使用 包的引入方式: import 包名import (包名1,包名2)

    • import包时,路径从$GOPATHsrc下开始(不用带src,编译器会自动从src下开始引入);或者从当面项目目录开始;
  3. 为了让其它的包文件,可以访问到本包的函数,则该函数名 的 首字母需要大写; 同理,如果需要调用其它包的 常量/变量名 也必须要首字母大写

  4. 在调用其它包函数时,其语法是 包名.函数名; 比如上面的的main.go文件中 调用 utils 包的 Add 函数:

    1
    
    utils.Add(12, 23)
    
  5. 如果包名较长,Go支持给包起别名,注意细节,取别名后,“原来的包名就不能用了”。

  6. 在同一个"包"下,不能有完全相同的函数名、常量、变量,否则包内标识符重复定义。

  7. 如果要把一个项目(代码)编译成一个可执行文件,就需要这个项目下有一个包声明为 main包 并且该包内包含 main 函数实现 ,即package main + func main() {...},这个就是一个语法规范,如果是写一个库,包名可以自定义。

三、Go 语言依赖包管理的核心(Go Modules)

3.1 go mod 简介

go mod 是 Go 语言的简化依赖的管理的工具,是Go v1.11版引入的,用于依赖管理的官方解决方案。并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。

go mod主要解决了这些问题

  • 依赖关系混乱,不清楚具体使用了哪些包和版本
  • 不同机器无法复现相同的依赖构建,导致构建失败
  • 上传代码时缺失依赖,导致他人无法构建

在Go1.11后新增了modules特性,模块是相关Go包的集合。如果在cmd中执行以下命令将GO111MODULE变量的值设为on:

1
2
3
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct
go env -w GOPROXY=https://goproxy.cn,direct(国内的七牛云提供)

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

  • GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
  • GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
  • GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:
    • 当前目录在GOPATH之外且该目录包含go.mod文件 开启
    • 当处于 GOPATH 内且没有 go.mod 文件存在时其行为会等同于 GO111MODULE=off
  • 如果不使用 Go Modules, go get 将会从模块代码的 master 分支拉取
  • 而若使用 Go Modules 则可以利用 Git Tag 手动选择一个特定版本的模块代码

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(解释为什么需要依赖)

go mod 的主要功能有

  • 使用 go.mod 文件记录依赖信息,根据 import 自动添加依赖
  • 自动下载依赖包并缓存在 GOPATH/pkg/mod 目录
  • 可以查询、添加、、更新、删除依赖包
  • 可以指定依赖版本,锁定依赖版本
  • 可以设置代理,实现国内下载加速

go mod 基本概念

  • module: 一个模块,通常对应一个代码仓库,一个 module 拥有独立的依赖信息
  • package: 一个 module 中的代码包,通常是一个目录(不包含子目录)一个package
  • dependency: 依赖包,一个 module 需要引用的其它 module

一个 module 拥有一个唯一的 module path 以及一个版本号。

go mod 工作原理

  • go mod 通过 go.mod 文件记录当前 module 的元信息和所有依赖项信息。
  • 然后会自动下载并缓存依赖的包,缓存放在 GOPATH/pkg/mod 目录。
  • 通过 go mod 管理依赖,就可以很方便地重现相同的依赖环境。

go mod 命令参考

1
2
3
4
5
6
go mod init [package_path_name] # 初始化一个新模块,这将在当前目录生成一个 go.mod 文件,该文件描述了模块的路径和依赖关系,
go mod edit     # 编辑 go.mod 文件
go mod download # 下载所有依赖
go mod tidy     # 清理和解决依赖问题
go mod verify   # 校验依赖
go mod graph    # 将依赖图生成 DOT 文件,然后使用 Graphviz 工具生成依赖关系图。

3.2 创建 go.mod 文件

go.mod 文件存储在模块根目录,记录了当前模块的元数据,包括模块路径、依赖列表等。 使用 go mod init 来初始化 go.mod 文件

1
go mod init example.com/module  # 初始化一个新模块,这将在当前目录生成一个 go.mod 文件,该文件描述了模块的路径和依赖关系。

这会在模块根目录生成一个 go.mod 文件

1
2
3
module example.com/module 

go 1.17.5

也可以通过 go build、go run 等命令自动创建 go.mod 文件。

3.3 查询依赖 及 生成依赖关系图

1
2
3
4
5
go list -m all              # 使用 go list -m all 可以列出当前 module 的所有依赖
go list -json               # 可以输出 JSON 格式的数据
go list -json all

go mod graph > graph.dot    # 使用 go mod graph 可以将依赖图生成 DOT 文件, 然后使用 Graphviz 工具生成依赖关系图。

3.4 添加、更新依赖

使用 go get 可以下载依赖

1
2
3
4
5
go get github.com/gin-gonic/gin     ## 这会自动找到合适的版本并写入 go.mod 文件。

# 如果需要指定版本, 可以
go get github.com/x/text@v0.3.0
# 导入包时,如果发现本地没有该依赖包,也会自动下载。

使用 go get -u 可以升级依赖包到最新的次要版本。 也可以手动修改 go.mod Require 语句指定版本后,运行 go get 导致更新。

1
go get -u example.com/some/module@v1.2.3    # 使用 go get 命令升级或降级模块依赖。

3.5 删除依赖

go mod tidy 可以移除不用的依赖项。 也可以直接编辑 go.mod 文件,删除不需要的 module 语句, 然后执行 go mod tidy 重新整理。

3.6 go mod 原理解析

go.mod 使用语义版本规范,可以记录最小和最大版本号。 环境变量控制代理,实现国内代理加速。 go mod 通过元数据和缓存实现可重现的依赖构建。

3.7 代理设置

GOPROXY 环境变量控制下载代理

1
export GOPROXY=https://goproxy.cn

GOPRIVATE 控制不走代理的私有模块。

Licensed under CC BY-NC-SA 4.0
最后更新于 2022-08-03 21:33 CST