Golang 01_Golang基础

一、Golang关键字集

1.1 Golang关键字集简介

Go语言有25个预留的关键字,不能用作标识符。以下是这些关键字的列表以及它们的简要解释:

关键字 代码示例 代码作用 功能
package package main 定义包名为 main 定义包
import import “fmt” 导入 fmt 包 导入包
const const PI = 3.14159 定义常量 PI 定义常量或常量组
var var name string = “John” 定义一个名为 name 的变量 声明变量或变量组
func func add(x, y int) int { return x + y } 定义名为 add 的函数 定义函数/方法
map m := map[string]int{“one”: 1} 创建一个键为字符串、值为整数的映射
chan ch := make(chan int) 创建一个新的整数类型的通道
type type Point struct {x, y int} 定义一个名为 Point 的结构体 定义新的数据类型、接口或类型别名
struct type Circle struct { Radius float64 } 定义一个名为 Circle 的结构体
interface type Writer interface { Write([]byte) error } 定义一个名为 Writer 的接口
if if x > 0 { // } 条件为 x > 0 时,执行某个操作
else if x > 0 { // } else { // } if 语句的否定分支
switch switch x { case 1: // } 多分支选择
fallthrough case 1: fallthrough 在 switch 语句中,强制执行下一个 case 用于switch语句,使得控制流穿透到下一个case
for for i := 0; i < 10; i++ { // } 循环10次
range for k, v := range m { // } 遍历映射 m 用于循环遍历数组、切片、字符串、map或通道
continue if x < 0 { continue } 如果 x < 0,则跳过当前循环迭代
break if x > 0 { break } 如果 x > 0,则跳出循环 用于中断循环或switch语句
select select { case <-ch: //} 多路通道选择
case case “apple”: fmt.Println(“It’s an apple”) 在 switch 语句中,定义一个 case 用于switch或select语句中的分支
default default: fmt.Println(“It’s default”) 在 switch 语句中,定义一个默认 case 用于switch或select语句,表示默认情况
goto goto myLabel 跳转到 myLabel 标签(不推荐)
go go doSomething() 在新的 goroutine 中执行 doSomething() 函数
defer defer fmt.Println(“bye”) 延迟执行 fmt.Println(“bye”) 直到函数退出
return return x + y 从函数中返回 x + y

二、Golang数据类型

2.1 Golang数据类型简介

Golang 中的数据类型按数据类别(kind)分为以下几种数据类型:

  • 布尔型:布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。
  • 数字类型:整型 int 和 浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中数据类型的位运算采用补码。
  • 字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
  • 派生类型:包括 指针类型(Pointer)数组类型(Array)切片类型(slice)Map 类型结构化类型(struct)Channel 类型函数类型接口类型(interface)

2.2 Golang数据存储方式

Golang 中数据类型按存储方式 有两大类数据类型:

  • 值类型: 也叫基本数据类型, 包括数值类型、bool、string、数组(array)、结构体(struct);
  • 引用数据类型:指针、slice切片、chan管道、map映射、以及 interface接口;

值类型:变量直接存储值,值类型的数据一般存储在栈内存空间中,栈在函数调用完成(返回时)栈内存会被回收释放。 引用类型:变量存储的是一个内存地址,这个内存地址存储最终的值,引用数据类型的数据一般存储在堆内存空间中,通过 GC 回收。

2.3 Golang基本数据类型

1、bool 类型

Golang 中以 bool 关键字声明布尔类型数据,布尔型的值只可以是 true 或者 false。代表条件成立(真)或条件不成立(假),bool类型有以下几个特性;

  • 布尔类型变量的默认值为 false;
  • Golang 中不允许将整型强制转换为布尔型;
  • 布尔型无法参与数值运算,也无法与其它类型进行转换;

2、Golang数值类型

1)整型

整型就是整数,不同的整数类型,占用的内存空间也不同,主要有 int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64;

有符号整型:

  • int8: 有符号 8 位整型 -128(-2^7^) 到 127(2^7^-1)
  • int16: 有符号 16 位整型 -32768(-2^15^) 到 32767(2^15^-1)
  • int32: 有符号 32 位整型 -2147483648(-2^31^) 到 2147483647(2^31^-1)
  • int64: 有符号 64 位整型 -9223372036854775808(-2^63^) 到 9223372036854775807(2^63^-1)
  • int:有符号整型,在 32 位系统上通常为 32 位整型;在 64 位系统上则为 64 位整型

无符号整型:

  • uint8: 无符号 8 位整型 0 到 255(2^8^-1)
  • uint16: 无符号 16 位整型 0 到 65535(2^16^-1)
  • uint32: 无符号 32 位整型 0 到 4294967295(2^32^-1)
  • uint64: 无符号 64 位整型 0 到 18446744073709551615(2^64^-1)
  • uint:无符号整型,在 32 位系统上通常为 32 位整型;在 64 位系统上则为 64 位整型

其它整数类型:

  • byte:与uint8等价(uint8的别名),用于存储 ASCII码 字符,详见下一篇《Golang字符串》讲解;
  • rune:与 int32一样(int32的别名),表示一个 unicode码,详见下一篇《Golang字符串》讲解;
  • uintptr:用于存储指针(内存地址),在 32 位系统上通常为 32 位宽;在 64 位系统上则为 64 位宽;

Go 语言的 intuintuintptr 类型 是不可移植的类型,其长度根据宿主机的机器字长决定:

  • 在 32 位系统上通常为 4字节 32位宽;
  • 在 64 位系统上则为 8字节 64位宽;

Go 语言中可以使用 type 关键字来给已经存在的(基本数据类型 或 自定义的 struct、interface、func 等)类型取别名,语法为:

1
type TypeAlias = Type
  • TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。
  • Type可以是一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型,也可以是已经定义过(已经存在)的struct、interface等。

例如在 Golang中 byte 和 rune 的定义如下:

1
2
type byte = uint8
type rune = int32

Go 语言中整数默认声明为int型:

1
2
3
4
5
6
n0 := 10     // n0是 int类型
var n1 = 100 // n1是 int类型

n2 := int16(10)     // n2是 in16t类型
var n3 int32 = 100  // n3是 int32类型
n4 := n3            // n4是 int32类型

查看变量的字节大小和数据类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main
 
import (
    "fmt"
    "unsafe"   //引入包
)
 
func main()  {
    var n2 int64 = 10
 
    // unsaf.Sizeof(n2) 是unsafe包的一个函数,可以返回n2变量占用的字节数
    fmt.Printf("n2 的数据类型是 %T, n2占用的字节数是 %d ", n2, unsafe.Sizeof(n2))
}

2)浮点型

浮点数类型就是带小数点的数值类型,有 float32、float64;

  • float32: IEEE-754 32 位浮点型数
  • float64: IEEE-754 64 位浮点型数

浮点数在机器中存放的形式:浮点数= 符号位+指数位+尾数位 浮点数都是有符号的数据类型

浮点数使用细节:

  • 通常情况下应该使用 float64,因为它比 float32 更精确
  • Golang 的浮点型默认声明为 float64 类型
  • 浮点型常量有两种表示形式:
    • 十进制数形式:如 5.12、.512(必须有小数点,0.512)
    • 科学计数法形式:如 5.1234e2 = 5.1234 * 10^2^、5.12E-2 = 5.12/(10^2^) (e / E 等效)

3)复数

  • complex64: 32 位实数和虚数
  • complex128: 64 位实数和虚数

4)强制类型转换

Golang 中只有强制类型转换,没有隐式类型转换。强制类型转换语法只能在两个类型之间支持相互转换的时候使用。

强制类型转换 的基本语法为:type(variable),可转换的数据包括变量、表达式、函数返回值等。 如下代码:

1
2
3
var a int = 8
// 将int类型改为int32类型
var b int32 = int32(a)

3、Golang字符串类型

1)字符类型

Golang中使用 byte类型 存储单个(ASCII码)字符;

1
2
var c1 byte = 'a'
var c2 byte = '0'

Tips: byteuint8 等价,用于存储 ASCII码 字符

字符类型使用细节:

  • 字符常量是用单引号(’’)括起来的单个字符 例如: var c1 byte = 'a'
  • GO中允许使用转义字符 '\' 来将后面的字符转换为特殊字符 例如:var c3 char = '\n' \n 表示换行符
  • Go语言的字符使用UTF-8编码(兼容ASCII码
  • 在Go中,字符的本质是一个整数,直接输出字符时,输出的是该字符在UTF-8编码表中对应的编码值
  • 可以直接给某个变量赋一个整数值,然后按 %c 格式化输出时会输出该数字对应的UTF-8编码字符
  • 字符类型是可以进行运算的,相当于一个整数,因为它们都对应有Unicode码

Tips: UTF-8 编码是在互联网上使用最广的一种 Unicode 编码的实现方式;UTF-8 是一种变长的编码方式,编码规则如下:

  • 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 的码点,兼容 ASCII
  • 对于需要 n 字节来表示的符号(n > 1),第一个字节的前 n 位都设为 1,第 n+1 位设置为 0;后面字节的前两位一律设为 10,剩下的的二进制位则用于存储这个符号的 Unicode 码点(从低位开始)。

编码规则如下表:

Unicode符号范围(十六进制)
UTF-8编码方式(二进制)
00000000 - 0000007F 0xxxxxxx
00000080 - 000007FF 110xxxxx 10xxxxxx
00000800 - 0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
00010000 - 0010FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

2)字符串类型

字符串定义:var str string

字符串一旦赋值,就不能修改,在go中字符串是不可变的

字符串有两种表示方式:双引号("");反引号,也叫原意符号(``),如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
    var str1 = "hello world"
    var str2 = `
    窗前明月光
    疑是地上霜
    举头望明月
    低头思故乡
    `

    fmt.Println("str1 = ", str1)
    fmt.Println("str2 = ", str2)
}

Tips: 反引号 定义的字符,会以字符串的原生形式输出,包括换行符和特殊字符,有防止攻击、输出源代码等效果;

4、基本数据类型的默认值

基本数据类型的变量,在声明后的默认值:

数据类型
变量默认值
整型 0
浮点型 0
字符串 ""
布尔型 false

2.4 Golang自定义数据类型

1、自定义数据类型简介

Go 语言中使用 type 关键字来定义自定义类型。 使用 type 关键字可以以一些基本的数据类型(如 string、整型、浮点型、布尔等数据类型)定义出一个新的数据类型,也可配合使用structinterface 关键字定义新的复合类型。

1
2
3
4
5
6
7
type Age uint8 // 使用 uint8 定义 Age类型

type Person struct { // 配合使用 struct 定义结构体复合类型 
    name string
    age  Age   // 前面定义的 Age 类型,底层是uint8类型
    sex  string
}

1、结构体

结构体(struct) 是 Go 语言中对数据进行抽象和封装的主要方法。

2、接口(interface)

在Go中,接口(interface)是一种抽象类型,用于定义对象的行为,它没有属性,是仅包含方法名、参数、返回值的未具体实现的方法的集合。 接口(interface)定义了一类对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

2.5 Golang数据类型的底层实现

在 Go 语言中,类型(Type) 是用来描述数据的属性和操作的概念,它定义了数据的内部表示以及对数据进行操作的方法。类型(Type) 在编程语言中起到了限制和约束数据的作用,它决定了数据的取值范围、可用的操作,以及数据在内存中的布局方式。

在 Go 语言中,每个值都有一个明确的类型。例如,整数类型(int)、浮点数类型(float64)、布尔类型(bool)、字符串类型(string)等都是 Go 语言中的内置类型。

此外,我们还可以自定义结构体类型(struct)、接口类型(interface)和 函数类型(func),以及通过类型别名(type)来基于已有类型(基础类型或自定义类型)创建新的自定义类型。

在类型中都有一些公有属性,例如类型的大小、对齐方式、哈希值、标志位、种类、相等性函数、垃圾回收数据、名称和指针等是所有类型最原始的元信息。这些元信息,记录在位于 src/runtime/type.go_type 结构体中,作为每个类型元数据的Header 结构:

 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
// src/runtime/type.go
type _type struct {
    size       uintptr  // 表示类型的大小,即占用内存的字节数

    // 类型中指针数据的大小,以字节为单位,
    // 这个字段用于垃圾回收器识别类型中哪些部分是指针,哪些部分是非指针
    ptrdata    uintptr  
    hash       uint32   // 类型的哈希值,用于在运行时比较两个类型是否相同
    tflag      tflag    // 类型的标志位,用于存储一些额外的信息,如是否有名称、是否有不可比较的字段等
    align      uint8    // 类型的对齐方式,以字节为单位。这个字段决定了类型在内存中的布局和对齐方式
    fieldAlign uint8    // 类型的字段对齐方式,以字节为单位。这个字段决定了类型中的字段在内存中的布局和对齐方式
    kind       uint8    // 类型的种类,用于区分不同的基本类型,如 int、string、struct 等

    // equal类型的相等性函数,用于在运行时比较两个值是否相等,
    // 如果为 nil,则表示该类型没有定义相等性函数,或者该类型是不可比较的
    equal      func(unsafe.Pointer, unsafe.Pointer) bool 
    gcdata     *byte   // 类型的垃圾回收数据,用于存储一些与垃圾回收相关的信息,如指针位图等

    // 类型的名称偏移量,用于在运行时获取类型的名称。这个字段是一个相对于 _type 结构体起始地址的偏移量,
    // 可以通过它找到一个 nameOff 结构体,进而找到一个 name 结构体,其中存储了类型的名称
    str        nameOff    
    
    // 类型的指针偏移量,用于在运行时获取指向该类型的指针类型,
    // 这个字段也是一个相对于 _type 结构体起始地址的偏移量,可以通过它找到一个 typeOff 结构体,
    // 进而找到一个 _type 结构体,其中存储了指向该类型的指针类型
    ptrToThis  typeOff                                   
}

Tips: _type 结构体是 Go 语言中实现 反射机制接口机制 的基础

  • 反射机制 可以在运行时动态地获取和操作任何值和类型的信息;
  • 接口机制 可以实现多态性和抽象性,让不同的类型可以实现相同的行为;
  • kind 基础类型,在 Go 语言中,基础类型是一个枚举常量,有 26 个基础类型,枚举值通过 kindMask 取出特殊标记位
 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
// runtime/typekind.go
const (
    kindBool = 1 + iota
    kindInt
    kindInt8
    kindInt16
    kindInt32
    kindInt64
    kindUint
    kindUint8
    kindUint16
    kindUint32
    kindUint64
    kindUintptr
    kindFloat32
    kindFloat64
    kindComplex64
    kindComplex128
    kindArray
    kindChan
    kindFunc
    kindInterface
    kindMap
    kindPtr
    kindSlice
    kindString
    kindStruct
    kindUnsafePointer
    kindDirectIface = 1 << 5
    kindGCProg      = 1 << 6
    kindMask        = (1 << 5) - 1
)
  • str 和 ptrToThis,对应的类型是 nameoff 和 typeOff。分别表示 name 和 type 针对最终输出文件所在段内的偏移量。在编译的链接步骤中,链接器将各个 .o 文件中的段合并到输出文件,会进行段合并,有的放入.text段,有的放入 .data 段,有的放入 .bss 段。nameoff和typeoff就是记录了对应段的偏移量。

对于int、string、bool等单一的基础结构,元信息存储于_type结构体内已经够用了,但对于 array、 chan、 slice、 func等复合型的结构体,它们除了基础的元信息,还需要存储一些额外的元数据,比如键和值类型、参数和返回值数量、结构体字段等,为了存储这些信息,Golang设定了很多内置类型来处理不同类型需要存储不同信息的需求。

对于内置复合类型,大部分也都在runtime/type.go文件里面,这些内置类型都是在 _type 基础上进一步封装而来,举例说明:

 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
//go 1.20.3 path: /src/runtime/type.go

//用于表示数组类型
type arraytype struct {
    typ   _type     // 类型描述符
    elem  *_type    // 数组元素类型的指针
    slice *_type    // 切片类型的指针
    len   uintptr   // 数组长度
}

//用于表示通道类型
type chantype struct {
    typ  _type    // 类型描述符
    elem *_type   // 通道元素类型的指针
    dir  uintptr  // 通道的方向
}

//表示切片类型
type slicetype struct {
    typ  _type    // 类型描述符
    elem *_type   // 切片元素类型的指针
}

//用于表示函数类型
type functype struct {
    typ      _type    // 类型描述符
    inCount  uint16   // 输入参数数量
    outCount uint16   // 输出参数数量
}

//用于表示指针类型
type ptrtype struct {
    typ  _type    // 类型描述符
    elem *_type   // 指针指向的类型
}

//用于表示结构体类型
type structtype struct {
    typ     _type
    pkgPath name          // 结构体所属包的路径
    fields  []structfield //结构体的字段列表
}

如果是自定义类型,后面还会有一个uncommontype结构体,uncommontype是指向一个函数指针的数组,收集了这类型的实现的所有方法:

1
2
3
4
5
6
7
8
9
// runtime/type.go
//自定义类型元数据
type uncommontype struct {
    pkgpath nameOff
    mcount  uint16 // number of methods
    xcount  uint16 // number of exported methods
    moff    uint32 // offset from this uncommontype to [mcount]method
    _       uint32 // unused
}
  • pkgpath 记录类型所在的包路径
  • mcount 记录该类型关联到多少个方法
  • xcount 记录该类型的导出型多少个方法
  • moff 记录的是这些方法元数据组成的数组,相对于这个uncommontype 结构体偏移了多少字节

例如,基于[]string定义一个新类型myslice,它就是一个自定义类型,可以给它定义两个方法Len和Cap。

myslice的类型元数据中,首先是slicetype类型描述信息,然后在后面加上uncommontype结构体。注意通过uncommontype这里记录的信息,就可以找到myslice的方法元数据列表了。如下图所示:

Tips: 数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。

三、Golang变量

3.1 Golang变量的概念

变量 是表示内存中的一个存储区域,该区域有自己的 名称(变量名)类型(数据类型),通过变量名可以访问(获取或修改)变量(值)。

Golang变量的作用域:

  • 局部变量:是在函数(方法)内声明的变量,作用域在函数体内或函数体内的代码块(选择结构代码块、循环结构代码块等等)内,包括函数(方法)形式参数、返回参数、函数(方法)内部定义的变量,只可在函数内 或 函数内的代码块内使用;
  • 包级变量: 在函数(方法)外声明的变量,变量名首字母必须小写,也就是 Golang 语言中的不可导出变量,包级变量的作用域是package,只能在同一个 package 中使用;
  • 全局变量: 如果包级变量名首字母大写,也就是 Golang 语言中的可导出变量,那么这个变量就成为全局变量,可以在全局使用;

Tips: 可导出 与 不可导出

  • 在Golang中,字段(或者方法)的导出与否是由其首字母的大小写决定的;
  • 如果一个字段首字母是大写的,它就是可以导出的(即在其它包中可以访问到);
  • 如果是小写的,则是不可导出的(即在其它包中不可以访问到);
  • 当试图在包外部使用不可导出符号(变量、常量、函数、方法等)时,编译器就会报错。

3.2 Golang变量声明方式

在 Golang 中,变量声明可以使用以下几种方式:

 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
// 全局变量(首字母大写)的声明方式与包级变量相同,只能使用 var 关键字的变量声明方式
// 如果变量声明显式初始化,一般会使用省略类型声明变量的方式。

// 包级变量声明
var a int           // 变量声明, 默认初始化为类型的零(0)值
var b int = 100     // 显式赋初始值声明变量
var c = 100         // 省略变量类型,让编译器自动推断变量类型
var d = int8(100)   // 省略变量类型,让编译器自动推断变量类型, 显式类型转换

// 变量列表声明:可以使用逗号分隔符在同一行中声明多个变量
var a, b, c int
var d, e, f int = 100, 200, 300
var g, h, i = 400, "你好", true // 声明多个变量,省略变量类型,让编译器自动推断变量类型

// 变量声明块, 使用关键字 var 将多个单个变量声明或变量列表声明放在一起
var (
    a int = 100
    b string = "Hello World"
)

// 声明多个变量:可以使用逗号分隔符在同一行中声明多个变量
var (
    e, f, g int = 10, 20, 30
    h, i, j string = "a", "b", "c"
)

// 声明多个变量,省略变量类型,让编译器自动推断变量类型
var (
    c = 300
    d = 3.14
    f = true
    e = "Hello World"
    g, h, i = 400, "你好", true
)

// 局部变量,一般会优先使用短变量声明的方式,除非不想给局部变量显式初始化,
// 可以使用省略类型声明变量的方式。
// 短变量声明的方式虽然是 Golang 语言中最简化的变量声明的方式,但是它也有局限性,
// 它只能用于局部变量声明。
a := 100
b := int8(60)
j, k, l := 500, "世界", 3.14

四、Golang常量

4.1 Golang常量的概念

相对于变量,常量 是恒定不变的值,多用于定义程序运行期间不会改变的那些值。

Tips:

  • 常量的声明和变量声明非常类似,只是把 var 换成了const,常量在定义的时候必须赋值;
  • 常量中的数据类型只可以是 布尔型数字型(整数型、浮点型和复数)字符串型
  • 常量可以用 len()cap()unsafe.Sizeof() 等内置函数去计算值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main
 
import "unsafe"
const (
    a = "abc"
    b = len(a)             // len(a) 计算变量a的长度,abc 长度为3
    c = unsafe.Sizeof(a)    // unsafe.Sizeof返回数据类型的大小
)
 
func main(){
    println(a, b, c)
}
// abc 3 16

4.2 Golang常量的一般声明方式

在 Golang 中,常量声明可以使用以下几种方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 单个常量声明
const Pi = 3.1415
const e = 2.7182

// 常量声明块
const (
    PI = 3.1415
    e = 2.7182
)

// 常量声明块(枚举)
const (
    Unknown = 0
    Female = 1
    Male = 2
)

// 常量声明块,如果省略了值, 则表示和上面一行的值相同。
const (
    n1 = 100
    n2          // 100
    n3          // 100
)

4.3 使用 iota 常量声明块(枚举)

iota 是go语言的常量计数器,只能在常量的表达式中使用;

Tips:iota 在 const 关键字出现时将被重置为0,在 const 声明块中 每新增一行常量声明将使 iota 计数一次(iota可理解为const语句块中的行索引)

iota 特殊常量,可以认为是一个可以被编译器修改的常量。

使用iota能简化定义,在定义枚举时很有用。

常见的iota示例:

 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// 直接从 0 枚举
const ( // iota 在 const关键字出现时将被重置为0
    a = iota  // 0
    b = iota  // 1
    c = iota  // 2
)

const ( // iota 在 const关键字出现时将被重置为0
    n1 = iota // 0
    n2        // 1
    n3        // 2
    n4        // 3
)

const ( // iota 在 const关键字出现时将被重置为0
    a = 10      // 10, iota 不会自动初始化括号作用域内 iota 前边的常量, 必须指定第一个常量值
    b           // 10
    c = iota    // 2
    d           // 3
    e           // 4
)

// 使用占位符 _ 跳过某些值
const ( // iota 在 const关键字出现时将被重置为0
    n1 = iota // 0
    n2        // 1
    _         // 跳过 2
    n4        // 3
)

// iota声明中间插队
const ( // iota 在 const关键字出现时将被重置为0
    a = iota   // 0
    b          // 1
    c          // 2
    d = "ha"   // 独立值, iota += 1
    e          // "ha", iota += 1
    f = 100    // 100,  iota +=1
    g          // 100,  iota +=1
    h = iota   // 7, 恢复计数
    i          // 8
)

const n = iota // 0, 遇到新的const,(重新)从0开始

// 定义常量初始值从 10 开始(iota 默认初始值为 0)
const ( // iota 在 const关键字出现时将被重置为0
    a = 10 + iota   // 10
    b               // 11
    c               // 12
    d               // 13
    e               // 14
)

// 多个 iota 表达式
const (
    a = iota + 10     // 10, iota初始化为 0 + 10 = 10
    a1                // 11
    b = iota          // 2 iota顺延为1
    c = iota + 5      // 7, iota初始化为 3 + 5 = 8
    d                 // 8
    e                 // 9
)

// 移位操作
// 注:x << n == x * (2^n)
// 这里的 << 表示左移操作,1 << 10 表示将1的二进制表示向左移10位,由1变成了10000000000,也就是十进制的 2^10 = 1024
// 同理 2 << 2 表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的 2 * (2^2) = 8
const ( // iota 在 const关键字出现时将被重置为0
    i = 1 << iota   // 1 << 0, 1 * 2^0 = 1
    j = 3 << iota   // 3 << 1, 3 * 2^1 = 6
    k               // 3 << 2, 3 * 2^2 = 12
    l               // 3 << 3, 3 * 2^3 = 24
)

// 定义数量级 
const ( // iota 在 const关键字出现时将被重置为0
    _  = iota               // 0
    KB = 1 << (10 * iota)   // 1  << 10 * 1, 2^10 = 1024 = 1K
    MB = 1 << (10 * iota)   // 1  << 10 * 2, 2^20 = 1M
    GB = 1 << (10 * iota)   // 1  << 10 * 3, 2^30 = 1G
    TB = 1 << (10 * iota)   // 1  << 10 * 4, 2^40 = 1T
    PB = 1 << (10 * iota)   // 1  << 10 * 5, 2^50 = 1P
)

// 多个iota定义在一行
const ( // iota 在 const关键字出现时将被重置为0
    a, b = iota + 1, iota + 2 // 1, 2
    c, d                      // 2, 3
    e, f                      // 3, 4
)

五、Golang函数

5.1 Golang函数简介

函数(Function) 是一段封装了特定功能的可重用代码块,用于执行特定的任务或计算。函数接受输入(参数)(也可以不接收)并产生输出(返回值)(也可以不返回任何数据),可以带有副作用(修改状态或执行其它操作)。

参考博文:https://blog.csdn.net/qqxjx/article/details/131130538

Tips: 只要通过调用函数的名称,就能执行一段定义好的代码程序;

Go语言中,函数被认为是一等公民(First-class citizens),这意味着函数在语言中具有与其他类型(如整数、字符串等)相同的权利和地位。以下是函数在Go语言中被视为一等公民的原因:

  • 函数可以作为值进行传递:在Go语言中,函数可以像其它类型的值一样被传递给其它函数或赋值给变量。这意味着可以将函数作为参数传递给其它函数,也可以将函数作为返回值返回(Golang的函数只能返回匿名函数)。
  • 函数可以赋值给变量:在Go语言中,可以将函数赋值给变量,然后通过变量来调用函数。这种能力使得函数可以像其它数据类型一样被操作和处理。
  • 函数可以匿名定义:Go语言支持匿名函数的定义,也称为闭包。这意味着可以在不给函数命名的情况下直接定义和使用函数,更加灵活和便捷。
  • 函数可以作为数据结构的成员:在Go语言中,函数可以作为结构体的成员,从而使得函数与其它数据一起存储在结构体中。这种特性使得函数能够更好地与数据相关联,实现更复杂的功能。

函数作为一等公民的特性使得Go语言具有很高的灵活性和表达力,可以方便地实现函数式编程的思想,并且支持构建高阶函数和函数组合等高级编程技巧。

函数定义格式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func 函数名 (形参列表) 返回值参数列表{
     代码块
     return 返回值列表
}
 
// 函数名:标识代码块的起始地址(可省略),通过调用函数的名称,就能执行一段定义好的代码程序
// 形参列表:表示函数的输入参数,函数可以有参数(一个或多个),也可以没有
// 返回值参数列表: 表示函数的返回参数,函数可以有(一个或多个)返回参数,也可以没有,
// 代码块:具体实现功能的代码命令
// 返回值列表:与 "返回值参数列表" 一一对应,函数可以有返回值(一个或多个),也可以没有, 多个返回值列表使用 () 包围

函数参数列表及调用函数传参: 函数参数列表由 参数变量(形参) 和对应的 参数变量的类型 组成,函数参数列表可以有一个参数,也可以有多个,也可以没有(省略函数参数列表);多个参数之间使用 , 分隔;多个参数时参数变量要么全写,要么全省略;如果多个相邻参数的类型是一样的,可以只保留同一类型最后一个参数的类型声明。

Go语言中所有的函数调用传参(实参)都是 值传递(传值),都是一个拷贝副本。

拷贝的内容有时候是非引用类型(int、string、struct等这些),这样在函数中就无法修改实参内容数据;有的是引用类型(指针、map、slice、chan等这些),这样可以修改实参(引用指向)内容数据。

Golang中所有函数参数传递都是传值(包括指针),slice、map和chan等看上去像引用只是因为它们内部有指针或本身就是指针而已(slice其实是一个含有指针的结构体,而map和slice本身就是一个指针)。在被调函数中修改 slice、map和chan等,对调用者同样可见,所以 slice、map和chan 作为函数实参传递时表现出了引用传递的效果。

一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能 和 安全性。

 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
// 1. 省略参数类别写法
func functionName() {
    // 函数体
}

// 2. 单个参数
func functionName(parameterName parameterType) {
    // 函数体
}

// 3. 多个参数
func functionName(parameter1Name parameter1Type, parameter2Name parameter2Type) {
    // 如果函数需要接受多个参数,可以在函数声明中依次列出参数的名称和类型。
}

// 4. 可变参数(Variadic parameters)函数定义格式:
func sum(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
    //  可变参数允许函数接受不定数量的参数。在参数类型之前使用...来指示可变参数的形式。在函数体内,可变参数被当作切片类型来处理。
}

// 5. 参数命名和类型省略
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
    // 在函数定义中,如果多个参数具有相同的类型,可以省略参数类型,并在最后一个参数上指定类型。这种情况下,所有的参数都将具有相同的类型。
}

// 6. 匿名参数
func functionName(int, string) {
    // 函数体
    // 在函数定义中,如果不需要使用参数的值,可以将参数名称省略,只保留参数类型。这种形式的参数被称为匿名参数。
}

返回值参数列表 返回值参数列表 由返回值变量和其变量类型组成,返回值变量可以省略,可以有一个返回值,也可以有多个,也可以没有;多个返回值必须用()包裹,并用 , 分隔;多个返回值时返回值变量要么全写,要么全省略。

 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
// 1. 单个返回值
func functionName() returnType {
    // 函数体
    return value  // 这是函数返回单个值的基本形式。returnType是返回值的类型,value是要返回的具体值。
}

// 2. 多个返回值
func functionName() (returnType1, returnType2) {
    // 函数体
    return value1, value2 // 如果函数需要返回多个值,可以在函数声明中使用括号将多个返回值类型括起来,并在函数体内使用逗号分隔返回的具体值。
}

// 3. 命名返回值
func functionName() (returnValue1 returnType1, returnValue2 returnType2) {
    // 函数体
    returnValue1 = value1
    returnValue2 = value2
    // 可以为返回值命名,通过在函数声明中为返回值指定名称和类型。
    // 在函数体内,可以直接为这些命名返回值赋值,并在最后使用return关键字返回结果。
    return value1, value2
    // return 也可 指明返回值
}

// 4. 空返回值
func functionName() {
    // 函数体
    return // 如果函数没有返回值,可以省略返回值的类型和具体值,只使用return关键字, 或省略 return关键字
}

函数使用细节说明:

  1. 函数的参数可以没有、可以有一个或多个,返回值也可以没有、可以有一个或多个;

  2. 形参列表和返回值列表的数据类型可以是值类型和引用类型;

  3. 函数命令遵循标识符命名规范: 3.1 首字符不能是数字; 3.2 首字符大写该函数可以被本包文件和其它包文件使用,首字母小写只能被本包文件使用;

  4. 基本数据类型和数组默认都是值传递的,即进行值拷贝,在函数内修改不会影响到原来的值;

  5. 如果希望函数内的变量能修改函数外的变量可以传入变量的地址&,函数内以指针的方式操作变量;

  6. Go函数不支持重载;

  7. 在GO中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量,通过该变量可以对函数调用;

  8. 函数既然是一种数据类型,因此在go中,函数可以作为形参,并且在其它函数内调用;

  9. 为了简化数据类型定义,go支持自定义数据类型

  10. 支持对函数返回值命名

    1
    2
    3
    4
    5
    
    func cal(n1 int,n2 int) (sum int, sub int){
        sum = n1 + n2
        sub = n1 - n2
        return
    }
    

5.2 Golang函数值(Function Value)

函数,在GO语言中属于头等对象,可以被当作参数传递、也可以作为函数返回值、也可绑定到变量。 如果一个函数像其它类型一样被赋值给变量、作为参数传递给其他函数或从函数返回的,则称为 Function Value

函数值( Function Value)是函数作为值的表达形式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func add(a, b int) int {
    return a + b
}

func main() {
    var sumFunc func(int, int) int
    sumFunc = add

    result := sumFunc(2, 3) // 调用函数值
    fmt.Println(result)     // 输出: 5
}

在上面的示例中,函数 add 被赋值给了名为 sumFunc 的函数变量,然后可以通过 sumFunc 来调用 add 函数。

Function Value本质上是一个指针,却不直接指向函数指令入口,而是指向runtime.funcval结构体

1
2
3
4
5
//runtime/runtime2.go
type funcval struct {
    fn uintptr
    // variable-size, fn-specific data here
}

其中的 fn 字段存储的是函数变量的地址,而根据下面的注释能够知道这个结构的大小是不确定的,实际上编译器会把捕获外层函数的 “捕获列表”追加到fn字段之后,至于捕获列表中存储的是值还是地址,需要根据实际情况而定。

5.3 Golang的init函数

init函数 也称作 初始化函数,是一种没有参数、没有返回值、名为init 的特殊的函数,定义方式如下:

1
2
3
4
func init () {
   // do something
   ...
}

init的特征:

  • init 函数没有输入参数、没有返回值,也未声明,所以无法被显示的调用,不能被引用(赋值给函数变量),否则会出现编译错误
  • 一个go文件可以拥有多个init函数,执行顺序按定义顺序执行
  • 初始化常量/变量优于 init 函数执行,init 函数先于 main函数自动执行。执行顺序先后为: const常量 > var 变量 > init函数 > main函数
  • init初始化函数,会在main函数执行前执行,如果 import 了其它的包中包含init函数,那么会按导入包的顺序优先去扫描导入包中的init函数并执行。

以上这张图片很清晰的反应了init函数的加载顺序:包加载优先级排在第一位,先层层递归进行包加载,每个包中加载顺序为:const > var > init,首先进行初始化的是常量,然后是变量,最后才是init函数。

init函数的使用场景:

  • 初始化全局变量或常量。
  • 注册/初始化数据库连接。
  • 执行配置加载和解析。
  • 注册/初始化各种组件或模块。
  • 进行一些必要的预处理操作等。
 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
// 初始化全局变量或常量:
var globalVar int

func init() {
    globalVar = 10
}

// 注册和初始化数据库连接:
func init() {
    db.Connect("localhost:3306", "username", "password")
}

// 执行配置加载和解析:
func init() {
    config.Load("config.json")
}

// 注册和初始化各种组件或模块:
func init() {
    cache.Register()
    logging.Init()
}

// 进行一些必要的预处理操作:
func init() {
    // 检查环境变量或命令行参数
    // 设置日志级别
    // 等等...
}

// 注册自定义的类型、接口实现或者其他初始化任务:
func init() {
    // 注册自定义的类型
    myTypeRegistry.Register(&MyType{})

    // 注册接口实现
    myInterfaceRegistry.Register(&MyInterfaceImpl{})
}

init() 函数在包被导入时自动执行,因此可以用于进行一些初始化操作。它们可以包含任何合法的 Go 代码,并且可以有多个 init() 函数,按照定义的顺序依次执行。

在实际开发中,init() 函数通常用于进行一些初始化设置、资源的注册和初始化,以及其他需要在程序启动时执行的任务。

5.4 Golang匿名函数

在 Go 语言中,匿名函数是一种没有名称的函数。它是一种函数字面量,可以在代码中直接定义和使用,而无需事先命名。

匿名函数通常用于需要在某个特定位置定义、传递或执行函数的场景,而不需要在其它地方重复使用该函数。它们可以作为变量的值,传递给其它函数,可以从函数中返回给调用者 或 直接调用。

匿名函数的语法类似于普通函数,但没有函数名。它可以包含参数列表、函数体和返回值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func (形参列表) 返回值列表 {
    代码块
}
 
// 匿名函数和正常的函数类似,只不过省钱了函数的名称
// 因为没有函数名,无法调用传参,所以在定义处直接在{}后面使用括号()进行参数传递
// 也可以在定义匿名函数时将其赋值给函数变量,然后再通过函数变量来重复调用匿名函数

// 示例1:定义处直接在{}后面使用括号()进行参数传递调用
func (n1 int,n2 int) int{
    return n1 + n2
}(10,20)

// 定义时赋值给函数变量,再通过函数变量重复调用
a := func (n1 int,n2 int) int{  //我们将这个匿名函数定义给了a变量 (没有设置参数)
    return n1 + n2
}

res1 := a(10,20)                // 此时a变量就可以看作是一个独立的函数,
res2 := a(100,200)              // 通过a()来调用这个函数,可以多次调用

全局匿名函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main
 
import "fmt"
 
var (
    // Fun1 是全局变量,将一个匿名函数赋给这个变量,他就可以在整个程序中使用了
    // 如果首字母大小,那么就可以在不同的包中调用该匿名函数
    Fun1 = func (n1 int,n2 int) int {
        return n1 * n2
    }
)
 
func main()  {
    
    res := Fun1(10,20)   // 调用匿名函数
    fmt.Println(res)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main
 
import "fmt"

var f = func(int) {}

func main() {
    f = func(i int) {
        fmt.Println(i)
    }
    f(2)
    f = func(i int) {
        fmt.Println(i * i * i)
    }
    f(2)
}
/*
输出:
2
8
*/

上述代码中,f 可以被任何输入一个整型参数、无返回值的函数赋值,这类似于C++中的函数指针。因此 f 可以看成是一个函数类型的变量。这样,可以动态的改变 f 的功能。

匿名函数可以动态的创建 和 随时改变功能,与之成对比的常规函数必须在包中编译前就定义完毕(定义后不可再改变功能)。

5.5 Golang函数闭包

1、闭包简介

闭包 是 匿名函数 与 匿名函数所引用环境(变量)的组合。匿名函数有动态创建的特性,该特性使得匿名函数不用通过参数传递的方式,就可以直接引用外部的变量,这就类似于 常规函数直接使用全局变量一样匿名函数和它引用的变量以及环境,类似常规函数引用全局变量处于一个包的环境

Tips: 闭包 类似于C++ 类中的静态变量 和 静态成员方法;

闭包可以看成函数的高阶应用,是Golang高级开发的必备技能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main
 
import "fmt"

func main() {
    n := 0
    f := func() int {
        n += 1
        return n
    }
    fmt.Println(f())  // 别忘记括号,不加括号相当于地址
    fmt.Println(f())
}
/*
输出:
1
2
*/

在上述代码中

1
2
3
4
5
n := 0
f := func() int {
    n += 1
    return n
}

就是一个闭包,类比于常规函数+全局变量+包。f 不仅仅是存储了一个函数的返回值,它同时存储了一个闭包的状态。

闭包作为函数返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
 
import "fmt"

func Increase() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

func main() {
    in := Increase()
    fmt.Println(in())
    fmt.Println(in())
}
/*
输出:
1
2
*/

闭包作为函数返回值返回后被赋予一个函数类型的变量时,同时赋值的是整个闭包的状态,该状态会一直存在外部被赋值的变量中,直到变量被销毁,整个闭包也被销毁。

2、Golang并发中的闭包

Go语言的并发时,一定要处理好循环中的闭包引用的外部变量。如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())

    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()
}
输出结果
5
5
5
5
5

这种现象的原因在于闭包共享外部的变量 i,for循环中每次循环调用 go就会启动一个goroutine,这需要一定时间;但是,启动的goroutine与循环变量递增不是在同一个goroutine,i 处于 主goroutine中。启动一个goroutine的速度远小于循环执行的速度,所以即使是第一个goroutine刚起启动时,外层的循环也执行到了最后一步了。由于所有的goroutine共享 i,而且这个 i 会在最后一个使用它的goroutine结束后被销毁,所以最后的输出结果都是最后一步的 i==5

可以使用循环的延时在验证上述说法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())

    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
        time.Sleep(1 * time.Second)   // 设置时间延时1秒
    }
    wg.Wait()
}
/*
输出结果:
0
1
2
3
4
*/

每一步循环至少间隔一秒,而这一秒的时间足够启动一个goroutine了,因此这样可以输出正确的结果。

在实际的工程中,不可能进行延时,这样就没有并发的优势,一般采取下面两种方法:

1、共享的环境变量作为函数参数传递

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())

    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}
/*
输出:
4
0
3
1
2
*/

输出结果不一定按照顺序,这取决于每个goroutine的实际情况,但是最后的结果是不变的。 可以理解为,函数参数的传递是瞬时的,而且是在一个goroutine执行之前就完成,所以此时执行的闭包存储了当前 i 的状态。

2、使用同名的变量保留当前的状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())

    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        i := i       // 注意这里的同名变量覆盖
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()
}
/*
输出结果:
4
2
0
3
1
结果顺序原因同1
*/

同名的变量 i 作为内部的局部变量,覆盖了原来循环中的 i,此时闭包中的变量不在是共享外循环的 i,而是都有各自的内部同名变量 i,赋值过程发生于循环goroutine,因此保证了独立。

5.6 Golang函数defer的

1、defer机制简介

在Golang中,提供了在函数体执行完毕后执行额外指令(函数)的机制,这一机制称为 延时机制defer

defer 还有错误捕获、修改函数返回值、资源释放等功能。

在Golang 函数/方法 中使用 defer 关键字调用的每个 defer函数语句,Go运行时都会为其创建一个 defer结构体,并将创建的defer结构体添加到当前函数的 defer栈 中缓存。当函数返回时,Go运行时会依次执行 defer栈中的 defer函数,直到链表为空为止,这个过程是在被调函数返回之前执行的,因此可以保证被延迟执行的函数在函数返回之前被执行。

即使函数执行发生 panic,在panic中断函数或进程前,也要先执行完已经压入 defer栈 中的 defer语句,panic 之后的defer语句由于还没进入 defer栈,所以不会被执行。

在函数/方法中, defer语句 可有可无、可有一条或多条。

defer延时指令的特点:

  • 当go执行到一个 defer 语句时,会将 defer 后的语句先压入到一个栈中,然后继续执行函数下一个语句;
  • 待函数其它语句执行完毕后,在从defer栈中弹出延迟语句依次执行(注: 遵守 栈 后进先出 的特征来出栈);
  • 在defer将语句放入到栈中的同时,会将相关的值拷贝入栈(也就是说:defer 调用的函数参数的值 在 defer被定义时就确定了,之后的修改不影响defer调用);
  • 在defer中操作调用(defer)函数的局部变量、返回值变量 或 全部变量(非defer调用参数传递),变量的值为函数return 时的值,且在 defer 中的操作(修改)将会反应到 局部变量(函数调用结束后自动回收,所以一般无语)、返回值变量(影响返回值)或 全部变量上。

2、return语句 先于 defer 语句执行

 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
package main

import "fmt"

func deferFunc() int {
    fmt.Println("defer func done")
    return 0
}

func returnFunc() int {
    fmt.Println("return func done")
    return 0
}

func returnAndDefer() int {
    defer deferFunc()
    return returnFunc()
}

func main() {
    returnAndDefer()
}

$ go run main.go
return func done
defer func done

3、defer 语句按照后进先出的顺序执行执行

 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
56
57
58
59
60
package main
import "fmt"

var n0 = 0
 
func defer_call(nx int, ny *int) int{
    n3 := 3
    defer fmt.Println("defer n0=",n0)  // n0=0,执行到defer时,会依次将defer后面的语句,压入到栈内存中(defer栈),暂时不执行
    defer fmt.Println("defer nx=",nx)  // n1=1 待函数执行完毕后,再从defer栈,按照后进先出的方式,出栈执行
    defer fmt.Println("defer ny=",*ny) // *n2=2 在defer将语句放入到栈中的同时,会将相关的值拷贝入栈
    defer fmt.Println("defer n3=",n3)  // n3=3
    
    // 传递指针时,在defer将语句放入到栈中的, 同样会将相关的指针(内存地址)值拷贝入栈,
    // 此时传递的是内存地址,执行defer时 打印的是内存地址中的值
    defer func (pn *int) {fmt.Println("defer n2=",*pn)}(ny) // ny指针指向 n ——> pn指针指向 n,
    
    // defer 定义之后的修改不影响 defer调用
    n0 = 10
    nx = 11  // n1 是值传递
    *ny = 12 // ny指针指向 n
    ny = &n0 // ny指针指向 n0
    n3 = 13
    fmt.Println("defer_call n0=",n0)
    fmt.Println("defer_call nx=",nx)
    fmt.Println("defer_call ny=",*ny)
    fmt.Println("defer_call n3=",n3)
    
    return n3
}
 
func main(){
    n := 2
    n1 := 1 
    n2 := &n

    res := defer_call(n1,n2) // n1 是值传递, n2是指针传递(传递的是 n 的地址)
    fmt.Println("main n0=",n0)
    fmt.Println("main n1=",n1)
    fmt.Println("main n2=",*n2)
    fmt.Println("main n3=",res)
}
// 执行输出结果:
/*
defer_call n0= 10
defer_call nx= 11
defer_call ny= 10 // ny指针指向 n0
defer_call n3= 13

// defer 语句 后进先出
defer pn= 12  // 传递指针,执行defer时 打印的是内存地址(n2)中的值(函数return时的值为 12)
defer n3= 3
defer ny= 2  ny指针指向 n
defer nx= 1
defer n0= 0

main n0= 10
main n1= 1  // n1 是值传递
main n2= 12
main n3= 13
*/

4、panic发生之后的 defer语句不会执行

当函数执行时发生 panic 时候,会首先按照 后进先出 的顺序执行 已经入栈的defer语句(也就是panic发生之前的defer语句),最后才会执行panic,panic发生之后的defer语句由于发生了panic函数终止执行后续语句,故不会入栈、也不会执行(不进不出不执行)。

 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
package main
 
import (
    "fmt"
 )
 
 func main() {
     defer_call()
 }
 
func defer_call() {
    // panic发生之前的defer语句的执行顺序是 后进先出先执行
    defer func() { fmt.Println("打印前") }()
    defer func() { fmt.Println("打印中") }()
    defer func() { fmt.Println("打印后") }()
    panic("触发异常")       //抛出异常,停止程序
    defer func() { fmt.Println("panic触发异常后") }() // panic发生之后的defer语句 不进不出不执行
}
// 执行输出结果:
/* 
打印后
打印中
打印前
panic: 触发异常
*/

Tips: defer 语句的执行顺序是 后进先出先执行不进不出不执行

5、defer延时语句通常用于释放函数内创建的资源 或 配合 recover() 用于函数执行过程中的异常处理

defer延时语句 一般用于释放函数内创建的资源(比如: 网络连接、文件句柄、锁等资源)或 配合 recover() 用于函数执行过程中的异常(panic)处理等工作。

 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
package main
 
import (
    "errors"
    "fmt"
    "os"
)

func defer_call(){
    fmt.Println("Enter function defer_call.")
    f, err := os.Create("./test.txt")
    if err != nil {
        panic(err) // defer - recover() 定义之前的panic不会被捕获
    }
    defer f.Close()  // 在函数执行完、返回前释放函数内创建的资源

    _ ,err = f.Write([]byte("Just for test."))
    if(err != nil){
        fmt.Println("writeFile error ..err =",err)
        return
    }

    defer func(){  // 配合 recover() 用于函数执行过程中的 panic异常 捕获和处理
        fmt.Println("Enter defer function.")
        if p := recover(); p != nil {
            fmt.Printf("panic: %s\n", p)
        }
        fmt.Println("Exit defer function.")
    }()
    
    // 手动引发 panic, 在 defer - recover() 定义之发生的 panic 将被捕获
    panic(errors.New("something wrong"))

    fmt.Println("Exit function defer_call.") // 发生 panic 之后的语句不会执行
}

func main() {
    fmt.Println("Enter function main.")
    defer_call()
    fmt.Println("Exit function main.")   
}
// 执行输出结果:
/*
Enter function main.
Enter function defer_call.
Enter defer function.
panic: something wrong
Exit defer function.
Exit function main.
*/

Tips:

  • defer 配合 recover() 用于 捕获和处理 函数执行过程中的 panic异常,阻止进程因 panic异常退出导致程序 或 在退出进程之前做一下记录(log)、清理等类型的工作;
  • defer - recover() 只能捕获该定义后发生的panic,定义之前的任何 panic不会被捕获,因为defer语句 还未进入 defer栈 中;

6、defer 中发生 panic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "fmt"
)


func main()  {
    defer func() {
       if err := recover(); err != nil{
     fmt.Println("err:", err)
       }else {
           fmt.Println("fatal")
       }
    }()

    defer func() {
        panic("defer panic2")
    }()

    panic("panic1")
}
$ go run main.go
err: defer panic2

在上面例子中,panic(“panic1”)先 触发 defer 强制出栈,第一个执行触发 panic(“defer panic2)“异常,此时会覆盖前一个异常 panic,最后继续执行 defer, 最终被 recover()捕获住。

7、defer对函数返回值的操作及影响

defer 可以影响函数的具名返回值。

在defer中操作 调用(defer的)函数的 局部变量具名返回值变量全部变量(非defer调用参数传递),函数使用这些类型变量为 return 的返回值时,在 defer 中的操作(修改)将会反应到 局部变量(函数调用结束后自动回收,所以一般无语)、具名返回值变量(影响返回值)或 全部变量上。

1)返回参数指定返回值变量(具名返回值),return 后不声明返回值 或 声明返回值为具名返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"

func f1() (r int) {
    r = 1
    defer func() {
        r++ // 执行到此时 r=2, ++ 操作后 r=3,函数返回值 r=3
        fmt.Println("f1 r value = ",r)
    }()
    r = 2
    return // 与 return r 相同
}

func main() {
    fmt.Println("main return value = ", f1())
}
// 执行输出结果:
/*
f1 r value =  3
main return value = 3
*/

2)返回参数指定返回值变量,return 后声明返回值并且声明的返回值不是返回参数变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"

func f1() (r int) {
    r = 1
    defer func() {
        r++ // 执行到此时 r=0, ++ 操作后 r=1
        fmt.Println("f1 r value = ",r)
    }()
    r = 2
    return 0 // 此处隐式 的将 r 设置为 0, 再执行defer 语句
}

func main() {
    fmt.Println("main return value = ", f1())
}
// 执行输出结果:
/*
f1 r value = 1
main return value = 1
*/

返回参数不指定返回值变量,return 后声明返回值并且声明的返回值为常量

 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
package main
import "fmt"

func f1() (int) {
    r := 1
    defer func() {
        r++ // 执行到此时 r=3, ++ 操作后 r=4
        fmt.Println("defer1 r value = ",r)
    }()
    defer func() {
        r++ // 执行到此时 r=2, ++ 操作后 r=3
        fmt.Println("defer2 r value = ",r)
    }()
    r = 2
    return 10 // 返回值已经确定,不再改变
}

func main() {
    fmt.Println("main return value = ", f1())
}
// 执行输出结果:
/*
defer2 r value =  3
defer1 r value =  4
main return value =  10
*/

返回参数不指定返回值变量,return 后声明返回值并且声明的返回值为函数内局部变量

 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
package main
import "fmt"

func f1() (int) {
    r := 1
    defer func() {
        r++ // 执行到此时 r=3, ++ 操作后 r=4
        fmt.Println("defer1 r value = ",r)
    }()
    defer func() {
        r++ // 执行到此时 r=2, ++ 操作后 r=3
        fmt.Println("defer2 r value = ",r)
    }()
    r = 2
    return r // 返回值已经确定,不再改变
}

func main() {
    fmt.Println("main return value = ", f1())
}
// 执行输出结果:
/*
defer2 r value =  3
defer1 r value =  4
main return value = 2
*/

Tips: defer 函数调用的执行时机是外层函数设置返回值之后, 并且在即将返回之前

8、defer 函数嵌套子函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func f(x int, y int) int {
    fmt.Println("x:", x)
    fmt.Println("y:", y)
    return x
}

func main() {
    fmt.Println("main running...")
    defer f(1, f(3, 0))
    defer f(2, f(4, 0))
    fmt.Println("main over...")
}

先分析下执行顺序,有 2 个 defer,则会产生 2 次入栈操作,分别是 f1 、f2。

  • f1 入栈时,因为形参 y 是一个函数,则需要执行该函数,故执行一次输出 (x:3 y:0)
  • f2 入栈时,因为形参 y 是一个函数,则需要执行该函数,故执行一次输出(x:4 y:0) main 函数执行完后,执行 defer 函数,所有 defer 出栈,所有执行顺序为 f2、f1。所以程序最终输出的结果是:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
main running...
x: 3
y: 0
x: 4
y: 0
main over...
x: 2
y: 4
x: 1
y: 3

9、使用defer实现方法插桩调用

以下代码可用于 计算一个函数的执行时间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import (
  "fmt"
  "time"
)

func main() {
  defer timeSpent()()
  fmt.Println("execute main()")
  time.Sleep(4 * time.Second)
}

func timeSpent() func() {
  start := time.Now()
  fmt.Println("Start to execute")
  return func() {
    fmt.Printf("Execute finished, spent:%v \n", time.Since(start))

  }
}

六、Go 命令简介

6.1 go 命令列表

以下是一些常用的Go命令,这些命令可以在Go开发中进行编译、测试、运行和管理依赖项等任务。

命令 描述
go bug 启动一个用于报告bug的工具
go build 编译Go程序包及其依赖项
go clean 删除编译生成的对象文件和缓存文件
go doc 显示有关包或符号的文档
go env 打印有关Go环境的信息
go fix 更新包以使用新的API
go fmt 使用gofmt重新格式化Go包的源代码
go generate 通过处理源代码来生成Go文件
go get 下载并安装包和依赖(下载包和依赖,并对它们进行编译安装)
go install 编译并安装指定包及它们的依赖包
go list 列出包或模块的信息
go mod 用于模块维护,包括初始化模块、添加和更新依赖项等
go work 用于工作区维护,例如查看、清理或打印工作区信息
go run 编译并运行Go程序
go test 运行Go程序的测试
go tool 运行指定的Go工具
go version 打印Go的版本信息
go vet 对Go代码进行静态分析,检查可能存在的错误

6.2 go build 编译命令

Golang 是强类型编译型语言,因此编译时会将所有依赖编译进同一个二进制文件。

go build 用于编译导入的包,包括import导入的包,以及这些包所依赖的其他关系包,但是并不会将编译后的结果进行安装。

Golang 的编译是以 package mainmain() 函数作为程序主入口,生成可执行文件。

go build 所在包为main 包时:

  • 若 go build 后跟多个 .go文件,编译时会认为这几个文件是在同一个包中,且已第一个 .go 文件名为输出结果(比如:go build ed.go rx.go 输出为 ed 或 ed.exe);
  • 如果 go build 后跟一个目录比如:go build /unix/sam 编译结果输出为:sam 或 sam.exe

go build 多个包或非main 包时,会丢弃编译结果,只检查是否可以执行编译。

go build 编译包时,会忽略 _test.go 结尾的文件(即测试文件)

如果 go build 后不跟任何参数则会编译当前目录所对应的代码包,生成目录名 或 目录名.exe 可执行文件。

go build -i:默认不写 -i 情况下,编译后的 .a 文件是不会保存也不会安装到 pkg 目录下的 windows_amd64 目录中,但是只要加上 -i 编译后的 .a 文件就会自动保存到 pkg 目录中,就是说:标记 -i 会使 go build 命令安装那些编译目标依赖的且还未被安装的代码包。

Golang 编译命令格式:

1
go build [-o output] [build flags] [packages]

go build 编译命令参数介绍:

参数 含义
-o output 指定编译输出的名称,代替包名
-i install 安装作为目标的依赖关系的包(用于增量编译提速)
-a 强行对项目所有的代码包(包含标准库中的代码包)进行重新构建,即使它们已经是最新的了
-n 打印编译期间所用到的命令,仅仅是打印并不真正执行它们
-p n 指定编译过程中执行各任务的并行数量(确切地说应该是并发数量)。在默认情况下,该数量等于CPU的逻辑核数。但是在darwin/arm平台(即iPhone和iPad所用的平台)下,该数量默认是1
-race 开启竞态条件的检测。不过此标记目前仅在linux/amd64、freebsd/amd64、darwin/amd64和windows/amd64平台下受到支持
-msan 使用内存清除器启用互操作。只支持Linux/AMD 64、Linux/ARM 64,并且只有clang/llvm作为主机c+编译器
-v 打印出那些被编译时的代码包的名字
-x 打印编译期间所用到的其它命令(且执行),注意它与-n标记的区别
-work 打印出编译时生成的临时工作目录的路径,并在编译结束时保留它。在默认情况下,编译结束时会删除该目录以下为不常用命令
-asmflags 此标记可以后跟另外一些标记,如-D、-I、-S等。这些后跟的标记用于控制Go语言编译器编译汇编语言文件时的行为
-buildmode 此标记用于指定编译模式,使用方式如-buildmode=default(这等同于默认情况下的设置)。此标记支持的编译模式目前有6种。借此,我们可以控制编译器在编译完成后生成静态链接库(即.a文件,也就是我们之前说的归档文件)、动态链接库(即.so文件)或/和可执行文件(在Windows下是.exe文件) go help buildmode
-compiler 此标记用于指定当前使用的编译器的名称。其值可以为gc或gccgo。其中,gc编译器即为Go语言自带的编辑器,而gccgo编译器则为GCC提供的Go语言编译器
-gccgoflags 此标记用于指定需要传递给gccgo编译器或链接器的标记的列表
-gcflags 此标记用于指定需要传递给go tool compile命令的标记的列表
-installsuffix
为了使当前的输出目录与默认的编译输出目录分离,可以使用这个标记。此标记的值会作为结果文件的父目录名称的后缀。其实,如果使用了-race标记,这个标记会被自动追加且其值会为race。如果我们同时使用了-race标记和-installsuffix,那么在-installsuffix标记的值的后面会再被追加_race,并以此来作为实际使用的后缀
-ldflags 此标记用于指定需要传递给go tool link命令的标记的列表
-linkshared 此标记用于与-buildmode=shared一同使用。后者会使作为编译目标的非main代码包都被合并到一个动态链接库文件中,而前者则会在此之上进行链接操作
-pkgdir 指定一个目录,并从改目录下加载编译好的.a 文件,并把编译可能产生新的 .a 文件放入到该目录中
-tags 此标记用于指定在实际编译期间需要受理的编译标签(也可被称为编译约束)的列表
-toolexec 此标记可以让我们去自定义在编译期间使用一些Go语言自带工具(如vet、asm等)的方式

-ldflags 'flag list' 可在 go install、go build、go run、go test中使用:

  • -s: 去掉符号表
  • -w: 去掉调试信息,不能gdb调试了
  • -X: 设置包中的变量值

-w-s 通常一起使用:go build -ldflags "-s -w" -o xxx,用来减少可执行文件的体积。但删除了调试信息后,可执行文件将无法使用 gdb/dlv 调试。

例:编译的时候,带上编译的人的姓名:

1
2
3
4
5
6
7
8
9
package main
import "fmt"

var Developer string
func main() {
    fmt.Println("Developer:", Developer)
}

go build -ldflags "-X main.Developer=zhangsan"

执行编译好的程序,可以看到 Developer: zhangsan 输出。

编译是动态添加版本相关信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Module=github.com/pubgo/xxx
GOPATH=$(shell go env GOPATH)
Version=$(shell git tag --sort=committerdate | tail -n 1)
GoVersion=$(shell go version)
BuildTime=$(shell date "+%F %T")
CommitID=$(shell git rev-parse HEAD)
LDFLAGS:=-ldflags "-X 'github.com/pubgo/xxx/version.GoVersion=${GoVersion}' \
-X 'github.com/pubgo/xxx/version.BuildTime=${BuildTime}' \
-X 'github.com/pubgo/xxx/version.GoPath=${GOPATH}' \
-X 'github.com/pubgo/xxx/version.CommitID=${CommitID}' \
-X 'github.com/pubgo/xxx/version.Module=${Module}' \
-X 'github.com/pubgo/xxx/version.Version=${Version:-v0.0.1}'"

go build ${LDFLAGS} -mod vendor -race -v -o main main.go

-gcflags:

  • -N:参数代表禁止优化;
  • -l:参数代表禁止内联;
  • -c int:编译过程中的并发数,默认是1;

go在编译目标程序的时候会嵌入运行时(runtime)的二进制,禁止优化和内联可以让运行时(runtime)中的函数变得更容易调试。

1
2
3
4
5
6
7
go build -gcflags='all=-N -l' main.go

go tool compile -N -l -S main.go
go tool compile -N -l main.go
go tool objdump main.o.      //反汇编出代码
go tool objdump -s Do main.o //反汇编特定的函
go build -gcflags -S main.go

go tool compilego build -gcflags -S 生成的是过程中的汇编,最终的机器码的汇编可以通过 go tool objdump 生成。

-gcflags 参数不光优化编译的二进制包,还能进行项目的逃逸分析

1
go build -gcflags="-m -l" ./main.go
  • 这一命令中,-m 表示打印出逃逸分析信息,-l 表示禁止内联,可以更好地观察逃逸。
  • 从以下输出结果可以看到,发生了逃逸,也就是说指针作为函数返回值的时候,一定会发生逃逸。
1
2
3
4
5
6
7
8
// 变量 m 没有逃逸,反而被变量 m 引用的变量 s 逃逸到了堆上。
// 所以被map、slice 和 chan 这三种类型引用的指针一定会发生逃逸的。

func main() {
    m := map[int]*string{}
    s := "小胡"
    m[0] = &s
}

用于项目编译过程展示:项目版本号、日期、git版本、go版本;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
var (
    version    = "" // "1.0.0"
    buildstamp = "" // time.Now()
    githash    = "" // "1.0.0"
    goversion  = "" // fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)
    
    // VERSION    string
    // BUILD_TIME string
    // GO_VERSION string
)

func main() {
    // fmt.Printf("%s\n%s\n%s\n", VERSION, BUILD_TIME, GO_VERSION)
    args := os.Args
    if len(args) == 2 && (args[1] == "--version" || args[1] == "-v") {
        fmt.Printf("Version : %s\n", version)
        fmt.Printf("Git Commit Hash: %s\n", githash)
        fmt.Printf("CST Build Time : %s\n", buildstamp)
        fmt.Printf("Golang Version : %s\n", goversion)
        return
    }
}

build.sh

 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
#!/bin/bash

echo "(1)Windows下 编译 Linux流程:"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64  go  build -ldflags="-s -w" main.go

# 文件 file main的属性
# main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

echo "(2)Linux 下编译 Mac 和 Windows 平台64位可执行程序:"
#CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
#CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

echo "(3)Mac 下编译 Linux 和 Windows平台 64位 可执行程序:"
#CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
#CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

echo "动态传值1-完整版(注入-版本号、时间date、版本git、go版本): -u 是展示UTC时间"
#flags="-X main.version=1.0.0 -X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git describe --long --dirty --abbrev=14` -X 'main.goversion=$(go version)'"
flags="-X main.version=1.0.0 -X main.buildstamp=`date '+%Y-%m-%d_%H:%M:%S'` -X main.githash=`git describe --long --dirty --abbrev=14` -X 'main.goversion=$(go version)'"
CGO_ENABLED=1 GOOS=linux GOARCH=amd64  go build -ldflags "$flags" -x -o build-version main.go

# 文件 file bulid-version的属性
# build-version: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

echo "手动传值2 (注入:版本号、时间、go版本) :"
# data = UTC
#CGO_ENABLED=1 GOOS=linux GOARCH=amd64  go build -ldflags "-X main.VERSION=1.0.0 -X 'main.BUILD_TIME=`date -u '+%Y-%m-%d_%I:%M:%S%p'`' -X 'main.GO_VERSION=$(go version)'"

# data = CST
CGO_ENABLED=0 GOOS=linux GOARCH=amd64  go build -ldflags "-X main.VERSION=1.0.0 -X 'main.BUILD_TIME=`date`' -X 'main.GO_VERSION=$(go version)'"

6.3 go clean 命令

go clean命令是Go语言中常用的命令之一,用于清理项目目录下生成的可执行文件、测试结果和临时文件等。

使用go clean命令非常简单,只需在终端中输入以下命令:

1
go clean [clean flags] [build flags] [packages]

该命令会删除项目目录下生成的可执行文件、测试结果和临时文件等。它会根据项目的构建配置和操作系统的不同,删除不同的文件。

  • 清理测试结果:
1
go clean -testcache

该命令会删除项目目录下生成的测试结果缓存文件。

  • 清理临时文件:
1
go clean -cache

该命令会删除项目目录下生成的临时文件和缓存文件。

需要注意的是,go clean命令会删除项目目录下生成的文件,因此在使用该命令之前,建议先备份重要的文件。

参数介绍:

参数名 说明
-i 同时删除与可执行文件相关的安装目录
-r 递归删除。该参数会删除指定目录及其子目录下的文件
-n 显示将要执行的清理操作,但不实际执行。使用该参数可以查看清理操作的详细信息,而不会真正删除文件
-cache 清理临时文件和缓存文件。该参数会删除项目目录下生成的临时文件和缓存文件
-testcache 清理测试结果缓存文件。该参数会删除项目目录下生成的测试结果缓存文件
-modcache 清理模块缓存文件。该参数会删除项目目录下生成的模块缓存文件
-x 显示执行的清理操作。使用该参数可以查看清理操作的详细信息,并同时执行清理操作

6.4 go run 命令

用于编译并运行 Go 源代码文件。它是一个方便的工具,可以在不生成可执行文件的情况下,直接编译并运行Go程序。常用的参数包括:

  • -n:打印执行的命令,但不执行。
  • -x:打印执行的命令及参数。
  • -race:启用数据竞争检测。
  • -gcflags:传递参数给编译器,如优化级别等。
  • -buildmode:指定编译模式,如共享库或插件等。
  • -ldflags:传递参数给链接器。
  • -trimpath:去除输出中的文件路径信息。
  • -memprofile:写入内存概要文件。
  • -cpuprofile:写入CPU概要文件。
  • -blockprofile:写入阻塞概要文件。
  • -timeout:执行超时时间。
  • -args:传递参数给程序,放在最后。

例如:

1
go run -race -ldflags "-s -w" main.go args

此命令会启用竞争检测和移除调试信息,并传递args参数给main.go执行。

6.5 go test 命令

go test命令用于运行测试代码并生成测试报告。它可以自动识别项目中的测试文件,并执行其中的测试函数,然后输出测试结果。

使用go test命令非常简单,只需在终端中输入以下命令:

1
go test

该命令会自动在当前目录下寻找以_test.go结尾的文件,并执行其中的测试函数。测试函数的命名必须以Test开头,并接着是要测试的函数名,例如TestAdd()。

除了默认的输出结果外,go test命令还支持一些可选的参数,例如-v参数可以输出更详细的测试结果,-cover参数可以生成代码覆盖率报告等。参数说明:

参数
说明
-v 显示详细的测试信息。使用该参数可以查看每个测试用例的详细输出
-run 指定要运行的测试用例的正则表达式。使用该参数可以选择性地运行符合指定正则表达式的测试用例
-cover 生成代码覆盖率报告。使用该参数可以生成代码覆盖率报告,显示测试代码对被测试代码的覆盖情况
-coverprofile 指定生成的代码覆盖率文件的名称和路径。使用该参数可以将生成的代码覆盖率报告保存到指定的文件中
-covermode 指定代码覆盖率的模式。该参数可以用于指定代码覆盖率的计算方式
-bench 运行性能测试。使用该参数可以运行性能测试,并输出性能测试的结果
-benchmem 显示内存分配的统计信息。使用该参数可以显示性能测试过程中的内存分配情况
-benchtime 指定性能测试的运行时间。该参数可以用于指定性能测试的运行时间,例如设置为"10s"表示运行10秒
-timeout 指定测试的超时时间。该参数可以用于指定测试的超时时间,例如设置为"5s"表示测试超过5秒将被终止
-short 运行短时间的测试。使用该参数可以运行短时间的测试,例如跳过一些耗时较长的测试用例

go test 命令的参数可以组合使用,以满足不同的测试需求。例如,可以使用go test -v -cover命令来运行测试并生成代码覆盖率报告。

6.6 go tool 命令

go tool 命令 运行指定的go工具,命令格式:

1
go tool [-n] command [args...]

可选参数:

  • -n 打印要执行的命令, 但是不真正的执行

go tool 工具列表

Tips: 各个工具源码位于 ${GOROOT}/src/cmd 目录下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
192:~ mac$ go tool
addr2line
asm
buildid
cgo
compile
covdata
cover
dist
doc
fix
link
nm
objdump
pack
pprof       // 可以帮助开发者快速分析及定位各种性能问题,如 CPU 消耗、内存分配及阻塞分析
test2json
trace
vet

查看工具帮助文档

1
go tool command -h

go tool compilego tool compile 编译由传入的file组成的单独的包。默认的,产物是一个.o的的中间目标文件 (intermediate object file) 。 目标文件可以被用来与其他的目标文件组成包集合 (package archive) , 也可以直接传递给链接器使用。

Tips: compile本身也是go实现的,其源码位于src/cmd/compile目录下

生成的目标文件包含包本身暴露的符号的类型信息, 也包含包引用的其他包的符号的类型信息。 所以在编译调用一个包的包时, 只需要读取被调用的包的目标文件即可。

用法:

1
go tool compile [flags] file...
  • file一定需要是一整个package所有的代码文件。

flags参数:

  • -D path 用于本地引用依赖的相对路径。
  • -I dir1 -I dir2 在查询完$GOROOT/pkg/$GOOS_$GOARCH之后, 进一步从dir1和dir2查询需要的依赖包。
  • -L 在错误信息中展示完整的文件路径。
  • -N 禁用优化。
  • -S 将code的汇编输出到标准输出。
  • -S -S code和data都输出。
  • -V 输出编译器的版本。
  • -asmhdr file 将汇编的头写到file中。
  • -buildid id 将id作为build_id写入输出的元数据中。
  • -blockprofile file 将编译时的block profile写入到file中。
  • -c int 编译时并发度,默认是1,表示不并发。
  • -complete 假定包不含有非go的部分。
  • -cpuprofile file 将编译时的CPU profile写入到file中。
  • -dynlink 允许引用在共享库中的go符号。
  • -e 移除错误数量的上限。
  • -goversion string 定义使用的go tool版本, 用于runtime的版本和goversion不匹配的情况。
  • -h 当第一个错误被发现时停止,并输出堆栈trace。
  • -importcfg file 从file读取配置。 配置包含importmap/packagefile。
  • -importmap old=new 在编译时,将对old的引用更换为new。 这个flag可以有多个来设置多个映射。
  • -installsuffix suffix 从$GOROOT/pkg/$GOOS_$GOARCH_suffix查找包, 而不是$GOROOT/pkg/$GOOS_$GOARCH。
  • -l 禁用内联。
  • -lang version 使用的语言版本,如-lang=go1.12, 默认使用当前版本。
  • -linkobj file 将面向链接器的目标文件写入到file。
  • -m 输出优化决定。可以传入整数 (-m=10) ,越大的整数输出越详细。
  • -memprofile file 输出编译时的内存profile到file。
  • -memprofilerate rate 设置编译时的runtime.MemProfileRate为rate。
  • -msan 开启内存检查器 (memory sanitizer) 。
  • -mutexprofile file 编译时的mutex profile写入到file。
  • -nolocalimports 禁用本地引用/相对引用。
  • -o file 将目标文件写入到file。
  • -p path 判断如果增加了对于path的引用是否会出现循环引用的问题。
  • -pack 输出打包过的格式,而不是目标文件。
  • -race 开启数据竞争检测。
  • -s 输出对于可简化掉的Composite literals的警告。
  • -shared 生成可以链接到共享库的代码。
  • -spectre list 在list中启用减轻幽灵攻击的机制。
  • -traceprofile file 将执行trace写入到file中。
  • -trimpath prefix 移除记录的源文件路径的前缀。

关于调试信息的flag:

  • -dwarf 生成DWARF符号。
  • -dwarflocationlists 在优化模式中, 向DWARF增加位置列表 (location list)。
  • -gendwarfinl int 生成DWARF的内联信息记录。

调试编译器本身的标志:

  • -E 调试符号导出。
  • -K 调试缺失的行号。
  • -d list 打印有关列表中项目的调试信息,尝试 -d help 以获得更多信息。
  • -live 调试活性分析。
  • -v 增加调试详细程度。
  • -% 调试非静态初始值设定项。
  • -W 类型检查后调试解析树。
  • -f 调试堆栈帧。
  • -i 调试行号堆栈。
  • -j 调试运行时初始化的变量。
  • -r 调试生成的包装器。
  • -w 调试类型检查。