(240226)
资料参考
Go 语言教程 | runoob
golang的类型推断 | jb51
Golang-100-Days | Github | rubyhan
Go 语言基础之 Context 详解 | zhihu | 程序员祝融
基础
基本语法
Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。
fmt.Println("Hello, World!")
6 个标记是(每行一个):
1. fmt
2. .
3. Println
4. (
5. "Hello, World!"
6. )
在 Go 程序中,一行代表一个语句结束。
注释不会被编译,每一个包应该有相关注释。
单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾。
数据类型
序号 | 类型和描述 |
---|---|
1 | 布尔型 布尔型的值只可以是常量 true 或者 false. |
2 | 数字类型 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
3 | 字符串类型 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 |
4 | 派生类型 包括:(a) 指针类型(Pointer) (b) 数组类型 (c) 结构化类型(struct) (d) Channel 类型 (e) 函数类型 (f) 切片类型 (g) 接口类型(interface) (h) Map 类型 |
数字类型
uint8 | 无符号 8 位整型 (0 到 255) |
---|---|
uint16 | 无符号 16 位整型 (0 到 65535) |
uint32 | 无符号 32 位整型 (0 到 4294967295) |
uint64 | 无符号 64 位整型 (0 到 18446744073709551615) |
int8 | 有符号 8 位整型 (-128 到 127) |
int16 | 有符号 16 位整型 (-32768 到 32767) |
int32 | 有符号 32 位整型 (-2147483648 到 2147483647) |
int64 | 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
float32 | IEEE-754 32位浮点型数 |
float64 | IEEE-754 64位浮点型数 |
complex64 | 32 位实数和虚数 |
complex128 | 64 位实数和虚数 |
byte | 类似 uint8 |
rune | 类似 int32 |
uint | 32 或 64 位 |
int | 与 uint 一样大小 |
uintptr | 无符号整型,用于存放一个指针 |
变量
//定义一个名称为“valName”,类型为"type"的变量
var valName type
//定义三个类型都是“type”的变量
var vname1, vname2, vname3 type
//定义三个类型都是"type"的变量,并且分别初始化为相应的值
//vname1为v1,vname2为v2,vname3为v3
var vname1, vname2, vname3 type= v1, v2, v3
var vname1, vname2, vname3 = v1, v2, v3
vname1, vname2, vname3 := v1, v2, v3
/*
:=这个符号直接取代了var和type,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;
在函数外部使用则会无法编译通过,所以一般用var方式来定义全局变量。
*/
//_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值2赋予b,并同时丢弃1:
_, b := 1, 2
如果变量为私有,且特有名词为首个单词,则使用小写,如 appService
若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
条件语句
if 语句
if 语句的基本语法如下:
if condition {
// 如果条件成立,执行这里的代码块
}
其中,condition
表示一个布尔表达式,如果 condition
为真,则执行 if
后面的代码块,否则不执行。
package main
import "fmt"
func main() {
num := 10
if num < 20 {
fmt.Println("num is less than 20")
}
}
在上面的代码中,我们使用 if
语句判断变量 num
是否小于 20,如果成立,则打印出相应的信息。
除了基本的 if
语句外,Go 语言还支持在条件语句前面添加一个简短的语句,用于初始化变量。例如:
package main
import "fmt"
func main() {
if num := 10; num < 20 {
fmt.Println("num is less than 20")
}
}
在上面的代码中,我们在 if
语句前面添加了一个简短的语句 num := 10
,用于初始化变量 num
的值。然后我们判断 num
是否小于 20,如果成立,则打印出相应的信息。
if condition {
// 如果条件成立,执行这里的代码块
} else {
// 如果条件不成立,执行这里的代码块
}
condition
是一个布尔表达式,如果为真,则执行if语句后面的代码块,否则执行else后面的代码块。可以看出,if...else语句只有两种情况,要么执行if代码块,要么执行else代码块。
除了基本的if...else语句外,Go语言还支持if...else if...else语句,用于根据不同的条件执行不同的代码块。if...else if...else语句的基本语法如下:
if condition1 {
// 如果条件1成立,执行这里的代码块
} else if condition2 {
// 如果条件1不成立,且条件2成立,执行这里的代码块
} else {
// 如果条件1和条件2都不成立,执行这里的代码块
}
condition1
和 condition2
都是布尔表达式,如果 condition1
为真,则执行第一个代码块;如果 condition1
不为真,且 condition2
为真,则执行第二个代码块;否则执行第三个代码块。
除了if...else语句和if...else if...else语句外,Go语言还支持if嵌套语句,用于根据多个条件执行不同的代码块。if嵌套语句的基本语法如下:
if condition1 {
// 如果条件1成立,执行这里的代码块
if condition2 {
// 如果条件1和条件2都成立,执行这里的代码块
}
} else {
// 如果条件1不成立,执行这里的代码块
}
在上面的代码中,我们使用了一层嵌套的if语句,根据两个条件来执行不同的代码块。首先判断 condition1
是否成立,如果成立,则继续判断 condition2
是否成立,如果成立,则执行第二个代码块;否则执行第一个代码块。如果 condition1
不成立,则执行else后面的代码块。
switch 语句
switch 语句的基本语法如下:
switch expression {
case value1:
// 如果 expression 的值等于 value1,执行这里的代码块
case value2:
// 如果 expression 的值等于 value2,执行这里的代码块
default:
// 如果 expression 的值都不等于上面的值,执行这里的代码块
}
其中,expression
表示一个表达式,可以是任何类型,而 value1
、value2
等则表示具体的值。当 expression
的值等于某个 case
后面的值时,就会执行相应的代码块。如果 expression
的值都不等于上面的值,则执行 default
后面的代码块。
package main
import "fmt"
func main() {
num := 3
switch num {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
default:
fmt.Println("other")
}
}
在上面的代码中,我们使用 switch
语句判断变量 num
的值,并根据不同的值执行不同的代码块。
与 if
语句类似,switch
语句也支持在条件语句前面添加一个简短的语句,用于初始化变量。例如:
package main
import "fmt"
func main() {
switch num := 3; num {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
default:
fmt.Println("other")
}
}
在上面的代码中,我们在 switch
语句前面添加了一个简短的语句 num := 3
,用于初始化变量 num
的值。
select 语句
select 语句用于处理通道(Channel)的发送和接收操作。其基本语法如下:
select {
case msg1 := <-channel1:
// 从 channel1 接收数据,并将数据赋值给变量 msg1
// 如果 channel1 没有数据可接收,则阻塞在这里
case channel2 <- msg2:
// 向 channel2 发送数据 msg2
// 如果 channel2 没有空间可发送,则阻塞在这里
default:
// 如果所有的 case 都没有匹配到,则执行这里的代码块
}
其中,channel1
和 channel2
表示通道变量,msg1
和 msg2
表示通道中的数据。当 select
语句执行时,会从多个通道中选择一个有数据可读或者有空间可写的通道执行相应的操作。如果所有的通道都没有数据可读或者没有空间可写,则执行 default
后面的代码块。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan int)
go func() {
ch1 <- "hello"
}()
go func() {
ch2 <- 10
}()
select {
case str := <-ch1:
fmt.Println(str)
case num := <-ch2:
fmt.Println(num)
}
}
在上面的代码中,我们定义了两个通道 ch1
和 ch2
,并将字符串 "hello"
和整数 10
分别发送到这两个通道中。然后我们使用 select
语句从多个通道中选择一个有数据可读的通道,并打印出其结果。由于通道中的数据是异步发送和接收的,因此输出的结果可能是字符串 "hello"
或者整数 10
中的任意一个。
循环语句
for循环
for循环是Go语言中最基本的循环语句,用于重复执行一定的代码块。它的基本语法如下:
for 初始化语句; 条件表达式; 后置语句 {
// 循环体
}
其中,初始化语句用于初始化循环变量,条件表达式用于判断是否继续执行循环,后置语句用于更新循环变量。循环体是需要重复执行的代码块。
循环嵌套
循环嵌套是指在一个循环语句中嵌套另一个循环语句,以实现更复杂的循环逻辑。例如,下面的代码展示了一个简单的循环嵌套:
for i := 0; i < 10; i++ {
for j := 0; j < 5; j++ {
fmt.Print(i * j, " ")
}
fmt.Println()
}
/*
0 0 0 0 0
0 1 2 3 4
0 2 4 6 8
0 3 6 9 12
0 4 8 12 16
0 5 10 15 20
0 6 12 18 24
0 7 14 21 28
0 8 16 24 32
0 9 18 27 36
*/
在上面的代码中,我们使用了两个for循环,一个是外层的循环,一个是内层的循环。内层的循环会在每次外层循环执行时重复执行,以实现更复杂的循环逻辑。
break语句
break语句用于跳出循环,即在循环体中使用break语句会立即退出循环。例如,下面的代码展示了如何使用break语句:
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Print(i, " ")
}
//0 1 2 3 4
在上面的代码中,当i等于5时,使用break语句跳出循环,不再执行后面的代码。
continue语句
continue语句用于跳过循环中的某一次迭代,即在循环体中使用continue语句会立即跳过本次循环,进入下一次循环。例如,下面的代码展示了如何使用continue语句:
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Print(i, " ")
}
//1 3 5 7 9
在上面的代码中,当i是偶数时,使用continue语句跳过本次循环,不再执行后面的代码。
goto语句
goto语句用于无条件跳转到指定的标签,即在循环体中使用goto语句可以跳转到指定的标签处执行代码。例如,下面的代码展示了如何使用goto语句:
for i := 0; i < 10; i++ {
if i == 5 {
goto LABEL
}
fmt.Print(i, " ")
}
LABEL:
fmt.Println("Jumped to label")
在上面的代码中,当i等于5时,使用goto语句跳转到标签LABEL处执行代码。注意,使用goto语句会增加代码的复杂度和阅读难度,因此应该尽量避免使用。
包,导入导出
Package(包):Go语言中的包是多个Go源代码的集合,用于组织源代码。每个包都有一个唯一的包名,这个包名是其源文件所在目录的名称。包的定义可以通过package关键字完成,例如package main或package src/main,其中src是源文件的目录路径。Go语言提供了多种内置包,如fmt、os、io等,这些包可以被其他代码引用。
Imports(导入):导入包是Go语言中引用其他包代码的基本方法。使用import关键字,可以在代码中直接引用其他包的内容。导入的方式有多种,包括点操作、相对路径和绝对路径等。例如,import "fmt"会从标准库目录$GOROOT/src/fmt加载fmt模块,而import "./test"则导入同一目录下的test包中的内容。导入支持单行导入和多行导入,当多行导入时,包名在import中的顺序不影响导入效果。
Exports(导出):在Go语言中,一个包的内容可以通过其导出标识符来访问。导出标识符通常位于包的头部,并以引号括起来。例如,fmt.Print("hello world")调用了fmt模块中的函数,该函数返回一个字符串,然后打印到控制台上。
在Go语言中,导入包的最佳实践主要包括以下几点:
单行导入与多行导入:Go语言中一个包可以包含多个.go文件,这些文件必须在同一级文件夹中。导入包主要有两种方式:单行导入和多行导入。单行导入是直接使用import "包名";而多行导入则是通过括号(包名)来导入多个包。
使用别名避免冲突:在导入多个具有同一包名的包时,可能会产生冲突。为了避免这种情况,可以为其中一个包定义一个别名,例如import ( "crypto/rand" mrand "math/rand"),这样就可以将冲突的包名替换为别名。
正确导入包:仅导入需要的包,并格式化导入部分以对标准库包、第三方包和自己的包进行分组。这有助于保持代码的整洁和可维护性。
使用go module导入本地包:对于本地包的导入,可以使用go module命令。这意味着不需要将项目目录放在GOPATH中,也不使用vendor目录。而是统一安装到$GOPATH/pkg/mod/cache中,在build/run时,自动析出项目import的包并安装。
格式化导入部分:为了更好地组织和管理包,应该格式化导入部分以将标准库包、第三方包和自己的包分组。这样做可以帮助开发者更快地找到所需的包,同时也便于团队成员之间的协作。
函数,多返回值,命名返回值
Go语言中的函数是一种可重用的代码块,用于封装特定的功能。函数定义格式如下:
func 函数名(参数列表) 返回值类型 {
函数体
}
其中,参数列表可以为空,也可以包含多个参数,每个参数包含名称和类型。返回值类型可以是单个类型或多个类型组成的元组。函数体中实现了函数的具体功能,并通过return语句返回结果。
下面是一些常见的函数用法:
函数调用
函数调用格式如下:
函数名(参数列表)
参数列表中需要传递与函数定义中参数列表相同类型和数量的参数。例如:
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
res := add(1, 2)
fmt.Println(res) // 输出 3
}
返回多值
函数可以返回多个值,这些值可以是不同类型的。例如:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func main() {
res, err := divide(10, 2)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(res) // 输出 5
}
}
这个例子中,我们定义了一个名为divide的函数,该函数接收两个float64类型的参数a和b,并返回两个值:a/b和一个error类型的值(用于处理除数为0的情况)。在主函数中,我们调用了divide函数,并使用res和err两个变量分别接收其返回值。如果err不为nil,则说明除数为0,需要进行错误处理;否则,我们可以使用res变量来获取计算结果。
函数参数
函数可以接收多种参数类型,包括:
- 值类型:函数接收的参数是值的副本,对参数的修改不会影响原始值;
- 指针类型:函数接收的参数是指向值的指针,对参数的修改会影响原始值;
- 可变参数:函数接收的参数数量是可变的,可以接收任意数量的参数。
例如:
func changeValue(a int) {
a = 10
}
func changePointer(a *int) {
*a = 10
}
func sum(nums ...int) int {
res := 0
for _, num := range nums {
res += num
}
return res
}
func main() {
num := 5
changeValue(num)
fmt.Println(num) // 输出 5
changePointer(&num)
fmt.Println(num) // 输出 10
res := sum(1, 2, 3, 4, 5)
fmt.Println(res) // 输出 15
}
这个例子中,我们定义了三个函数changeValue、changePointer和sum,分别演示了值类型、指针类型和可变参数的用法。在主函数中,我们先定义了一个变量num,然后分别调用了changeValue和changePointer函数,分别修改了其值。最后,我们调用了sum函数,传递了5个参数,并将结果赋给了res变量。
(接收一个可变参数nums,类型为int类型的切片。使用for循环遍历nums中的每个元素,并将它们加起来,最后返回总和。range遍历nums切片中的每个元素,通过_忽略了每个元素的索引,只保留了元素本身。
...int表示可变参数列表,也称为不定参数。这意味着函数可以接受任意数量的int类型参数,并将它们视为一个int类型的切片。)
func sum(nums ...int) int { // 定义一个名为sum的函数,参数为int类型的可变参数nums,返回值为int类型
res := 0 // 定义一个变量res,初始值为0
for i := range nums { // 使用for循环遍历nums切片中的每个元素,其中i表示当前元素的索引,num表示当前元素的值
res += nums[i] // 将当前元素加到变量res中
}
return res // 返回变量res的值,表示所有元素的和
}
func main() {
res := sum(1, 2, 3, 4, 5) // 调用sum函数,传递5个int类型的参数,并将返回值赋给res变量
fmt.Println(res) // 在控制台输出res的值,即所有参数的和
}
在这个例子中,我们将循环改为使用变量i表示当前元素的索引,这样可以访问nums切片中的任意元素。在循环体中,我们使用nums[i]访问当前元素的值,并将其加到变量res中。
需要注意的是,在这种情况下,我们不能使用_来忽略索引,因为我们需要访问每个元素的值和索引。因此,我们需要显式地将索引变量命名为i或其他名称。
这个函数仍然接受任意数量的int类型参数,并将它们视为一个int类型的切片。
defer
defer是Go语言的一个关键字,它可以让我们在函数执行完毕之后再执行一些特定的操作。无论函数是通过return正常返回,还是触发了panic异常,defer语句都能够确保在函数退出前被执行。
下面是一个简单的例子,演示defer语句的使用:
func main() {
defer fmt.Println("deferred statement")
fmt.Println("hello")
}
在这个例子中,我们在main函数中使用了defer语句,将一条语句fmt.Println("deferred statement")推迟到函数返回前执行。在函数体中,我们先输出了一条hello语句,然后函数执行完毕,defer语句被执行,输出了deferred statement。最终,程序退出。
defer语句的执行顺序是后进先出,也就是说,最后一个defer语句会最先执行,而第一个defer语句会最后执行。例如:
func main() {
defer fmt.Println("deferred statement 1")
defer fmt.Println("deferred statement 2")
defer fmt.Println("deferred statement 3")
fmt.Println("hello")
}
在这个例子中,我们使用了三个defer语句,分别输出了三条deferred statement语句。在函数体中,我们先输出了一条hello语句。最终,程序退出时,defer语句会按照后进先出的顺序执行,先输出deferred statement 3,然后是deferred statement 2,最后是deferred statement 1。
func foo() (string, int) {
a, b := 3, 5
c := a + b
defer fmt.Println("deferred statement 1", c)
defer fmt.Println("deferred statement 2", c)
defer func() {
defer fmt.Println("deferred statement 3", c)
}()
c = 100
fmt.Println("hello")
return "result:", c
}
func main(){
foo()
}
/*
hello
deferred statement 3 100
deferred statement 2 8
deferred statement 1 8
*/
defer语句中的变量c的值是在调用defer语句时确定的,而不是在执行defer语句时确定的。因此,第一个和第二个defer语句中输出的变量c的值都是8,而第三个defer语句中输出的变量c的值是100。这是因为在第三个defer语句中,我们使用了一个匿名函数,延迟了defer语句的执行,使得变量c的值在执行时已经被修改为了100。
数组
声明数组
Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:
var arrayName [size]dataType
其中,arrayName 是数组的名称,size 是数组的大小,dataType 是数组中元素的数据类型。
以下定义了数组 balance 长度为 10 类型为 float32:
var balance [10]float32
package main
import "fmt"
func main() {
var n [10]int /* n 是一个长度为 10 的数组 */
var i,j int
/* 为数组 n 初始化元素 */
for i = 0; i < 10; i++ {
n[i] = i + 100 /* 设置元素为 i + 100 */
}
/* 输出每个数组元素的值 */
for j = 0; j < 10; j++ {
fmt.Printf("Element[%d] = %d\n", j, n[j] )
}
}
Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109
package main
import "fmt"
func main() {
var i,j,k int
// 声明数组的同时快速初始化数组
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
/* 输出数组元素 */ ...
for i = 0; i < 5; i++ {
fmt.Printf("balance[%d] = %f\n", i, balance[i] )
}
balance2 := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
/* 输出每个数组元素的值 */
for j = 0; j < 5; j++ {
fmt.Printf("balance2[%d] = %f\n", j, balance2[j] )
}
// 将索引为 1 和 3 的元素初始化
balance3 := [5]float32{1:2.0,3:7.0}
for k = 0; k < 5; k++ {
fmt.Printf("balance3[%d] = %f\n", k, balance3[k] )
}
}
balance[0] = 1000.000000
balance[1] = 2.000000
balance[2] = 3.400000
balance[3] = 7.000000
balance[4] = 50.000000
balance2[0] = 1000.000000
balance2[1] = 2.000000
balance2[2] = 3.400000
balance2[3] = 7.000000
balance2[4] = 50.000000
balance3[0] = 0.000000
balance3[1] = 2.000000
balance3[2] = 0.000000
balance3[3] = 7.000000
balance3[4] = 0.000000
多维数组
二维数组
二维数组是最简单的多维数组,二维数组本质上是由一维数组组成的。二维数组定义方式如下:
var arrayName [ x ][ y ] variable_type
variable_type 为 Go 语言的数据类型,arrayName 为数组名,二维数组可认为是一个表格,x 为行,y 为列,下图演示了一个二维数组 a 为三行四列:
package main
import "fmt"
func main() {
// Step 1: 创建数组
values := [][]int{}
// Step 2: 使用 append() 函数向空的二维数组添加两行一维数组
row1 := []int{1, 2, 3}
row2 := []int{4, 5, 6}
values = append(values, row1)
values = append(values, row2)
// Step 3: 显示两行数据
fmt.Println("Row 1")
fmt.Println(values[0])
fmt.Println("Row 2")
fmt.Println(values[1])
// Step 4: 访问第一个元素
fmt.Println("第一个元素为:")
fmt.Println(values[0][0])
}
Row 1
[1 2 3]
Row 2
[4 5 6]
第一个元素为:
1
初始化二维数组
多维数组可通过大括号来初始值。以下实例为一个 3 行 4 列的二维数组:
a := [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}
package main
import "fmt"
func main() {
// 创建二维数组
sites := [2][2]string{}
// 向二维数组添加元素
sites[0][0] = "Google"
sites[0][1] = "Runoob"
sites[1][0] = "Taobao"
sites[1][1] = "Weibo"
// 显示结果
fmt.Println(sites)
}
[[Google Runoob] [Taobao Weibo]]
访问二维数组
二维数组通过指定坐标来访问。如数组中的行索引与列索引,例如:
val := a[2][3]
或
var value int = a[2][3]
package main
import "fmt"
func main() {
/* 数组 - 5 行 2 列*/
var a = [5][2]int{ {0,0}, {1,2}, {2,4}, {3,6},{4,8}}
var i, j int
/* 输出数组元素 */
for i = 0; i < 5; i++ {
for j = 0; j < 2; j++ {
fmt.Printf("a[%d][%d] = %d\n", i,j, a[i][j] )
}
}
}
结构体
在Go语言中,结构体(struct)是一种自定义的数据类型,它可以包含多个字段,每个字段都有自己的类型和值。结构体的定义和使用也非常简单。例如,我们可以定义一个名为Dog的结构体,它包含了两个字段:Name和Age。
type Dog struct {
Name string
Age int
}
在这个例子中,我们定义了一个名为Dog的结构体,它包含了两个字段:Name和Age。这个结构体可以用来表示一只狗的信息,包括它的名字和年龄。
切片
Go语言中的切片是一种灵活、动态的数据结构,它与数组类似,但长度是可变的。切片函数是用于操作切片的一系列内置函数,包括len()、cap()、nil、append()、copy()
等等。
len()函数
len()函数用于获取切片的长度,即切片中元素的个数。例如,下面的代码展示了如何使用len()函数获取切片的长度:
s := []int{1, 2, 3, 4, 5}
fmt.Println(len(s)) // 输出:5
这是一个包含5个整数的切片,切片名为s,元素为1、2、3、4、5。可以通过下标访问切片中的元素,例如s[0]表示获取第一个元素,即1。
在上面的代码中,使用len()函数获取切片s的长度,即5。
cap()函数
cap()函数用于获取切片的容量,即切片可以容纳的元素个数。例如,下面的代码展示了如何使用cap()函数获取切片的容量:
s := make([]int, 5, 10)
fmt.Println(cap(s)) // 输出:10
在上面的代码中,使用make()函数创建一个切片,长度为5,容量为10,使用cap()函数获取切片s的容量,即10。
容量表示切片底层数组的长度,长度表示切片中元素的个数。
(make函数,像是规定一个盒子,盒子大小是能放10个东西,长度是你放了5个东西在能放10个东西的盒子里面)
nil切片
nil切片是指没有分配任何数据空间的切片,它的长度和容量为0。在Go语言中,切片的零值就是nil切片。例如,下面的代码展示了如何创建一个nil切片:
var s []int
fmt.Println(s == nil) // 输出:true
在上面的代码中,定义一个变量s,它的类型为[]int,由于没有分配任何数据空间,s就是一个nil切片。
append()函数
append()函数用于向切片中追加元素,可以一次追加一个或多个元素。如果切片容量不足以容纳新元素,则会自动扩容。例如,下面的代码展示了如何使用append()函数向切片中追加元素:
s := []int{1, 2, 3}
s = append(s, 4, 5, 6)
fmt.Println(s) // 输出:[1 2 3 4 5 6]
在上面的代码中,定义一个切片s,包含元素1、2、3,使用append()函数向切片s中追加元素4、5、6,最终s包含元素1、2、3、4、5、6。
copy()函数
copy()函数用于将一个切片的内容复制到另一个切片中。例如,下面的代码展示了如何使用copy()函数复制切片:
s1 := []int{1, 2, 3}
s2 := make([]int, len(s1))
copy(s2, s1)
fmt.Println(s2) // 输出:[1 2 3]
在上面的代码中,定义一个切片s1,包含元素1、2、3,使用make()函数创建一个长度与s1相同的切片s2,使用copy()函数将s1中的内容复制到s2中,最终s2包含元素1、2、3。
集合
Go语言中的map是一种无序的键值对集合,其中每个键唯一对应一个值。map中的所有键和所有值的类型必须相同,可以是任何内置或自定义类型。下面是一个简单的例子,代码中注释有详细说明:
package main
import "fmt"
func main() {
// 创建一个空的map,键为string类型,值为int类型
m1 := make(map[string]int)
// 向map中添加键值对
m1["a"] = 1
m1["b"] = 2
m1["c"] = 3
// 获取map中指定键的值
fmt.Println("m1[a] =", m1["a"]) // 输出 m1[a] = 1
// 获取map的长度
fmt.Println("len(m1) =", len(m1)) // 输出 len(m1) = 3
// 判断map中是否存在指定键
val, ok := m1["d"]
if ok {
fmt.Println("m1[d] =", val)
} else {
fmt.Println("m1[d] does not exist")
}//m1[d] does not exist
// 删除map中的指定键值对
delete(m1, "c")
// 遍历map中的键值对
for k, v := range m1 {
fmt.Println(k, v)
}
/*
a 1
b 2
*/
}
- 这个例子中,我们首先创建了一个空的map,键为string类型,值为int类型。
- 然后我们向map中添加了三个键值对,分别是"a":1、"b":2和"c":3。我们可以通过指定键来获取map中的值,例如m1["a"]返回1。我们可以使用len()函数获取map的长度,例如len(m1)返回3。
- 我们可以通过判断第二个返回值来判断map中是否存在指定的键,例如val, ok := m1["d"],如果键"d"不存在,ok将为false,否则将为true,并且val将为键"d"对应的值。
- 我们可以使用delete()函数删除map中的指定键值对,例如delete(m1, "c")将删除键"c"对应的值3。
- 最后我们使用for循环遍历map中的所有键值对,并输出它们的键和值。
(可以看作SQL里面的建表,create table test ( string int );,insert into test (string,int) values (a,1))
(ok := 就是判断是否有这个值)
范围
package main
import "fmt"
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
以上实例运行输出结果为:
2**0 = 1
2**1 = 2
2**2 = 4
2**3 = 8
2**4 = 16
2**5 = 32
2**6 = 64
2**7 = 128
for 循环的 range 格式可以省略 key 和 value
package main
import "fmt"
func main() {
map1 := make(map[int]float32)
map1[1] = 1.0
map1[2] = 2.0
map1[3] = 3.0
map1[4] = 4.0
// 读取 key 和 value
for key, value := range map1 {
fmt.Printf("key is: %d - value is: %f\n", key, value)
}
// 读取 key
for key := range map1 {
fmt.Printf("key is: %d\n", key)
}
// 读取 value
for _, value := range map1 {
fmt.Printf("value is: %f\n", value)
}
}
key is: 4 - value is: 4.000000
key is: 1 - value is: 1.000000
key is: 2 - value is: 2.000000
key is: 3 - value is: 3.000000
key is: 1
key is: 2
key is: 3
key is: 4
value is: 1.000000
value is: 2.000000
value is: 3.000000
value is: 4.000000
package main
import "fmt"
func main(){
nums := []int{1,2,3,4}
for i,num := range nums {
fmt.Printf("索引是%d,长度是%d\n",i, num)
}
}
索引是0,长度是1
索引是1,长度是2
索引是2,长度是3
索引是3,长度是4
make函数
s := make([]int, 5, 10)
fmt.Println(cap(s)) // 输出:10
(make函数,像是规定一个盒子,盒子大小是能放10个东西,长度是你放了5个东西在能放10个东西的盒子里面)
类型转换
Go 语言类型转换基本格式如下:
type_name(expression)
type_name 为类型,expression 为表达式。
数值类型转换
以下实例中将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量:
package main
import "fmt"
func main() {
var sum int = 17
var count int = 5
var mean float32
mean = float32(sum)/float32(count)
fmt.Printf("mean 的值为: %f\n",mean)
}
mean 的值为: 3.400000
字符串类型转换
将一个字符串转换成另一个类型,可以使用以下语法:
var str string = "10"
var num int
num, _ = strconv.Atoi(str)
以上代码将字符串变量 str 转换为整型变量 num。strconv.Atoi 函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误
package main
import (
"fmt"
"strconv"
)
func main() {
str := "123"
num, err := strconv.Atoi(str)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num)
}
}
以上实例执行输出结果为:
字符串 '123' 转换为整数为:123
package main
import (
"fmt"
"strconv"
)
func main() {
num := 123
str := strconv.Itoa(num)
fmt.Printf("整数 %d 转换为字符串为:'%s'\n", num, str)
}
以上实例执行输出结果为:
整数 123 转换为字符串为:'123'
package main
import (
"fmt"
"strconv"
)
func main() {
str := "3.14"
num, err := strconv.ParseFloat(str, 64)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 '%s' 转为浮点型为:%f\n", str, num)
}
}
以上实例执行输出结果为:
字符串 '3.14' 转为浮点型为:3.140000
package main
import (
"fmt"
"strconv"
)
func main() {
num := 3.14
str := strconv.FormatFloat(num, 'f', 2, 64)
fmt.Printf("浮点数 %f 转为字符串为:'%s'\n", num, str)
}
以上实例执行输出结果为:
浮点数 3.140000 转为字符串为:'3.14'
接口类型转换
类型断言和类型转换
类型断言用于将接口类型转换为指定类型,其语法为:
value.(type)
或者
value.(T)
其中 value 是接口类型的变量,type 或 T 是要转换成的类型。
如果类型断言成功,它将返回转换后的值和一个布尔值,表示转换是否成功。
package main
import "fmt"
func main() {
var i interface{} = "Hello, World"
str, ok := i.(string)
if ok {
fmt.Printf("'%s' is a string\n", str)
} else {
fmt.Println("conversion failed")
}
}
以上实例中,我们定义了一个接口类型变量 i,并将它赋值为字符串 "Hello, World"。然后,我们使用类型断言将 i 转换为字符串类型,并将转换后的值赋值给变量 str。最后,我们使用 ok 变量检查类型转换是否成功,如果成功,我们打印转换后的字符串;否则,我们打印转换失败的消息。
类型转换用于将一个接口类型的值转换为另一个接口类型,其语法为:
T(value)
T 是目标接口类型,value 是要转换的值。
在类型转换中,我们必须保证要转换的值和目标接口类型之间是兼容的,否则编译器会报错。
package main
import "fmt"
type Writer interface {
Write([]byte) (int, error)
}
type StringWriter struct {
str string
}
func (sw *StringWriter) Write(data []byte) (int, error) {
sw.str += string(data)
return len(data), nil
}
func main() {
var w Writer = &StringWriter{}
sw := w.(*StringWriter)
sw.str = "Hello, World"
fmt.Println(sw.str)
}
以上实例中,我们定义了一个 Writer 接口和一个实现了该接口的结构体 StringWriter。然后,我们将 StringWriter 类型的指针赋值给 Writer 接口类型的变量 w。接着,我们使用类型转换将 w 转换为 StringWriter 类型,并将转换后的值赋值给变量 sw。最后,我们使用 sw 访问 StringWriter 结构体中的字段 str,并打印出它的值。
类型推断
a := 123
var a = 123
var a int = 123.0
上述三个语句是等效的。但编译阶段的执行细节是不同的。
1.a := 123
会显式的触发类型推断,编译器解析右边的每一个字符为十进制数字(IntLit),然后构建为一个整型节点,在类型检查的时候,将其类型赋值给左边的节点变量a。
2.由于var a = 123
左边的a未显式指定其类型,因此仍然会触发类型推断,ir.AssignStmt.Def=false
,过程同上,依然在类型检查的时候,将123的类型赋值给左边的a。
3.对于var a int = 123.0
, 由于123.0包含小数点'.',编译器解析右边的每一个字符为十进制浮点数(FloatLit),由于赋值操作符=左边显式定义了a的类型为int, 因此在类型检查阶段,右边的123.0会发生隐式类型转换,因为类型兼容,会转换为整型123。因此对于显式指定类型的表达式不会发生类型推断。
错误处理
- 错误处理的原则就是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。接收到错误,要么返回err,或者使用log记录下来
- 尽早return:一旦有错误发生,马上返回
- 尽量不要使用panic,除非你知道你在做什么
- 错误描述如果是英文必须为小写,不需要标点结尾
- 采用独立的错误流进行处理
// 错误写法
if err != nil {
// error handling
} else {
// normal code
}
// 正确写法
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
type User struct {
username string
password string
}
// 初始化User对象的方法
func (p *User) init(username string ,password string) (*User, string) {
// 检查用户名和密码是否为空
if "" == username || "" == password {
return p, p.Error()
}
// 初始化User对象的属性
p.username = username
p.password = password
return p, ""
}
// 返回错误消息字符串的方法
func (p *User) Error() string {
return "Username or password shouldn't be empty!"
}
func main() {
var user User
user1, _ := user.init("", "")
fmt.Println(user1)
}
结果:
Usernam or password shouldn't be empty!
这段代码定义了一个名为User
的结构体,包含username
和password
两个属性。
这是User
结构体的init
方法,通过指针接收者(*User
)来关联该方法与结构体。该方法接受username
和password
作为参数,并将其赋值给结构体的属性。如果其中任意一个参数为空字符串,则返回当前的User
指针和一个错误字符串。否则,将参数赋值给结构体的属性,并返回当前的User
指针和一个空字符串。
这是User
结构体的Error
方法,同样使用指针接收者来关联该方法与结构体。该方法返回一个错误字符串,指示用户名或密码不应为空。
在main
函数中,首先声明了一个User
类型的变量user
。然后调用user.init
方法,并将空字符串作为参数传递给该方法。返回的结果被存储在user1
变量中,但由于使用了空标识符 _
,忽略了第二个返回值。最后,使用fmt.Println
打印出user1
的值。
总体上,该程序的功能是创建一个User
对象并初始化其属性。如果传递的用户名或密码为空,则返回错误消息。在示例中,由于传递了空字符串作为参数,因此输出的结果将是一个指向未初始化的User
对象的指针。
panic()和recover()
Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。 一直等到包含defer语句的函数执行完毕时,延迟函数(defer后的函数)才会被执行,而不管包含defer语句的函数是通过return的正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。 当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。如果一路在延迟函数中没有recover函数的调用,则会到达该协程的起点,该协程结束,然后终止其他所有协程,包括主协程(类似于C语言中的主线程,该协程ID为1)。
panic: 1、内建函数 2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行 3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行,这里的defer 有点类似 try-catch-finally 中的 finally 4、直到goroutine整个退出,并报告错误
recover: 1、内建函数 2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为 3、一般的调用建议 a). 在defer函数中,通过recever来终止一个gojroutine的panicking过程,从而恢复正常代码的执行 b). 可以获取通过panic传递的error
简单来讲:go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
错误和异常从Golang机制上讲,就是error和panic的区别。很多其他语言也一样,比如C++/Java,没有error但有errno,没有panic但有throw。
Golang错误和异常是可以互相转换的:
错误转异常,比如程序逻辑上尝试请求某个URL,最多尝试三次,尝试三次的过程中请求失败是错误,尝试完第三次还不成功的话,失败就被提升为异常了。
异常转错误,比如panic触发的异常被recover恢复后,将返回值中error类型的变量进行赋值,以便上层函数继续走错误处理流程。
以下给出异常处理的作用域(场景):
- 空指针引用
- 下标越界
- 除数为0
- 不应该出现的分支,比如default
- 输入不应该引起函数错误
错误处理实践
1.失败的原因只有一个时,不使用error
func (self *AgentContext) CheckHostType(host_type string) error {
switch host_type {
case "virtual_machine":
return nil
case "bare_metal":
return nil
}
return errors.New("CheckHostType ERROR:" + host_type)
}
我们可以看出,该函数失败的原因只有一个,所以返回值的类型应该为bool,而不是error,重构一下代码:
func (self *AgentContext) IsValidHostType(hostType string) bool {
return hostType == "virtual_machine" || hostType == "bare_metal"
}
说明:大多数情况,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息,这时的返回值类型不再是简单的bool,而是error。
2.没有失败时,不使用error
error在Golang中是如此的流行,以至于很多人设计函数时不管三七二十一都使用error,即使没有一个失败原因。 我们看一下示例代码:
func (self *CniParam) setTenantId() error {
self.TenantId = self.PodNs
return nil
}
对于上面的函数设计,就会有下面的调用代码:
err := self.setTenantId()
if err != nil {
// log
// free resource
return errors.New(...)
}
根据我们的正确姿势,重构一下代码:
func (self *CniParam) setTenantId() {
self.TenantId = self.PodNs
}
于是调用代码变为:
self.setTenantId()
3.error应放在返回值类型列表的最后
对于返回值类型error,用来传递错误信息,在Golang中通常放在最后一个。
resp, err := http.Get(url)
if err != nil {
return nill, err
}
bool作为返回值类型时也一样。
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist…
}
4.错误值统一定义,而不是跟着感觉走
很多人写代码时,到处return errors.New(value),而错误value在表达同一个含义时也可能形式不同,比如“记录不存在”的错误value可能为:
"record is not existed."
"record is not exist!"
"###record is not existed!!!"
...
这使得相同的错误value撒在一大片代码里,当上层函数要对特定错误value进行统一处理时,需要漫游所有下层代码,以保证错误value统一,不幸的是有时会有漏网之鱼,而且这种方式严重阻碍了错误value的重构。
于是,我们可以参考C/C++的错误码定义文件,在Golang的每个包中增加一个错误对象定义文件,如下所示:
var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")
5.错误逐层传递时,层层都加日志
层层都加日志非常方便故障定位。
说明:至于通过测试来发现故障,而不是日志,目前很多团队还很难做到。如果你或你的团队能做到,那么请忽略这个姿势。
6.错误处理使用defer
我们一般通过判断error的值来处理错误,如果当前操作失败,需要将本函数中已经create的资源destroy掉,示例代码如下:
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
err = createResource2()
if err != nil {
destroyResource1()
return ERR_CREATE_RESOURCE2_FAILED
}
err = createResource3()
if err != nil {
destroyResource1()
destroyResource2()
return ERR_CREATE_RESOURCE3_FAILED
}
err = createResource4()
if err != nil {
destroyResource1()
destroyResource2()
destroyResource3()
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}
当Golang的代码执行时,如果遇到defer的闭包调用,则压入堆栈。当函数返回时,会按照后进先出的顺序调用闭包。 对于闭包的参数是值传递,而对于外部变量却是引用传递,所以闭包中的外部变量err的值就变成外部函数返回时最新的err值。
根据这个结论,我们重构上面的示例代码:
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
defer func() {
if err != nil {
destroyResource2()
}
}()
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
defer func() {
if err != nil {
destroyResource3()
}
}()
err = createResource4()
if err != nil {
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}
7.当尝试几次可以避免失败时,不要立即返回错误
如果错误的发生是偶然性的,或由不可预知的问题导致。一个明智的选择是重新尝试失败的操作,有时第二次或第三次尝试时会成功。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
两个案例:
我们平时上网时,尝试请求某个URL,有时第一次没有响应,当我们再次刷新时,就有了惊喜。
团队的一个QA曾经建议当Neutron的attach操作失败时,最好尝试三次,这在当时的环境下验证果然是有效的。
8.当上层函数不关心错误时,建议不返回error
对于一些资源清理相关的函数(destroy/delete/clear),如果子函数出错,打印日志即可,而无需将错误进一步反馈到上层函数,因为一般情况下,上层函数是不关心执行结果的,或者即使关心也无能为力,于是我们建议将相关函数设计为不返回error。
9.当发生错误时,不忽略有用的返回值
通常,当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,应该将读取到的字符串和错误信息一起打印出来。
说明:对函数的返回值要有清晰的说明,以便于其他人使用。
异常处理实践
1.在程序开发阶段,坚持速错
速错,简单来讲就是“让它挂”,只有挂了你才会第一时间知道错误。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。
2.在程序部署后,应恢复异常避免程序终止
在Golang中,某个Goroutine如果panic了,并且没有recover,那么整个Golang进程就会异常退出。所以,一旦Golang程序部署后,在任何情况下发生的异常都不应该导致程序异常退出,我们在上层函数中加一个延迟执行的recover调用来达到这个目的,并且是否进行recover需要根据环境变量或配置文件来定,默认需要recover。 这个姿势类似于C语言中的断言,但还是有区别:一般在Release版本中,断言被定义为空而失效,但需要有if校验存在进行异常保护,尽管契约式设计中不建议这样做。在Golang中,recover完全可以终止异常展开过程,省时省力。
我们在调用recover的延迟函数中以最合理的方式响应该异常:
打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见;
将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行。
我们看一个简单的例子:
func funcA() error {
defer func() {
if p := recover(); p != nil {
fmt.Printf("panic recover! p: %v", p)
debug.PrintStack()
}
}()
return funcB()
}
func funcB() error {
// simulation
panic("foo")
return errors.New("success")
}
func test() {
err := funcA()
if err == nil {
fmt.Printf("err is nil\\n")
} else {
fmt.Printf("err is %v\\n", err)
}
}
我们期望test函数的输出是:
err is foo
实际上test函数的输出是:
err is nil
原因是panic异常处理机制不会自动将错误信息传递给error,所以要在funcA函数中进行显式的传递,代码如下所示:
func funcA() (err error) {
defer func() {
if p := recover(); p != nil {
fmt.Println("panic recover! p:", p)
str, ok := p.(string)
if ok {
err = errors.New(str)
} else {
err = errors.New("panic")
}
debug.PrintStack()
}
}()
return funcB()
}
3.对于不应该出现的分支,使用异常处理
当某些不应该发生的场景发生时,我们就应该调用panic函数来触发异常。比如,当程序到达了某条逻辑上不可能到达的路径:
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}
4.针对入参不应该有问题的函数,使用panic设计
入参不应该有问题一般指的是硬编码,我们先看这两个函数(Compile和MustCompile),其中MustCompile函数是对Compile函数的包装:
func MustCompile(str string) *Regexp {
regexp, error := Compile(str)
if error != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
}
return regexp
}
所以,对于同时支持用户输入场景和硬编码场景的情况,一般支持硬编码场景的函数是对支持用户输入场景函数的包装。 对于只支持硬编码单一场景的情况,函数设计时直接使用panic,即返回值类型列表中不会有error,这使得函数的调用处理非常方便(没有了乏味的"if err != nil {/ 打印 && 错误处理 /}"代码块)。
深入了解
依赖管理(Go Modules)
go mod init <module>
:初始化一个新的模块,生成go.mod
文件。go mod tidy
:根据go.mod
文件,检查当前模块所需的依赖,并将它们添加到go.mod
文件中。go mod vendor
:将当前模块所需的依赖复制到vendor/
目录中。go mod download
:下载当前模块所需的所有依赖。go mod graph
:打印当前模块依赖关系图。go mod verify
:验证依赖是否正确并且没有被篡改。go mod why <module>
:解释为什么需要依赖某个模块。go list -m all
:列出所有的依赖模块。
实践
(VMware Workstation,Ubuntu 22.04 LTS,FinalShell)
go version
mkdir Workspace
mkdir Workspace/Space{1..2}
mkdir Workspace/Space2/Space3
cd Workspace
go mod init Test_go
cat Test_go
(Space1,Test_main.go)
package main
import (
"fmt"
"Test_go/Space2"
math "Test_go/Space2/Space3"
"github.com/bytedance/sonic"
)
func main(){
fmt.Println(util.Name)
fmt.Println(util.Add(3,4))
fmt.Println(math.Add(1,2,3))
bytes,_ := sonic.Marshal("hello")
fmt.Println(string(bytes))
}
(Space2,a.go)
package util
import "fmt"
var Name="Mugetsu"
func Add(a,b int) int{
return a+b
}
func init(){
fmt.Println("init util package")
}
(Space3,b.go)
package maths
import "fmt"
func sub (a,b int) int{
return a-b
}
func init(){
fmt.Println("init maths package")
}
(Space3,c.go)
package maths
func Add(a,b,c int) int{
return a+sub(b,c)
}
go run Test_main.go
/*
init util package
init maths package
Mugetsu
7
0
"hello"
*/
import里用工作区相对路径,大写函数为可以跨路径调用,小写函数不能调用
json序列化
在 Go 语言中,可以使用 encoding/json
包来实现 JSON 序列化和反序列化。下面是一个示例代码,同时加上了详细的注释说明:
package main
import (
"encoding/json"
"fmt"
)
type Person struct { // 定义一个结构体类型
Name string `json:"name"` // 指定结构体字段对应的 JSON 名称
Age int `json:"age"`
}
func main() {
// JSON 序列化
p := Person{Name: "张三", Age: 18} // 创建一个 Person 类型的变量
b, err := json.Marshal(p) // 将 Person 类型的变量 p 转换为 JSON 字节切片
if err != nil { // 如果转换失败,输出错误信息并退出程序
fmt.Println("JSON 序列化失败:", err)
return
}
fmt.Println("JSON 序列化结果:", string(b)) // 如果转换成功,输出 JSON 字符串
// JSON 反序列化
var p1 Person // 定义一个 Person 类型的变量
err = json.Unmarshal(b, &p1) // 将 JSON 字节切片 b 转换为 Person 类型的变量 p1
if err != nil { // 如果转换失败,输出错误信息并退出程序
fmt.Println("JSON 反序列化失败:", err)
return
}
fmt.Printf("JSON 反序列化结果:name=%s, age=%d\n", p1.Name, p1.Age) // 如果转换成功,输出反序列化结果
}
在这个示例中,我们定义了一个 Person
结构体类型,其中包含 Name
和 Age
两个字段。我们使用 json
标签来指定结构体字段的 JSON 名称,这样在序列化和反序列化时就可以按照指定的名称进行转换。
接下来,我们使用 json.Marshal
函数将 Person
类型的变量 p
转换为 JSON 字节切片。如果转换失败,输出错误信息并退出程序。如果转换成功,输出 JSON 字符串。
然后,我们使用 json.Unmarshal
函数将 JSON 字节切片 b
转换为 Person
类型的变量 p1
。如果转换失败,输出错误信息并退出程序。如果转换成功,输出反序列化结果。
需要注意的是,JSON 序列化和反序列化时,结构体的字段必须是可导出的(即首字母大写),否则无法进行转换。
接口(Interfaces)
Go语言中的接口(interface)是一种类型,它定义了一组方法的集合。实现这些方法的任何类型都可以被称为这个接口的实现类型。接口的定义和使用能够大大提高代码的灵活性和可复用性。
定义接口
在Go语言中,定义接口非常简单,只需要使用interface关键字即可。例如,我们可以定义一个名为Animal的接口,它包含两个方法:Eat()和Sleep()。
type Animal interface {
Eat()
Sleep()
}
在这个例子中,我们定义了一个名为Animal的接口,它包含了两个方法:Eat()和Sleep()。任何实现了这两个方法的类型都可以被称为Animal接口的实现类型。
接口的实现
在Go语言中,要实现一个接口,只需要实现这个接口中定义的所有方法即可。例如,我们可以定义一个名为Poodle的类型,它实现了Animal接口中的两个方法:Eat()和Sleep()。
type Poodle struct {
Name string
Age int
}
func (p *Poodle) Eat() {
fmt.Printf("%s is eating.\n", p.Name)
}
func (p *Poodle) Sleep() {
fmt.Printf("%s is sleeping.\n", p.Name)
}
在这个例子中,我们定义了一个名为Poodle的类型,它包含了两个字段:Name和Age。同时,我们实现了Animal接口中的两个方法:Eat()和Sleep()。在Eat()方法中,我们输出了一条狗正在吃的信息;在Sleep()方法中,我们输出了一条狗正在睡觉的信息。
使用接口
在Go语言中,使用接口非常简单,只需要将实现了接口的类型赋值给接口变量即可。例如,我们可以创建一个名为animal的Animal接口变量,并将一个Poodle类型的值赋值给它。
func main() {
var animal Animal
poodle := &Poodle{Name: "Fido", Age: 2}
animal = poodle
animal.Eat()
animal.Sleep()
}
在这个例子中,我们首先创建了一个名为animal的Animal接口变量。然后,我们创建了一个Poodle类型的值,并将它赋值给animal。最后,我们在animal变量上调用了接口中定义的两个方法:Eat()和Sleep()。这两个方法实际上是Poodle类型中定义的方法,但是由于Poodle类型实现了Animal接口,因此我们可以将它赋值给Animal接口变量,并在Animal接口上调用这两个方法。
我理解的例子
(网上给的例子我实在看不懂,我只能通过我学过的东西来定义)
这里假设我要用iostat命令去查看磁盘占用率。
Device tps kB_read/s kB_wrtn/s kB_dscd/s %util
sda 0.10 0.00 3.87 0.00 0.00
sdb 0.00 0.00 0.00 0.00 0.00
在shell里面,使用的命令是
iostat -d -x -k | grep 'sda' | awk '{print $1,$NF}'
用-d和-x显示,grep过滤到sda,再awk格式化文本输出Device和util
用Go语言接口来体现就是如下
package main
import (
"fmt"
"os/exec"
"strings"
)
type iostat interface {
GetDevice() string
GetUtil() float64
}
type script struct {
name string
util float64
}
func (s *script) GetDevice() string {
return s.name
}
func (s *script) GetUtil() float64 {
return s.util
}
func main() {
// 执行 iostat -d -x -k | grep 'sda' | awk '{print $1,$NF}' 命令
cmd := exec.Command("sh", "-c", "iostat -d -x -k | grep 'sda' | awk '{print $1,$NF}'")
//输出的结果为sda 0.00
output, err := cmd.Output()
if err != nil {
fmt.Println("Error:", err)
return
}
// 解析 awk 命令的输出
fields := strings.Fields(string(output)) //用strings.Fields()函数切片输出结果
if len(fields) != 2 {
fmt.Println("Error: unexpected output format")
return
}
s := &script{name: fields[0]}
fmt.Sscanf(fields[1], "%f", &s.util) //占用率为浮点型
// 输出结果
fmt.Println(s.GetDevice())
fmt.Println(s.GetUtil())
}
func Fields(str string) []string
str子字符串的切片或者如果str仅包含空格,则返回空切片。
fmt.Sscanf(fields[1], "%f", &s.util)
占用率是浮点型,不能用fmt.Printf。
我的理解
将一个输出结果,用结构体(script)连接进入接口(iostat)将里面的信息提取出来(Device和%util),然后输出结果。
(用生活化的例子来说。你买了一杯珍珠奶茶,珍珠奶茶里面有珍珠(信息,Device和%util),奶茶顶上有塑封的纸(接口,iostat)。你通过吸管(结构体,script)插进你的纸,然后把珍珠吸进你的口腔里,然后吃掉(输出结果)。)
协程(Goroutine)
上下文(Context)
Go 语言基础之 Context 详解 | zhihu | 程序员祝融
Go语言中的Context(上下文)是一个非常重要的特性,它在Go 1.7版本中引入,主要用于在goroutine之间传递各种信息,如截止日期、取消信号、超时时间等。Context的核心在于它的接口定义,允许开发者通过特定的方法来创建和操作Context,从而在多个Goroutine之间进行有效的通信。
基本用法
在 Go 语言中,Context 被定义为一个接口类型,它包含了三个方法:
# go version 1.18.10
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- Deadline() 方法用于获取 Context 的截止时间;
- Done() 方法用于返回一个只读的 channel,用于通知当前 Context 是否已经被取消;
- Err() 方法用于获取 Context 取消的原因;
- Value() 方法用于获取 Context 中保存的键值对数据。
func users(ctx context.Context, request *Request) {
// ... code
}
deadline, ok := ctx.Deadline()
if ok && deadline.Before(time.Now()) {
// 超时
return
}