认识 GO 语言
创建工程目录:
-
在 Linux 终端输入
echo $GOPATH
,即可查看到工程的目录:
-
创建
GO
工程文件夹:shellmkdir -p $GOPATH/bin $GOPATH/pkg $GOPATH/src # 如果权限不够,需要加上 sudo sudo mkdir -p $GOPATH/bin $GOPATH/pkg $GOPATH/src
工程文件的各个文件夹解释:
bin
: 存放编译成功后的可执行文件。pkg
: 存放自定义的第三方库。src
: 存放工程项目的源码地址。
-
在
src
目录下创建自己的项目:shellmkdir GolangStudy
-
在
GolangStudy
中创建程序:shellmkdir firstGolang
-
进入
firstGolang
文件夹中,输入code .
即可使用VSCode打开该文件(前提是已经安装有 VSCode),语法:code + 路径即可使用 VSCode 打开指定的路径。shellcode /path1/path2/...
第一个程序
1. 实例:
go
package main // 当前程序的包名,一个项目只能有一个 main 包(主包)
// 倒入格式化输出包
import "fmt"
// 倒入time包
import "time"
// 导入多个包
// import (
// "fmt"
// "time"
// )
// main 函数
func main(){ // 函数的左花括号必须与函数名同行
fmt.Println("Hello Go!")
time.Sleep(1 * time.Second)
}
// 终端命令:
// 编译并运行程序: go run 包名.go
// 分布运行:1. go build 包名.go => 生成编译后的"包名"文件; 2. 直接输入"./包名" 可以直接运行。
// 注意:可以加分号也可以不加分号,建议不加分号
2. 导入包的两种方式:
-
一个包一个包导入:
goimport "fmt" import "time"
-
一次导入多个包:
goimport ( "fmt" "time" )
3. 终端程序的两种运行方式:
go run 包名.go
,可以自动编译并运行文件,但是不会生成编译文件。go build 包名.go
,可以生成编译文件,并且编译文件的名称与包名
一致,但是没有后缀,然后在终端输入./包名
即可运行程序。
注意:
- 对于 Go 语言,每一条语句都可以加分号也可以不加分号,建议不加分号。
- 函数名后面的左花括号不能单独占用一行,只能与函数名在同一行,否则会报错。
- 声明
package main
,只能是在有main
函数下,是工程的入口,且一个工程只能有一个main
包。
声明变量的方式:
1. 声明单个变量:
-
方式一 :
var 变量名 变量类型
,没有赋初始值,默认是0govar a int
-
方式二 :
var 变量名 变量类型 = 值
,直接赋初始值govar b int = 100
-
方式三 :
var 变量名 = 值
,不直接声明变量类型,而是通过值去推测变量类型govar c = 100
-
方式四(常用) :
变量名 := 值
,省略var
和变量类型
,直接使用:=
对变量赋值gog := 3.14
注意:
- 声明全局变量 时,可以使用方式一、方式二、方式三;但是不能使用方式四。
- 方式四只能用在局部变量,只能用在函数中。
2. 声明多个变量:
-
相同类型 :
var 变量名1, 变量名2, ... 变量类型 = 值1, 值2, ...
govar xx, yy int = 100, 200
-
不同类型 :
var 变量名1, 变量名2, ... = 值1, 值2, ...
govar kk, ll = 100, "Ace"
-
多行声明变量:
govar ( vv int = 50 jj bool = true )
3. 实例:
go
package main
/*
四种变量的声明方式
*/
import (
"fmt"
)
// 声明全局变量,方法一、方法二、方法三都是可以的
var gA int = 100
var gB int = 200
// 方法四不能用来声明全局变量,只能声明局部变量。
// := 只能用在函数体内。
// gC := 300 // 错误
func main(){
// 方法一:声明一个变量, 默认的值是0
var a int
fmt.Println("a =", a)
// 方式二: 生命一个变量,并初始化值
var b int = 100
fmt.Println("b =", b)
fmt.Printf("type of b = %T\n", b)
var st_const string = "aabb"
fmt.Printf("st_const = %s, type of st_const = %T\n", st_const, st_const) // 格式化字符串,%s 表示输出字符串,%T 表示输出变量类型
// 方式三: 在初始化的时候,可以省去数据类型,通过值自动匹配当前的变量的数据类型
var c = 100
fmt.Println("c =", c)
fmt.Printf("type of c = %T\n", c)
var st_auto string = "aabb"
fmt.Printf("st_auto = %s, type of st_auto = %T\n", st_auto, st_auto)
// 方式四:(最常用的方法),省去 var 关键字,直接自动匹配
e := 100
fmt.Println("e =", e)
fmt.Printf("type of e = %T\n", e)
f := "ffff"
fmt.Println("f =", f)
fmt.Printf("type of f = %T\n", f)
g := 3.14
fmt.Println("g =", g)
fmt.Printf("type of g = %T\n", g)
// 声明多个变量
// 相同类型
var xx, yy int = 100, 200
fmt.Println("xx =", xx, "yy =", yy)
// 不同类型
var kk, ll = 100, "Ace"
fmt.Println("kk =", kk, "ll =", ll)
// 多行的多变量声明
var (
vv int = 50
jj bool = true
)
fmt.Println("vv =", vv, "jj =", jj)
}
常量类型
1. 常量的声明方式:
Go 语言中,常量的声明,使用关键字const
。
-
定义一个简单的常量:
goconst MINIMUM int = 1
-
使用
const
关键字来定义枚举:goconst ( BEIJING = 0 SHAGNHAI = 1 SHENZHEN = 2 )
-
对于有规律且需要大量赋值的枚举,可以使用
iota
关键字,iota
关键字是按行递增的(增幅为 1),iota
的默认初值为 0:goconst ( // 可以在const()添加一个关键字iota,每行的iota都会累加1,第一行的iota的默认值是0, iota 只能在const内进行累加 BEIJING = iota // iota = 0, => BEIJING = 0 SHAGNHAI // iota = 1, => SHAGNHAI = 1 SHENZHEN // iota = 2, => SHENZHEN = 2 )
-
可以使用复杂的规则进行对
const
的枚举类型进行赋值,以达到目的:goconst ( a, b = iota + 1, iota + 2 // iota = 0, a = iota + 1, b = iota + 2 => a = 1, b = 2 c, d // iota = 1, c = iota + 1, d = iota + 2 => c = 2, d = 3 e, f // iota = 2, e = 3, f = 4 g, h = iota * 2, iota * 3 // iota = 3, g = iota * 2, h = iota * 3 => g = 6, h = 9 i, k // iota = 4, i = 8, k = 12 )
注意:
iota
关键字只能用在const
的枚举类型中,不能用在其他地方。iota
初值为0,并且按行递增,增幅为 1
2. 实例:
go
package main
import "fmt"
// const 来定义枚举
// const (
// BEIJING = 0
// SHAGNHAI = 1
// SHENZHEN = 2
// )
// const (
// // 可以在const()添加一个关键字iota,每行的iota都会累加1,第一行的iota的默认值是0, iota 只能在const内进行累加
// BEIJING = iota // iota = 0
// SHAGNHAI // iota = 1
// SHENZHEN // iota = 2
// )
const (
// 可以在const()添加一个关键字iota,每行的iota都会累加1,第一行的iota的默认值是0
BEIJING = 10 * iota // iota = 0
SHAGNHAI // iota = 1
SHENZHEN // iota = 2
)
const (
a, b = iota + 1, iota + 2 // iota = 0, a = iota + 1, b = iota + 2 => a = 1, b = 2
c, d // iota = 1, c = iota + 1, d = iota + 2 => c = 2, d = 3
e, f // iota = 2, e = 3, f = 4
g, h = iota * 2, iota * 3 // iota = 3, g = iota * 2, h = iota * 3 => g = 6, h = 9
i, k // iota = 4, i = 8, k = 12
)
func main() {
// 常量
const length int = 10
fmt.Println("length =", length)
fmt.Println("BEIJING =", BEIJING)
fmt.Println("SHAGNHAI =", SHAGNHAI)
fmt.Println("SHENZHEN =", SHENZHEN)
fmt.Println("a =", a, "b =", b)
fmt.Println("c =", c, "d =", d)
fmt.Println("e =", e, "f =", f)
fmt.Println("g =", g, "h =", h)
fmt.Println("i =", i, "k =", k)
}
函数
1. 定义函数的几种方式:
用关键字 func
来定义函数,参数的书写方式为 参数名 参数类型
,并且可以在函数名后写返回值类型,如果没有返回值可以不写,因此,其语法可以概括如下:
go
func 函数名 (参数名1 参数类型 参数名2 参数类型, [参数名n 参数类型, ...]) 返回值类型 {
...
return ...
}
-
定义一个简单的函数,只有一个返回值:
gofunc foo1(a string, b int) int { fmt.Println("a =", a) fmt.Println("b =", b) c := 100 return c }
-
有多个返回值,返回值只声明返回值类型,但是没有返回值名称(匿名返回值):
gofunc foo2(a string, b int) (int , int) { fmt.Println("a =", a) fmt.Println("b =", b) return 666, 777 }
-
有多个返回值,返回值有返回值名称,有返回值名称的函数,在返回的时候直接使用
return
即可,并且初始化值为0(int 类型),在对其赋值的时候,直接使用返回值名赋值即可:gofunc foo3(a string, b int)(r1 int, r2 int) { // r1, r2初始化赋值为0 fmt.Println("---- foo3 ----") fmt.Println("a =", a) fmt.Println("b =", b) // 给有名称的返回值变量赋值 r1 = 1000 r2 = 2000 // 直接进行return即可 return }
-
多个返回值有名函数的另一种写法,对于多个返回值且类型相同,可以直接声明返回值名称,并在最后书写返回值类型即可:
gofunc foo4(a string, b int)(r1, r2 int){ fmt.Println("---- foo4 ----") fmt.Println("a =", a) fmt.Println("b =", b) r1 = 1000 r2 = 2000 return }
2. 实例:
go
package main
import "fmt"
// 数据类型和返回值类型都写在后面,如果没有返回值可以不写
func foo1(a string, b int) int {
fmt.Println("a =", a)
fmt.Println("b =", b)
c := 100
return c
}
// 多个返回值(都是匿名的)
func foo2(a string, b int) (int , int) {
fmt.Println("a =", a)
fmt.Println("b =", b)
return 666, 777
}
// 返回多个值(有返回值名称)
func foo3(a string, b int)(r1 int, r2 int) { // r1, r2初始化赋值为0
fmt.Println("---- foo3 ----")
fmt.Println("a =", a)
fmt.Println("b =", b)
// 给有名称的返回值变量赋值
r1 = 1000
r2 = 2000
// 直接进行return即可
return
}
func foo4(a string, b int)(r1, r2 int){
fmt.Println("---- foo4 ----")
fmt.Println("a =", a)
fmt.Println("b =", b)
// 给有名称的返回值变量赋值
r1 = 1000
r2 = 2000
// 直接进行return即可
return
}
func main() {
c := foo1("abc", 555)
fmt.Println("c =", c)
ret1, ret2 := foo2("haha", 99)
fmt.Println("ret1 =", ret1)
fmt.Println("ret2 =", ret2)
ret1, ret2 = foo3("foo3", 333)
fmt.Println("ret1 =", ret1, "ret2 =", ret2)
ret1, ret2 = foo4("foo4", 444)
fmt.Println("ret1 =", ret1, "ret2 =", ret2)
}
import导包与init方法
1. 程序的流程图:
可以看到
init()
方法的执行时机在main()
执行时机前,因此可以在main()
前执行一些初始化的操作。
2. 实例:
项目结构:
├── lib1
│ └── lib1.go
├── lib2
│ └── lib2.go
└── main.go
main.go
go
package main
import (
"GolangStudy/init/lib1"
"GolangStudy/init/lib2"
)
func main() {
lib1.Lib1Test()
lib2.Lib2Test()
}
lib1.go
go
package lib1
import "fmt"
// 当前lib1包提供的API, 函数名首字母大写的话,说明该函数是一个对外开放的函数,否则只能在的当前的包内调用
func Lib1Test() {
fmt.Println("lib1Test()...")
}
func init() {
fmt.Println("lib1. init()...")
}
lib2.go
go
package lib2
import "fmt"
// 当前lib2包提供的API
func Lib2Test() {
fmt.Println("lib2Test()...")
}
func init() {
fmt.Println("lib2. init()...")
}
注意:对于各个包中的函数来讲,函数名首字母大写,说明该函数是一个对外开放的函数,否则只能在的当前的包内调用。
3. 导自定义包失败问题:
参考链接 :go引入自建包名报错 package XXX is not in std_package is not in std-CSDN博客
对于 go1.23.2
版本(之前或之后的版本的可能也有这种问题),导入自定义的包会失败,错误如下:
此时需要查看是否关闭GO111MODULE
,使用命令 go env
进行查看,如下是没有关闭的状态,此时不会去$GOPATH
路径下寻找包,$GOPATH
路径是我们写程序的路径:
此时需要关闭GO111MODULE
,命令:
shell
go env -w GO111MODULE=off
再次使用命令 go env
,查看结果和下图一致,说明修改正确:
此时即可运行程序了。
注意 :导包的时候只会从
$GOPATH
路径下的src
目录下进行寻找,因此在导包的时候,路径要从src
的下一级开始写。
匿名导包与别名导包
Golang 语言是一门严谨的语言,如:对于声明的变量但是下文没有使用,这会报错,并且无法进行编译;对于导包,但是下文中没有使用,这也会报错,并且无法编译。
对于导包但是下文没有使用的问题,可以使用匿名导包的方式进行解决。
匿名导包:
-
匿名导包的方式 :
import _ "包路径"
,注意如果使用这种方式的话,在代码的下文中,就不能再使用改包中的函数了,但是还会 调用改包的init()
方法,如:goimport _ "GolangStudy/init/lib1" // 或 import ( _ "GolangStudy/init/lib1" "GolangStudy/init/lib2" )
-
别名导包 :
import ex "包路径"
,这里使用ex
来作为包的别名,下文中可以直接使用ex
直接调用包中的函数,如:goimport ( ex "GolangStudy/init/lib1" "GolangStudy/init/lib2" ) func main() { ex.Lib1Test() lib2.Lib2Test() }
-
直接导入 :
import . "包路径"
,这里使用.
来进行导包,相当于直接将包中的代码与该程序中的代码进行了合并,可直接使用导入包的函数名称,如:goimport ( . "GolangStudy/init/lib1" "GolangStudy/init/lib2" ) func main() { Lib1Test() // 注意这里的函数是 lib1 包中的函数 lib2.Lib2Test() }
注意 :直接导入的方式不要轻易使用,因为这种方式很可能会导致命名冲突问题,即在一个程序中导入多个包时,不同的包之间的变量或函数会出现相同的名字,从而产生命名冲突。
指针
Go 语言中也存在指针的概念,与C语言和C++中的指针相似,如果学习过C或C++那么你将会很快的上手Go语言中的指针。
指针的使用方式:
-
取地址 :语法
& 变量名
,使用这种方式可以直接取到变量的物理内存地址,如:govar a int = 10 // 取 a 的地址 fmt.Println("a 的物理内存地址为:", &a)
-
指针 :指针的作用就是指向变量的物理内存地址,语法
var 指针名 *变量的数据类型 = &变量名
或var 指针名 = &变量名
或指针名 := &变量名
,对于指针,对指针使用* 指针名
表示得到指针指向的变量的值,例子:govar a = 10 // 使用指针指向变量a的地址 var pointer0_a *int = &a // 注意:如果声明数据类型,则数据类型要与指向变量的数据类型相同 var pointer1_a = &a pointer2_a := &a // 得到指针指向变量的值 fmt.Println("value of pointer2_a =", *pointer2_a)
实例:交换变量的值:
go
package main
import "fmt"
func swap(temp_a *int, temp_b *int) bool{
fmt.Println("temp_a =", *temp_a, "temp_b =", *temp_b)
fmt.Println("temp_a 的值是a的物理,temp_b的值是b的物理地址 : temp_a =", temp_a, "temp_b =", temp_b)
// 进行交换
swap := *temp_a
*temp_a = *temp_b
*temp_b = swap
return true
}
func main() {
var a, b int = 10, 5
fmt.Println("a =", a, "b =", b)
if swap(&a, &b) {
fmt.Println("交换成功!!!: a =", a, "b =", b)
} else {
fmt.Println("很遗憾,交换失败!!!")
}
// 输出 a的地址与 b的地质
fmt.Println("输出 a的地址与b的地址: &a =", &a, "&b =", &b)
}
defer 关键字
defer 关键字用于在函数的生命周期结束的前调用一次。
使用方式 :defer 函数名()
,这样可以在函数生命周期将要结束的时候 调用一次defer
之后函数,通常用于关闭操作,如:在读取或写入完文件的时候关闭文件流。
注意 :对于多个defer
的函数,其执行顺序是按照栈(Stack)的方式执行的,即先入后出的方式执行,其形式流程图如下:
实例:输出的顺序
go
package main
import (
"fmt"
)
func main() {
defer fmt.Println("first - 1")
defer fmt.Println("second - 2")
defer fmt.Println("third - 3")
fmt.Println("main")
fmt.Println("main - end")
}
实例:函数的执行顺序
go
package main
import (
"fmt"
)
func foo1(){
fmt.Println("I am foo1.")
}
func foo2(){
fmt.Println("I am foo2")
}
func foo3(){
fmt.Println("I am foo3")
}
func main() {
defer foo1()
defer foo2()
foo3()
}
实例:return 与 defer的执行顺序
go
package main
import (
"fmt"
)
func returnFunction() string {
fmt.Println("return implement!!!")
return "SUCCEED"
}
func deferFunction() string{
fmt.Println("defer implement!!!")
return "SUCCEDD"
}
func compareFunction() string {
defer deferFunction()
return returnFunction()
}
func main() {
compareFunction()
}
数组与动态数组
数组:是一种内置的数据类型,具有固定的大小,一旦声明,其长度就不能改变。数组在内存中是连续存储的。
**切片(动态数组)**是一种更加灵活的数据结构,它引用了一个数组的一段区域。切片没有固定的长度,你可以创建一个新的切片来引用原数组的不同部分,或者扩展原切片的长度。切片在内存中不一定是连续存储的,它们是对底层数组的一种抽象。
普通数组的声明方式:
-
使用
var
关键字进行声明,语法:var 数组名称 [数组长度] 数组元素类型
,如:govar myArray [10] int
-
使用
:=
进行声明,语法:数组名称 := [数组长度] 数组元素类型 {元素1, 元素2, 元素3,...}
,注意使用这种方式必须要给数组赋初值,并且这种方式的声明的数组只能作用在函数体内,不能用作全局变量,如:gomyArray := [10] int {1, 2 ,3, 4, 5}
动态数组的声明方式:
在Go语言中,切片这种数据结构就是所谓的动态数组,可以使用切片来表示动态数组。
-
使用
var
关键字进行声明,语法:var 动态数组名称 [] 动态数组元素类型 = [] 动态数组元素类型 {元素1, 元素2, 元素3,...}
,声明时必须要给元素赋初值,如:govar mySlice []int = []int{1, 2, 3}
-
使用
:=
进行声明,语法:动态数组名称 := [] 动态数组元素类型{元素1, 元素2, 元素3,...}
,声明时必须要给元素赋初值,如:gomySlice := []int{1, 2, 3}
注意:
- 普通的数组对函数进行传参的时候,传递的实际上是数组的拷贝,对参数数组进行操作,并不会影响真实的数组。
- 使用切片(动态数组)的方式进行传参,传递的实际上是数组的引用,对参数数组进行操作,实际上是对真实的数组进行操作。
遍历数组的两种方式:
-
使用索引进行遍历,如:
gofor i := 0; i < len(myArray1); i++ { fmt.Println(myArray1[i]) }
-
使用
for
循环结合range
关键字进行遍历,这样不仅会遍历到数组中每一个元素的值,还可以遍历到每一个元素的索引,如:gofor index, value := range myArray2 { fmt.Println("index =", index, "value =", value) }
实例:固定长度数组
go
package main
import (
"fmt"
)
// 数组传参
func printArray(myArray [4]int){
for index, value := range myArray {
fmt.Println("index =", index, "value =", value)
}
}
func main() {
// 声明一个固定长度的数组
var myArray1 [10] int
myArray2 := [10] int {1, 2, 3, 4}
myArray3 := [4] int {11, 22, 33, 44}
// 遍历数组
for i := 0; i < len(myArray1); i++ {
fmt.Println(myArray1[i])
}
for index, value := range myArray2 {
fmt.Println("index =", index, "value =", value)
}
// 查看数组的数据类型
fmt.Printf("myArray1 types = %T\n", myArray1)
fmt.Printf("myArray2 types = %T\n", myArray2)
fmt.Printf("myArray3 types = %T\n", myArray3)
printArray(myArray3)
}
实例:动态数组
go
package main
import (
"fmt"
)
func printArray(myArray []int) { //
// _ 表示匿名变量
for _, value := range myArray {
fmt.Println("value =", value)
}
myArray[0] = 100
}
func main() {
// 动态数组, 切片 slice,传参的时候相当于是一个引用传递,相当于传递的是一个指针,动态数组类一个指针,指向内存的一片地址
myArray := []int {1, 2, 3, 4}
fmt.Printf("myArray types = %T\n", myArray)
printArray(myArray)
fmt.Println(" ----- ")
for _, value := range myArray {
fmt.Println("value =", value)
}
}
slice(切片)的声明方式
四种声明方式:
slice(切片)的声明方式一共有四种,其声明的方法如下:
-
使用
:=
关键字,声明一个切片,并且初始化值,语法:切片名称 := [] 切片数据类型 {元素1, 元素2, 元素3,...}
如:goslice1 := []int {1, 2, 3}
-
使用
var
关键字进行声明(注意:这种声明方式没有给切片分配内存空间,因此不能直接对其进行赋值操作,需要分配内存空间之后才能进行赋值操作),语法:var 切片名称 [] 切片数据类型
,分配内存空间需要使用方法make(数据类型, 长度大小)
,分配空间后数组元素的初始的值为0
,如:govar slice2 []int // 此时只是声明了,并没有实际的内存空间,因此不能进行赋值操作。 // 给slice2开辟空间 slice2 = make([]int, 3) // 开辟3个空间,默认值为0
-
使用
var
关键字声明并且直接分配内存空间,语法:var 切片名称 [] 切片数据类型 = make(数据类型, 长度大小)
,如:govar slice3 []int = make([]int , 3)
-
使用
:=
进行声明,并且直接分配内存空间,语法:切片名称 := make(数据类型, 长度大小)
,如:goslice4 := make([]int , 3)
判断数组为空的方式(即没有内存空间) :使用关键字
nil
,例子如下:
goif slice4 == nil { fmt.Println("slice4 是一个空切片") } else { fmt.Println("slice4 是有空间的") }
实例:
go
package main
import (
"fmt"
)
func main() {
// 四种切片的方式
// 第一种方式, 声明是一个切片,并且初始化值为1, 2, 3, 长度为3
slice1 := []int {1, 2, 3}
fmt.Printf("len = %d, slice1 = %v\n", len(slice1), slice1) // %v 表示打印出详细的信息
// 第二种方式:声明slice2是一个切片,但是并没有给slice分配空间,在没有空间的时候是不能够赋值的
var slice2 []int
// 给slice2开辟空间
slice2 = make([]int, 3) // 开辟3个空间,默认值为0
slice2[0] = 100
fmt.Printf("len = %d, slice2 = %v\n", len(slice2), slice2)
// 第三种方式:声明一个slice3,并且分配3个空间
var slice3 []int = make([]int , 3)
fmt.Printf("len = %d, slice3 = %v\n", len(slice3), slice3)
// 第四种方式:声明一个slice4,并且分配3个空间,使用 := 方式进行推导
slice4 := make([]int , 3)
fmt.Printf("len = %d, slice4 = %v\n", len(slice4), slice4)
// 判断 slice是否为空
if slice4 == nil {
fmt.Println("slice4 是一个空切片")
} else {
fmt.Println("slice4 是有空间的")
}
}
slice的追加与切片
slice的追加方式:
使用方法append(数组,元素)
,如:
go
numbers = append(numbers, 1) // 这里的number是一个slice(切片)
slice的属性:
slice(切片)的属性有长度(length)和容量(capacity)。
- 长度(length):切片的长度是指切片中包含的元素数量。你可以使用内置的
len()
函数来获取切片的长度。例如,如果你有一个切片s
,那么len(s)
将返回s
中的元素数量 - 容量(capacity):切片的容量是指切片可以增长到的最大长度,而不需要重新分配底层数组。容量总是大于或等于长度。你可以使用内置的
cap()
函数来获取切片的容量。例如,如果你有一个切片s
,那么cap(s)
将返回s
的容量。
在创建 slice 的时候,使用make()
方法不仅可以分配切片长度,还可以分配容量大小,语法make(数据类型, 长度大小, 容量大小)
,如:
go
var numbers = make([]int, 3, 5) // 给 numbers 切片分配3个长度大小,并且分配的总容量大小为5
注意 :由于slice可以充当动态数组 ,因此如果不断的向slice中添加元素,直到超过了分配总容量大小,此时go语言底层会再次分配给该切片一个初始容量大小,如初始的时候使用
make
分配给slice容量大小为5,当不断向slice中添加元素的时候,超过了5,即切片中需要添加第6个元素,此时go语言底层会在次分配给该slice 5个容量大小,总容量变为了10,如果添加元素超过了10,那么会再次分配5个容量大小,如此往复。
slice的截取方式:
slice 的截取方式与 Python 的切片方式一样,遵循左闭右开的原则,截取的结果实际上与原来的动态数组所指向的内存空间一致,如:
go
s := []int{1, 2, 3}
// 截取索引0, 1, 左闭右开,与Python类似,但是截取的结果实际上是同一片内存空间,及s1和s指向同一片内存空间
s1 := s[0: 2]
slice 相当于一个指针,指向内存的一片区域,截取相当于在该内存区域添加一个新的指针,并且规定新的指针的首和尾,对截取进行操作,原来的slice也会进行相应的改变。
深度拷贝:
深度拷贝使用方法copy(待拷贝的变量,新的变量)
,这样会重新开辟一个内存空间然后将值复制到该内存空间中,注意新的变量
的内存空间一定要大于等于待拷贝的变量的
内存空间,否则会报错,如:
go
s2 := make([]int, 3) // s2 = [0, 0, 0]
// 将s中的值拷贝到s2中
copy(s2, s)
实例:slice的追加
go
package main
import (
"fmt"
)
func main() {
var numbers = make([]int, 3, 5) // 长度为3, 容量为5, 容量表示切片内存的总量是多少,数组大小不能越界
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 追加元素
numbers = append(numbers, 1)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
numbers = append(numbers, 2)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 此时超过了切片的最大容量,go 语言会在底层开辟新的空间,新的空间的大小与原来空间大小相等
numbers = append(numbers, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 如果不去声明,则设置的容量与长度一致
var numbers2 = make([]int, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers2), cap(numbers2), numbers2)
numbers2 = append(numbers2, 1)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers2), cap(numbers2), numbers2)
}
在
fmt.Printf()
中使用%v
表示能够输出任何数据类型的信息。
实例:slice的切片
go
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3} // len = 3, cap = 3
// 截取索引0, 1, 左闭右开,与Python类似,但是截取的结果实际上是同一片内存空间,及s1和s指向同一片内存空间
s1 := s[0: 2]
s1[0] = 100
fmt.Println(s1)
fmt.Println(s)
// 深度拷贝, copy
s2 := make([]int, 3) // s2 = [0, 0, 0]
// 将s中的值拷贝到s2中
copy(s2, s)
fmt.Println(s2)
s2[2] = -1 // 此时不会改变s中的值,因为是深拷贝
fmt.Println("s2 =", s2)
fmt.Println("s =", s)
}
练习题:斐波那契数列
go
package main
import (
"fmt"
)
// 实现斐波那契数列 ------ 动态规划
func fb(number int) (int, [] int){
temp_array := make([]int, number + 1, number + 1)
if number <= 0 {
return 0, temp_array
} else if number == 1 {
temp_array[number] = 1
return 1, temp_array
} else if number == 2 {
temp_array[1] = 1
temp_array[2] = 1
return temp_array[number], temp_array
}
temp_array[1] = 1
temp_array[2] = 1
for i := 3; i <= number; i++{
temp_array[i] = temp_array[i - 1] + temp_array[i - 2]
}
return temp_array[number], temp_array
}
// 遍历切片
func printSlice(slice []int) {
fmt.Println("====== 《遍历切片》 ======")
fmt.Println("- 切片的总长度为: ", len(slice))
for _, value := range slice {
fmt.Printf("%d, ", value)
}
fmt.Println()
}
func main() {
var input int // 用于存储控制台中的值
fmt.Printf("- 请输入要计算的第几个斐波那契数:")
fmt.Scanln(&input)
fb_value, slice := fb(input)
fmt.Println("- 斐波那契数为:", fb_value)
fmt.Println("- 该斐波那契数前的所有值为:", slice)
printSlice(slice)
}
map 的声明方式
map 等同于 Python 中的字典格式,其底层是基于哈希表实现的。在Go语言中,map
的底层实现也使用了哈希表,但它使用了一个不同的策略来存储键值对。
三种声明方式:
在Golang语言中,一共有三种声明 map
(又称:映射) 的方式。
-
使用
var
关键字进行声明,语法:var 映射名称 map[key数据类型] value数据类型
,注意,这种声明方式并没有给map
划分存储空间,所以需要进行分配内存空间后才能正常使用,使用map
前需要使用make
分配内存空间,和slice
一样,会动态开辟内存空间,如:govar myMap1 map[int] string myMap1 = make(map[int] string, 10)
-
使用
:=
方式进行声明,语法:映射名称 := make(map[key数据类型] value数据类型)
,注意,如果不确定要分配多少内存,可以省略make()
方法的第二个参数,如:gomyMap2 := make(map[string]string)
-
声明
map
的同时,给映射赋初值,注意这种方式,赋初值的每一个键值后都必须要有一个,
(逗号),包括最后一个键值对也必须要有一个逗号结尾,如:gomyMap3 := map[string]string { "one": "php", "two": "c++", "three": "python", // 注意每一个及键值对后面都需要有一个逗号 }
map 的使用方式:
map 的增、删、改、查操作。
-
添加数据 ,直接给
key
进行赋值即可,如:gocityMap["China"] = "Beijing" cityMap["Japan"] = "Tokyo" cityMap["USA"] = "NewYork"
-
遍历数据 :使用
for
和range
进行遍历,注意,这种遍历方式会返回映射的key
和value
,如:gofor key, value := range cityMap { fmt.Printf("key = %s, value = %s\n", key, value) }
-
删除数据 :使用
delete()
方法进行删除,语法delete(映射名称, key)
,如:godelete(cityMap, "China")
-
修改数据 :直接对映射中的
key
进行赋值即可,如:gocityMap["USA"] = "DC"
注意 :
map
的传参方式是引用传参,改变参数的值会导致实际的map
的值发生变化。如:
gofunc ChangeValue(cityMap map[string]string) { cityMap["England"] = "London" // 会改变实际的cityMap映射的 key = England 的值 }
实例:map的三种声明方式
go
package main
import (
"fmt"
)
// map 的三种声明方式
func main() {
// 第一种声明方式:key 是 int类型, value 是string类型
var myMap1 map[int] string
if myMap1 == nil {
fmt.Println("myMap1 是一个空map")
}
// 给 map 开辟内存空间,使用map前需要使用make分配内存空间,和slice一样,会动态开辟内存空间
myMap1 = make(map[int] string, 10)
myMap1[100] = "JAVA"
myMap1[200] = "PYTHON"
myMap1[300] = "C ++"
fmt.Println(myMap1)
// 第二种声明方式
myMap2 := make(map[string]string) // 如果不确定要分配多少内存,可以省略第二个参数
myMap2["one"] = "JAVA"
myMap2["two"] = "PYTHON"
myMap2["three"] = "C ++"
fmt.Println(myMap2)
// 第三种声明方式
myMap3 := map[string]string {
"one": "php",
"two": "c++",
"three": "python", // 注意每一个及键值对后面都需要有一个逗号
}
fmt.Println(myMap3)
}
实例:map的使用方法
go
package main
import "fmt"
// map 的使用方法
// 传参方法
func printMap(cityMap map[string]string) {
// cityMap 是一个引用传递
for key, value := range cityMap {
fmt.Printf("key = %s, value = %s\n", key, value)
}
}
func ChangeValue(cityMap map[string]string) {
cityMap["England"] = "London"
}
func main() {
cityMap := make(map[string]string)
// 添加
cityMap["China"] = "Beijing"
cityMap["Japan"] = "Tokyo"
cityMap["USA"] = "NewYork"
// 遍历
printMap(cityMap)
// 删除
delete(cityMap, "China")
// 修改
cityMap["USA"] = "DC"
ChangeValue(cityMap)
fmt.Println("=============")
// 遍历
for key, value := range cityMap {
fmt.Println("key =", key)
fmt.Println("value =", value)
}
}
结构体
Golang 语言中声明结构体使用struct
关键字进行声明.
结构体的声明方法:
-
换名 :可以使用
type
关键字对数据类型启别名,可以理解为声明一种新的数据类型,如:gotype myint int // 这样在程序中使用 myint 等价于使用 int
-
结构体 :结构体相当于自定义的数据类型,与面向对象语言中的类很相似,定义方法如下:
gotype Book struct { title string auth string }
结构体的使用方法
-
创建结构体:创建结构体的方法,与创建常规的变量类似,只不过将常见的数据类型转变为自定义结构体的数据类型,如:
govar book1 Book
-
修改内部属性 :更改结构体变量的内部属性,直接使用
.
的方法进行引用即可,如:gobook1.title = "Golang" book1.auth = "zhangsan"
-
结构体传参:结构体传参与常规的普通数据变量传参一致,可以直接使用结构体变量名称进行传参,这样传参的方式实际上传递的是结构体变量的副本,对副本进行修改不会影响实际的结构体变量数据,也可以使用引用传参的方式,引用传参的方式传递的是结构体变量的地址,具体例子如下:
go// 普通的传参方式 func changeBook(book Book) { // 传递一个book副本 book.auth = "666" } // 引用传参方式,传递的实际上是一个结构体变量的地址 func changeBook2(book *Book) { // 指针传递 book.auth = "777" } changeBook(book1) changeBook2(&book1)
实例:
go
package main
import "fmt"
// 结构体
// type 用于声明一种新的数据类型,是int的一个别名
type myint int
// 定义一个结构体
type Book struct {
title string
auth string
}
// 结构体传参
func changeBook(book Book) {
// 传递一个book副本
book.auth = "666"
}
func changeBook2(book *Book) {
// 指针传递
book.auth = "777"
}
func main(){
// var a myint = 10
// fmt.Printf("a = %d, type of a = %T\n", a, a)
var book1 Book
book1.title = "Golang"
book1.auth = "zhangsan"
fmt.Printf("%v\n", book1)
changeBook(book1)
fmt.Printf("%v\n", book1)
changeBook2(&book1)
fmt.Printf("%v\n", book1)
}
类
在 Golang 语言中,使用关键字struct
来定义一个类,类的方法是一个独立的函数,并不是写在类中的。
定义一个类:
使用关键字struct
来定义一个类,可以在类中定义属性,如:
go
type Hero struct {
// 属性名称首字母大写,表示共有属性;首字母小写,表示私有属性。
id int
Name string
Level int
}
定义类的方法:
定义一个类的方法与定义一个函数格式类似,其语法为:func (this 类名) 方法名(参数1, ...) 返回类型 {方法体}
,如:
go
func (this Hero) SetName(newName string) {
// this 调用的实际上是对象的副本(拷贝)
this.Name = newName
}
注意:这种类方法不会改变对象的属性的值,因为传入方法中的函数实际上对象的副本,如果需要改变对象的属性值,需要使用指针类型才能够真正意义上改变对象的属性值,如:
gofunc (this *Hero) GetName() string { fmt.Printf("Name =", this.Name) return this.Name }
实例:
go
package main
import "fmt"
// 声明一个类
// 类名首字母大写表示公有类,即可以被包外的程序访问,首字母小写表示私有的类,不能被包外的程序
type Hero struct {
// 属性名称首字母大写,表示共有属性;首字母小写,表示私有属性。
id int
Name string
Level int
}
// 创建类的方法
// 类方法名称大写,表示共有方法,小写表示私有的方法
func (this Hero) Show() {
fmt.Println("ID =", this.id)
fmt.Println("Name =", this.Name)
fmt.Println("Level =", this.Level)
}
/*
func (this Hero) GetName() string {
fmt.Printf("Name =", this.Name)
return this.Name
}
func (this Hero) SetName(newName string) {
// this 调用的实际上是对象的副本(拷贝)
this.Name = newName
}
*/
// 指针类型的类方法
func (this *Hero) GetName() string {
fmt.Printf("Name =", this.Name)
return this.Name
}
func (this *Hero) SetName(newName string) {
// 使用指针类型this来引用对象的地址,this参数指向的实际上是目标对象的内存地址
this.Name = newName
}
func main() {
// 创建类对象
hero := Hero{id: 1, Name: "HK", Level: 100}
hero.Show()
hero.SetName("DJ")
fmt.Println("=====================")
hero.Show()
}
封装
在 Golang 语言中,类的封装是通过其名称的首字母的大小写来表示的。
- 类的封装 :
- 首字母大写:表示类是一个共有类,可以被该类所在的包外的其他程序调用。
- 首字母小写:表示类是一个私有类,不可以被该类所在的包外的其他程序调用。
- 属性封装 :
- 首字母大写:表示该属性是一个共有属性,可以被该类所在的包外的其他程序调用。
- 首字母小写:表示该属性是一个私有属性,不可以被该类所在的包外的其他程序调用。
- 方法封装 :
- 首字母大写:表示类是一个共有类,可以被该类所在的包外的其他程序调用。
- 首字母小写:表示类是一个私有类,不可以被该类所在的包外的其他程序调用。
继承
Golang 语言中,直接在类中的第一行写需要继承的类名即可实现继承,如:
go
type Human struct {
name string
sex string
}
type SuperHuman struct {
Human // 直接声明需要继承的父类
level int // 子类的属性
}
如果需要从写父类的方法,直接给子类定义与父类方法名称相同的方法即可,如:
go
// 父类的 Eat 方法
func (this *Human) Eat() {
fmt.Println("Human Eating ...")
}
// 子类SuperHuman 继承父类Human,重写父类的Eat()方法
func (this *SuperHuman) Eat() {
fmt.Println("SuperHuman Eat ...")
}
实例:
go
package main
import "fmt"
type Human struct {
name string
sex string
}
func (this *Human) Eat() {
fmt.Println("Human Eating ...")
}
func (tihs *Human) Sleep() {
fmt.Println("Human Sleeping ...")
}
type SuperHuman struct {
Human // 直接声明需要继承的父类
level int // 子类的属性
}
// 子类重写父类的方法
func (this *SuperHuman) Eat() {
fmt.Println("SuperHuman Eat ...")
}
// 子类的新的方法
func (this *SuperHuman) Fly() {
fmt.Println("SuperHuman Fly ...")
}
func (this *SuperHuman) Print() {
fmt.Println("name =", this.name)
fmt.Println("sex =", this.sex)
fmt.Println("level =", this.level)
}
func main() {
soym := Human{name: "soyMilk", sex: "male"}
soym.Eat()
soym.Sleep()
fmt.Println("======================")
// 定一个子类(方式一)
// supSoym := SuperHuman{Human{"soyMilk", "male"}, 99}
// (方式二)
var supSoym SuperHuman
supSoym.name = "soyMilk"
supSoym.sex = "male"
supSoym.level = 99
supSoym.Eat() // 子类重写父类的方法
supSoym.Sleep() // 子类继承父类的方法
supSoym.Fly() // 子类自己的方法
fmt.Println("=======================")
supSoym.Print()
}
定义类对象的两种方法:
使用关键字
var
来定义,这种方法很直观,如果是声明一个继承的子类,需要将父类以及子类中的所有属性给初始化:
govar supSoym SuperHuman supSoym.name = "soyMilk" supSoym.sex = "male" supSoym.level = 99
使用
:=
来定义,这种方法稍微有一些不直观,如果是声明一个子类,需要显示的将父类中的属性给初始化:
gosupSoym := SuperHuman{Human{"soyMilk", "male"}, 99} // 这是一个继承字 Human 类的子类的实例对象
多态
实现多态的方式需要使用到关键字interface
,interface
用来定义一个接口,接口中可以声明方法,只是声明而不去实现这些方法,接口的实例对象相当于是一个指针,在赋值时需要赋值具体类的地址。
定义接口:
go
type AnimalIF interface {
Sleep()
GetColor() string // 得到动物的颜色
GetType() string // 得到动物的种类
}
实现接口的类:
如果要实现接口的方法,需要创建一个类,来实现接口中所有声明的方法(注意:这里需要实现接口中定义的所有声明的方法,不能是其中的一部分),这样才算类实现了该接口。
go
type AnimalIF interface {
Sleep()
GetColor() string // 得到动物的颜色
GetType() string // 得到动物的种类
}
// 具体的类
type Cat struct {
Color string // 表示猫的颜色
}
// 重写接口的方法,需要将接口中的所有方法全部重写,否则就仅仅只是类自己的方法
func (this *Cat) Sleep() {
fmt.Println("Cat Sleep ...")
}
func (this *Cat) GetColor() string {
return this.Color
}
func (this *Cat) GetType() string {
return "Cat"
}
// Dog
type Dog struct {
Color string
}
func (this *Dog) Sleep() {
fmt.Println("Dog Sleep ...")
}
func (this *Dog) GetColor() string {
return this.Color
}
func (this *Dog) GetType() string {
return "Dog"
}
实例:
go
package main
import "fmt"
// interface (接口) 本质上是一个指针
type AnimalIF interface {
Sleep()
GetColor() string // 得到动物的颜色
GetType() string // 得到动物的种类
}
// 具体的类
type Cat struct {
Color string // 表示猫的颜色
}
// 重写接口的方法,需要将接口中的所有方法全部重写,否则就仅仅只是类自己的方法
func (this *Cat) Sleep() {
fmt.Println("Cat Sleep ...")
}
func (this *Cat) GetColor() string {
return this.Color
}
func (this *Cat) GetType() string {
return "Cat"
}
// Dog
type Dog struct {
Color string
}
func (this *Dog) Sleep() {
fmt.Println("Dog Sleep ...")
}
func (this *Dog) GetColor() string {
return this.Color
}
func (this *Dog) GetType() string {
return "Dog"
}
func showAnimal(animal AnimalIF) { // 对传入的不同的类对象,会直接调用不同类对象的自己的对应的方法。
animal.Sleep()
fmt.Println("Color =", animal.GetColor())
fmt.Println("Kind =", animal.GetType())
}
func main() {
// var animal AnimalIF // 接口类型的数据
// animal = &Cat{"Brown"} // 这里体现出接口是一个指针的具体含义了
// animal.Sleep() // 调用的就是Cat的Sleep方法
// animal = &Dog{"Yellow"}
// animal.Sleep() // 调用的是Dog的Sleep方法
cat := Cat{"Brown"}
dog := Dog{"Yellow"}
showAnimal(&cat) // 注意这里传递的是对象的地址
showAnimal(&dog)
}
interface 万能接口
在go语言中基本的数据类型(int,string,bool,float32,float64,struct,...
)都实现了 interface{}
,因此interface{}
可以引用任意的数据类型,因此又称为万能的数据类型,一个最简单的例子,如:
go
var test string = "TEST"
var intest interface{} = test // 这里声明的一个interface{}数据类型可以直接赋值为test的值(string)
fmt.Println("intest =", intest)
断言机制:
断言(Assertion)是编程中用来验证程序状态的一种机制。它允许开发者声明某个条件必须为真,如果条件为假,则程序会抛出错误或异常。断言通常用于调试目的,以确保程序的某个部分按照预期工作。
断言的基本思想是,如果程序的某个部分在逻辑上应该总是为真,那么可以通过断言来验证这一点。如果断言失败(即条件为假),则程序会立即停止执行,并提供有关失败位置的信息,这有助于开发者快速定位和修复问题。
Go 语言中,使用变量名.(数据类型)
来对某一个变量进行断言,这种方式返回两个变量:value
和err
,其中value
表示该变量的值,err
表示该变量的是否与指定的数据类型一致,是一个bool
类型的变量,如:
go
value, ok := arg.(string) // 这里的arg是一个interface{}类型的形参
实例:
go
package main
import "fmt"
// interface{} 是万能的数据类型,
func myFunc(arg interface{}) {
fmt.Println("myFunc is called ...")
fmt.Println(arg)
// 给 interface{} 提供 "断言" 的机制
value, ok := arg.(string) // 判断args是否是字符串类型
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Println("arg is string type, value =", value)
fmt.Printf("value type is %T\n", value)
}
}
type Book struct {
auth string
}
func main() {
book := Book{"Golang"}
myFunc(book)
myFunc(100)
myFunc("abc")
myFunc(3.14)
}
/*
interface 是一个通用万能类型,是一个空接口; int, string, fload32, float64, struct... 等数据类型都实现了 interface{}, 因此可以使用
interface{}类型 来引用任意的数据类型。
*/
pair结构详解
每一个实例对象都包含一个pair<type, value>
内置结构,这个结构不是显示的,直观上来讲,每一个实例对象都有一个数据类型和一个具体的值,这是毫无异议的。这里说的实例对象表示的是具有具体的值和类型的对象,如:整型变量、结构体变量、类对象等都属于实例对象,都是某一种数据类型的具体实现。
pair结构图
注意 :pair结构并不是显式存在的,它是一种隐含的结构,在每一个实例对象中。
实例:简单的pair结构变换
go
package main
import "fmt"
func main() {
var a string
// pair<statictype:string, value: "abcde">
a = "abcde"
// pair<type: string, value: "abcde">
var allType interface{}
allType = a // 这里将a的数据类型赋值给了allType,其值也赋值给了allType了
str, _ := allType.(string)
fmt.Println(str)
}
实例:调用linux终端
go
package main
import (
"fmt"
"os"
"io"
)
/* 改代码只是一个pair的变化过程的演示,并无实际意义 */
func main() {
// "/dev/tty" 表示linux的终端
// tty: pair<type: *os.File, value: "/dev/tty"文件描述符>
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
fmt.Println("open file error", err)
return
}
// r: pair<type: , value: >
var r io.Reader
// r: pair<type: *os.File, value: "/dev/tty"文件描述符>
r = tty
// w: pair<type: , value: >
var w io.Writer
// w: pair<type: *os.File, value: "/dev/tty"文件描述符>
w = r.(io.Writer) // 这一步可以理解为强制类型转换
w.Write([]byte("HELLO THIS is A TEST !!!\n"))
}
实例:
go
package main
import (
"fmt"
)
type Reader interface {
ReadBook()
}
type Writer interface {
WriteBook()
}
// 具体的类
type Book struct {
}
func (this *Book) ReadBook() {
fmt.Println("Read a Book")
}
func (this *Book) WriteBook() {
fmt.Println("Write a Book")
}
func main() {
// b: pair<type: Book, value: book{}地址>
b := &Book{}
// r: pair<type: , value: >
var r Reader
// r: pair<type: Book, value: book{}地址>
r = b
r.ReadBook()
var w Writer
// w: pair<type: Book, value: book{}地址>
w = r.(Writer) // 此处会断言成功,是因为w和r具体的type是一致的,都是interfac{} 数据类型
w.WriteBook()
}
reflect反射机制
Go语言中的反射(reflection)是一种在运行时检查程序本身的机制,它允许程序在运行时动态地操作对象的类型和值。Go的反射机制主要通过reflect
包实现,它提供了Type
和Value
两个核心类型,分别代表了Go的类型信息和值信息。
概述:
导包方法:
go
import "reflect"
// 或
import (
"reflect"
)
-
动态的取到变量的数据类型 (type ),语法 :
reflect.TypeOf(变量名称)
,该方法返回的是变量的数据类型:gofmt.Println("type: ", reflect.TypeOf(arg))
-
动态的取到变量的值 (value ),语法 :
reflect.ValueOf(变量名称)
,该方法返回的是变量的值:gofmt.Println("value: ", reflect.ValueOf(arg)
注意:在使用
reflect.TypeOf(变量名称)
的时候,传递的变量如果是一个指针,那么返回的将会是一个地址,如:变量是一个指向结构体的指针,那么直接打印返回的数据类型,将会得到&{main.结构体名称}
,这是一个地址类型的数据类型,并且可读性很差,如果要得到指针指向的具体的数据类型,可以在返回的变量后添加.Name()
来得到具体的数据类型,而不是一个地址,如:
goinputType := reflect.TypeOf(input) // input 是一个指针,指向一个结构体类型。 fmt.Println("inputType is :", inputType.Name())
-
通过
TyepOf()
来得到类或结构体中每一个字段(属性),可以划分为两个步骤:-
得到一共有多少个字段(属性),语法:
inputType.NumField()
,这里的inputType
是TypeOf()
返回的值。这样可以得到字段(属性)的个数:gonums := inputType.NumField()
-
得到每一个字段,语法:
inputType.Field(i)
,表示得到第i
个字段(属性)。可以通过for
循环来得到所有的字段信息:gofor i := 0; i< inputType.NumField(); i++ { field := inputType.Field(i) // 这里得到的是每一个字段实例 value := inputValue.Field(i).Interface() fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value) }
注意 :在Go语言的反射(reflection)中,
reflect.Value
的Interface()
方法被用来将reflect.Value
对象转换回其原始类型的值 ,这个原始类型的值被封装在Go的interface{}
类型中。
-
实例:简单的反射机制
go
package main
import (
"fmt"
"reflect"
)
func reflectNum(arg interface{}) {
fmt.Println("type: ", reflect.TypeOf(arg)) // reflect.TypeOf可以取到变量的数据类型
fmt.Println("value: ", reflect.ValueOf(arg)) // reflect.ValueOf可以取到变量的值
}
func main() {
var num float64 = 1.2345
reflectNum(num)
}
实例:reflect进阶
go
package main
import (
"reflect"
"fmt"
)
type User struct {
Id int
Name string
Age int
}
func (this User) Call() {
fmt.Println("user is called ..")
fmt.Printf("%v\n", this)
}
func main() {
user := User{1, "Aceld", 18}
DoFiledAndMethod(user)
}
func DoFiledAndMethod(input interface{}) {
// 获取input的type
inputType := reflect.TypeOf(input)
fmt.Println("inputType is :", inputType.Name()) // 直接打印出(干净的)当前数据类型的名称
// 获取input的value
inputValue := reflect.ValueOf(input)
fmt.Println("inputValue is :", inputValue)
// 通过type获取里面的属性
// 1. 获取interface的reflect.Type,通过Type得到NumField(一共有多少个属性)
// 2. 得到每个field,数据类型
// 3. 通过field有一个Interface()方法得到对应的value
for i := 0; i< inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
// 通过type 获取里面的方法,调用
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}
结构体标签(Tag)
在Go语言中,结构体(struct)的字段可以被"标签"(tag),这是一种特殊的字符串,它紧跟在字段声明的类型后面,用反引号(```)包裹。标签提供了一种为结构体字段添加元数据的方式,这些元数据可以被反射(reflection)用来在运行时读取。
反射解析结构体标签:
-
定义结构体标签,在结构体中,每一个字段后可以使用反引号来写入标签,如:
gotype resume struct { Name string `info:"name" doc:"我的名字"` // 使用反引号来写标签, 注意这里使用空格进行分隔,不要无故的添加空格 Sex string `info:"sex"` }
-
使用
reflect
来解析结构体标签,首先得到结构体中的每一个字段(方法在上一节中【reflect反射机制】),对每一个字段使用Get("标签名")
,这样就可以得到每一个字段标签的值了,如:got := reflect.TypeOf(str).Elem() // Elem需要获取指针所指的结构体类型,所以需要传递指针 for i := 0; i < t.NumField(); i++ { tagstring := t.Field(i).Tag.Get("info") tagdoc := t.Field(i).Tag.Get("doc") fmt.Println("info: ", tagstring, "doc: ", tagdoc) }
注意:
在Go语言的反射(reflection)中,
Elem()
方法用于获取一个指针类型的底层类型。当你使用reflect.TypeOf()
方法时,如果传递给它的是一个指针,那么返回的Type
对象将代表指针类型,而不是指针指向的类型。
结构体与Json之间的转换:
结构体标签还可以与 Json 进行结合使用,可以将结构体转换为 Json 类型,同样可以将 Json 类型转换为结构体类型。需要使用到包encoding/json
来进行解析。
导包方法:
go
import "encoding/json"
// 或
import (
"encoding/json"
)
-
声明一个结构体,并且添加结构体标签,注意这里的结构体标签要以
json
为标签的名称,json
标签对应的值可以转换为json
中的键,并且结构体中字段的值可以转换为json
中的值,如:gotype Movie struct { Title string `json:"title"` Year int `json:"year"` Price int `json:"rmb"` Actors []string `json:actors` } // 对于这部分,转换为json数据后,键值对可以表示为:"title": Title.Value()
-
带标签的结构体转换为Json数据 ,需要使用方法:
json.Marshal(结构体对象实例)
,该方法返回两个值jsonStr
和err
,jsonStr
表示结构体转换为的json字符串,err
表示是否转换成功了,是一个bool
的数据类型,:gojsonStr, err := json.Marshal(movie)
-
Json数据转换为结构体变量 ,使用方法:
json.Unmarshal(Json变量名称, &结构体空变量)
,该方法返回一个值err
,表示是否转换成功,如:gomyMovie := Movie{} // 注意,要想将json数据转换为结构体类型,需要先定义一个空的结构体类型 err = json.Unmarshal(jsonStr, &myMovie)
实例:反射解析结构体标签
go
package main
import (
"fmt"
"reflect"
)
type resume struct {
Name string `info:"name" doc:"我的名字"` // 使用反引号来写标签, 注意这里使用空格进行分隔
Sex string `info:"sex"`
}
func findTag(str interface{}) {
// 如果明确指导str是一个非指针变量,就可以不加Elem(),否则就需要加Elem()
t := reflect.TypeOf(str).Elem() // Elem需要获取指针所指的结构体类型,所以需要传递指针
for i := 0; i < t.NumField(); i++ {
tagstring := t.Field(i).Tag.Get("info")
tagdoc := t.Field(i).Tag.Get("doc")
fmt.Println("info: ", tagstring, "doc: ", tagdoc)
}
}
func main() {
var re resume
findTag(&re)
}
实例:Json与结构体之间的转换
go
package main
import (
"fmt"
"encoding/json"
)
// 定义一个结构体
type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:actors`
}
func main() {
// 实例化一个结构体
movie := Movie{"戏剧之王", 2000, 10, []string{"zhouxingxing", "zhangbozhi"}}
// 编码过程
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal error", err)
return
}
fmt.Printf("jsonStr = %s\n", jsonStr)
// 解码过程
// jsonStr = {"title":"戏剧之王","year":2000,"rmb":10,"Actors":["zhouxingxing","zhangbozhi"]}
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json unmarshal error", err)
return
}
fmt.Printf("%v\n", myMovie)
}
协程(coroutine)
进程、线程、协程:
定义:
- 进程:进程是操作系统进行资源分配和调度的一个独立单位。它是应用程序运行的实例,拥有独立的内存空间。
- 线程:线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自身不拥有系统资源,只拥有一点在运行中必不可少的资源(如执行栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。
- 协程:协程是一种程序组件,它允许挂起和恢复执行,通常用于处理I/O密集型任务。协程是一种用户态的轻量级线程,由程序员显式创建和管理。
区别:
- 资源管理 :
- 进程拥有独立的资源。
- 线程共享进程资源。
- 协程通常不拥有资源或只拥有少量资源。
- 调度方式 :
- 进程由操作系统调度。
- 线程由操作系统调度。
- 协程由程序本身调度,这句话的意思是协程的执行流程是由程序员通过代码控制的,而不是由操作系统的调度器来管理。
- 开销 :
- 进程的创建和销毁开销最大,线程次之,协程最小。
- 并发性 :
- 进程和线程可以实现并发执行,协程则通过协作式调度实现并发。
进程
注意 :这里的单个进程的操作系统也可以使用时间片轮转算法,也可以一定程度解决阻塞问题,这里列举的是较为早期的操作系统,用以突出多进程的优势,多进程的最大的优势是并行处理。
线程
并发与并行:
并发(Concurrent):
- 并发指的是两个或多个计算或任务在同一时间段内开始、运行至完成,但其执行点不必同时发生。
- 例子:一个Web服务器处理多个客户端请求,虽然请求是并发处理的,但服务器可能只有一个CPU核心,通过快速切换任务来给用户并发处理的假象。
并行(Parallel):
- 并行指的是两个或多个计算或任务在同一时刻真正地同时执行。
- 例子:一个支持多核处理器的科学计算程序,可以并行地在每个核心上运行不同的计算任务,从而加快整体计算速度。
协程
线程:协程 = 1:1
对于线程:协程 = 1:1模型,本质上还是一个线程模式,没有发挥出协程的优势,协程之间的切换等同于线程之间的切换,开销较大。
线程:协程 = 1:N
对于线程:协程 = 1:N模式,当某个协程因为缺少数据资源而进行阻塞时,此时其他的协程会等待该协程,直到时间片结束或者得到了缺少的资源而结束,这就耗费了一定的时间与系统资源,因此需要对模型进一步改进。
线程:协程 = N:N
调度器
早期调度器
- G:goroutine 协程
- M:线程
早期调度器的几个缺点:
- 创建、销毁、调度GO协程(G)都需要每个线程(M)获取锁,这就形成了激烈的锁竞争。
- 线程(M) 转移GO协程(G)时会造成延迟和额外的系统负载 。(局部性问题:在执行一个GO协程时,该GO协程可能会生成新的协程,从而占用其他的线程)
- 系统调用(CPU 在线程(M)之间切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
GMP
工作模式
Go 调度器的设计策略:
- 线程复用:避免频繁的创建、销毁线程,而是对线程进行复用。
- 并行:利用多核CPU并行执行多个goroutine。
- 抢占:Go 1.14引入了抢占式调度,如果一个goroutine执行时间过长,调度器会强制中断它的执行,将控制权交给其他goroutine。
- 全局goroutine队列:除了每个P维护的本地队列,还有一个全局队列,用于存储所有待执行的goroutine。
创建协程
在 Go 语言中,使用关键字go
来创建一个协程实例,每一个协程实例都是一个函数 或匿名函数。
-
创建一个普通函数协程 的语法,
go 函数名(参数)
,如:gofunc newTask() { i := 0 for { i ++ fmt.Printf("new Goroutine :i = %d\n", i) time.Sleep(1 * time.Second) } } func main() { go newTask() // 使用关键字 go }
-
创建匿名函数协程,如:
gogo func(a int, b int) bool { fmt.Println("a =", a, "b =", b) return true }(10, 20) // 这里在匿名函数后直接使用一个()表示调用该匿名函数,可以根据具体的匿名函数传参。
注意:
- 使用函数或匿名函数协程,如果函数有返回值,并不能直接得到返回值,如:
ans := go func()
,这是不正确 的,函数的返回值不能直接通过赋值得到,需要使用下一节所讲的通道channel
得到函数的返回值。- 如果主函数结束,那么响应的子协程也会跟着结束,即使子协程还没有执行完毕,通常会设置一个标记,来表示子协程执行完毕,父协程(主函数)才能结束。
实例:创建函数协程
go
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i ++
fmt.Printf("new Goroutine :i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
func main() {
// 创建一个go协程,执行newTask()流程
go newTask() // 使用关键字 go
fmt.Println("main Groutine exit.")
// i := 0
// for {
// i ++
// fmt.Printf("main Goroutine :i = %d\n", i)
// time.Sleep(1 * time.Second)
// }
}
实例:创建匿名函数协程
go
package main
import (
"fmt"
"time"
_ "runtime"
)
func main() {
// 使用go创建一个匿名函数协程
// go func() {
// defer fmt.Println("A.defer")
// // return
// func() {
// defer fmt.Println("B.defer")
// // return
// // 退出当前goroutine
// runtime.Goexit()
// fmt.Println("B")
// }() // 使用小括号来调用匿名函数
// fmt.Println("A")
// }()
// 有参匿名函数
// 注意:无法直接得到返回值,不能写 ans :=go func(a int, b int) bool{} 来得到返回值
go func(a int, b int) bool {
fmt.Println("a =", a, "b =", b)
return true
}(10, 20)
// 死循环
for {
time.Sleep(1 * time.Second)
}
}
Channel
Channel是Go语言中的一个核心类型,用于在不同的goroutine之间同步和传递数据。goroutine是Go语言中的轻量级线程,由Go运行时管理。使用channel可以安全地在goroutine之间进行通信,避免共享内存时出现的竞态条件。
基本使用方法:
-
创建/定义一个channel:使用make进行创建channel变量。
goc := make(chan, int) // make给变量c
-
数据传递 :使用符号
<-
尽心数据的传递。goc <- 666 // 将数据 666 传递到通道 c 中 num := <- c // 从c中接受数据,并赋值给num <-c // 也可以不写num,直接扔掉
-
创建有缓冲的channel :使用
make
进行创建。goc := make(chan int, 3) // 创建一个channel,并指定缓冲为3。
-
关闭channel :使用
close(通道名)
进行关闭,注意,关闭channel之后就不能在对channel进行读写操作了。goclose(c) // 判断channel是否关闭 if data, ok := <- c; ok { fmt.Println("channel 没有关闭") } else { fmt.Pringln("channel 已经关闭") break }
-
使用range读取通道数据 :关键字
range
。gofor data := range c { // 可以不断的从channel中读取数据,只有c中有数据,或不断地存入数据 fmt.Println(data) }
-
select 语句 :
select
是一个用于处理多个channel操作的控制结构。它类似于switch
语句,但是每个case
都是针对channel的发送或接收操作。select
语句会阻塞,直到其中一个通信操作可以进行。goselect { case <-channel1: // 当channel1可以接收数据时执行的代码 case channel2 <- x: // 当可以向channel2发送数据时执行的代码 case <-channel3, ok := channel4: // 当channel4可以接收数据时执行的代码,同时接收数据和检查channel是否关闭 default: // 如果以上所有case都不满足时执行的代码 }
有缓冲与无缓冲的区别:
无缓冲channel:
无缓冲channel在发送和接收数据时是同步的,也就是说,发送操作必须等待对应的接收操作才能完成,反之亦然。
它在创建时不会分配任何缓冲空间,因此每次发送操作都必须有对应的接收操作,否则发送方goroutine将会被阻塞。
无缓冲channel通常用于需要确保两个goroutine之间严格同步的场景,它们也被称为同步channel。
无缓冲channel的创建语法是
ch := make(chan Type)
或ch := make(chan Type, 0)
。有缓冲channel:
有缓冲channel允许在发送数据时,如果没有接收者,数据可以被存储在channel的缓冲区中,直到缓冲区满。
接收操作也是类似的,如果缓冲区中没有数据,接收者goroutine将会被阻塞,直到缓冲区中有数据可读。
有缓冲channel可以提高数据传输的效率,因为它们允许发送者和接收者以不同的速率工作,而不必每次都进行同步。
有缓冲channel的创建语法是
ch := make(chan Type, capacity)
,其中capacity
是缓冲区的大小。
实例:定义channel
go
package main
import (
"fmt"
)
// 阻塞机制
func main() {
// 定义一个channel
c := make(chan int)
go func() {
defer fmt.Println("goroutine 结束")
fmt.Println("goroutine 正在运行...")
c <- 666 // 将666 发送到c通道
}()
// 从c中接受,并赋值给num,也可以不写num,直接扔掉, <-c
num := <- c
fmt.Println("num =", num)
fmt.Println("main goroutine 结束...")
}
实例:channel 传值
go
package main
import (
"fmt"
"time"
)
func main() {
// 创建有缓冲的channel
c := make(chan int, 3)
fmt.Println("len(c) =", len(c), ", cap(c) =", cap(c))
go func() {
defer fmt.Println("子go协程结束")
for i:= 0; i < 4;i ++ {
c <- i
fmt.Println("子go协程正在运行: 发送的元素:", i, " len(c)", len(c), ", cap(c) =", cap(c))
}
}()
// 休眠2秒,待数据存满缓冲之后在进行接下来的读取操作。
time.Sleep(2 * time.Second)
for i := 0; i < 4;i ++ {
num := <- c // 从c中接受数据,并赋值给num
fmt.Println("num =", num)
}
// time.Sleep(1 * time.Second)
fmt.Println("main 结束")
}
实例:关闭 channel
go
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i ++ {
c <- i
}
// close可以关闭一个channel
close(c) // 如果没有close就会出现死锁状态
}()
for {
// ok 如果为true表示channel没有关闭,如果为false表示channel已经关闭
if data, ok := <- c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Main Finished ...")
}
实例:range 遍历通道
go
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i ++ {
c <- i
}
// close可以关闭一个channel
close(c) // 如果没有close就会出现死锁状态
}()
// 使用range进行读取数据,可以迭代不断从c通道中读取数据
for data := range c {
fmt.Println(data)
}
fmt.Println("Main Finished ...")
}
实例:select 语句
go
package main
import (
"fmt"
)
func fibnacii(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
// 如果c可写,则该case就会进来
x = y
y = x + y
case <- quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i ++ {
fmt.Println(<- c)
}
quit <- 0
}()
// main go
fibnacii(c, quit) // 通道传参是引用传参
}
GoPath 与 GoModules
GoPath 模式
在Go 1.11版本之前,安装 GO 一定要在环境变量中配置 GoPath
,我们可以简单的将其理解成是工作目录。目录结构如下:
bin
:存放编译后生成的二进制可执行文件。pkg
:存放编译后生成的.a
文件。src
:存放项目的源代码,可以是你自己写的代码,也可以是你 go get 下载的包。
GoPath 模式的缺点
- GOPATH模式下,
go get
命令总是获取依赖的最新版本,没有版本号的概念,无法指定特定版本的依赖包,这在多人协作开发时可能会导致不同开发者使用的依赖版本不一致,从而引发错误 - 在GOPATH模式下,所有项目代码和第三方依赖包都放在
GOPATH/src
目录下,这会导致项目结构混乱,难以管理,特别是当项目数量增多时,不同项目的依赖包会相互交织,难以区分。 - 不同的项目可能需要不同版本的同一依赖,但在GOPATH模式下,无法做到这一点。每个项目都需要下载所有依赖的完整副本,这不仅导致磁盘空间的浪费,也增加了管理的复杂性。
- 在团队协作中,需要确保所有开发成员的
GOPATH/src
目录下的依赖包保持一致,这在实际操作中非常困难,容易出错且不易排查原因。 - 当需要升级某个依赖包时,会导致所有使用该依赖的项目都升级到新版本,这可能带来兼容性问题,因为你无法预知新版本在其他项目中的表现。
- 在GOPATH模式下,项目代码必须放在
GOPATH/src
目录下,这限制了项目的存放位置,不灵活。 - 在Go 1.11之前,由于GOPATH的局限性,社区出现了多种依赖管理工具,如
godep
、govendor
等,这些工具的使用增加了学习成本,且没有统一的标准。
GoModules 模式
Go Modules 在 1.11 版本正式推出,在发布的 v1.14 版本中,官方正式发话,称其已经足够成熟,可以应用于生产上。
在 go env 中多了一个环境变量,GO111MODULE
,这里的 111 表示的是 1.11 版本的象征,该环境变量用于开启或关闭 go mod 模式,可以在终端中输入 go env
来查看 Go 语言所有的环境变量:
GO111MODULE
该环境变量是开启GO MODULES
的开关,其中 GO111MODULE
有三个可选值:
off
:禁用模块支持,编译时会从GOPATH
和vendor
文件夹中查找包。on
:启用模块支持,编译时会忽略GOPATH
和vendor
文件夹,只根据go.mod
下载依赖。auto
:当项目在$GOPATH/src
外且项目根目录有go.mod
文件时,自动开启模块支持。
开启GO Modules模式的命令:
shell
go env -w GO111MODULE="on"
GOPROXY
该环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。GOPROXY的默认值是:'https://proxy.golang.org,direct',其中网络地址proxy.golang.org:国内访问不了,需要设置国内的代理。
设置国内代理:
shell
go env -w GOPROXY=https://goproxy.cn,direct
这条命令会将 Go 代理设置为国内的代理地址 https://goproxy.cn
,并且忽略任何可能存在的代理缓存。
GOSUMDB
它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go moduleproxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。
GOSUMDB 的默认值为:sum.golang.org
,在国内也是无法访问的,但是GOSUMDB可以被 Go模块代理所代理(详见:ProxyingaChecksumDatabase),因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn
就能支持代理 sum.golang.org
,所以这一个问题在设置 GOPROXY后,你可以不需要过度关心。另外若对GOSUMDB的值有自定义需求,其支持如下格式:
- 格式1:
<SUMDB_NAME>+<PUBLIC KEY>
。 - 格式2:
<SUMDB NAME>+<PUBLIC KEY> <SUMDB URL>
。
也可以将其设置为"off",也就是禁止Go在后续操作中校验模块版本。
GONOPROXY/GONOSUMDB/GOPRIVATE
这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。
更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database都无法访问到的模块时的场景。而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GOPRIVATE。
它们的值都是以一个逗号,
分割模块路径前缀,也就是说可以设置多个值,例如:
shell
go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
如此设置之后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块如果不想每次都重新设置。我们也可以利用通配符,例如:
shell
go env -W GOPRIVATE="*.example.com"
这样子设置的话,所有模块路径为example.com的子域名(例如:git.example.com)都将不经过 Gomodule proxy 和 Go checksum database,需要注意的是不包括 example.com 本身。
GoModule的优势
go mod 不再依靠 $GOPATH,使得它可以脱离 GOPATH 来创建项目,你可以在你电脑的任意位置创建一个文件夹。
GoModules 常用命令
go mod init 项目名称
:生成 go.mod 文件,这里的项目名称是自定义的。go mod tidy
:添加缺少的包,且删除无用的包。go mod download
:下载 go.mod 文件中指明的所有依赖go mod tidy 整理现有的依赖。go mod graph
:查看现有的依赖结构。go mod edit
:编辑 go.mod 文件。go mod vendor
:导出项目所有的依赖到vendor目录go mod verify 校验一个模块是否被篡改过go mod why 查看为什么需要依赖某模块。go mod why
: 查看为什么需要依赖。
初始化GOMODULE 项目
- 创建一个文件夹,并进入该目录中。
- 执行
go mod init 项目名称
,初始化项目,此时会生成一个go.mod
文件. - 下载第三方库:
go get 第三方库地址
。 - 当执行包含第三方库的程序之后,会生成
go.sum
文件。
go.mod 与 go.sum
go.mod 文件
功能:
-
定义模块:go.mod 文件定义了项目的模块名称,即模块的路径(通常是代码仓库的路径)。
-
记录依赖:go.mod 文件列出了当前项目所依赖的所有模块及其版本号。它确保了项目在不同环境下能够使用相同的依赖版本进行构建。
-
管理版本:在 go.mod 文件中,可以通过指定版本号来管理依赖的版本,确保项目的可重复构建。
go.sum 文件
功能:
- 校验和信息 :
go.sum
文件记录了所有依赖模块及其版本的校验和信息。这个文件用于确保依赖模块在不同环境下的一致性和完整性,防止模块内容被篡改或不一致。 - 保护项目完整性 :在构建项目时,Go工具链会根据
go.sum
文件中的校验和信息来验证下载的依赖模块是否完整和正确。 - 模块版本校验 :
go.sum
中的每一行记录了模块及其版本的校验和(h1:
开头的部分)。 go.mod
文件校验 :go.sum
还记录了依赖模块自身的go.mod
文件的校验和,确保依赖模块的元数据也是一致的。
区别:
功能不同:
-
go.mod
:主要用于定义模块和记录项目的依赖关系,指定项目使用的依赖版本。 -
go.sum
:主要用于记录依赖模块的校验和信息,确保模块的内容在不同环境下的一致性。
内容不同:
-
go.mod
:内容相对简洁,主要是模块路径和版本号。 -
go.sum
:内容更详细,包含每个依赖模块和其 go.mod 文件的校验和。