Go杂记
学习Go好久了,知识很多都遗忘了,这篇记录下写kv系统时遇到的Go的问题。全文由我和ChatGPT组成。
Q: Go Modules是什么?
Go Modules 是 Go 语言的官方依赖管理系统,从 Go 1.11 版本开始引入。它允许你明确地管理项目的依赖版本,包括自动添加新的依赖到你的模块和自动升级或降级已有的依赖。
在 Go Modules 中,每个项目都是一个模块,每个模块都有一个 go.mod
文件在其根目录。这个文件描述了模块的名称、Go 的版本以及模块的依赖。
例如,一个 go.mod
文件可能看起来像这样:
1 | module github.com/my/awesomeproject |
在这个例子中,github.com/my/awesomeproject
是模块的名称,go 1.14
是使用的 Go 版本,require
部分列出了模块的依赖及其版本。
当你在模块中运行 Go 命令(如 go build
、go test
或 go run
)时,Go 会查看 go.mod
文件,确保所有列出的依赖都被正确地下载和安装。
Go Modules 还包括一个叫做 go.sum
的文件,它包含了每个依赖的预期加密哈希值,以确保你下载的依赖的完整性和安全性。
go mod init
是 Go 语言的一个命令,用于初始化一个新的 Go Modules 项目。Go Modules 是 Go 语言的依赖管理系统。
Q: go mod init这个命令什么意思?
当你在项目的根目录下运行 go mod init [module-name]
命令时,它会创建一个新的 go.mod
文件。这个文件会包含模块名(module name)和 Go 语言的版本。模块名通常是你的代码库的导入路径。
例如,如果你的代码库在 GitHub 上,那么你可以使用以下命令来初始化你的 Go Modules 项目:
1 | go mod init github.com/yourusername/yourreponame |
这将会创建一个 go.mod
文件,内容如下:
1 | module github.com/yourusername/yourreponame |
这个文件将会被 Go 工具链用来解析和管理你的项目的依赖。
Q: 介绍下Go的结构体,让我快速学会使用,并告诉我使用时需要注意的地方
Go语言中的结构体是一种复合数据类型,允许你将数据项组织成一个单独的结构。这些数据项可以是不同类型的。结构体特别适用于那些需要模拟真实世界对象属性和行为的场景。以下是一些关于如何定义和使用结构体的基础知识,以及使用时的注意事项。
定义结构体
你可以使用 type
和 struct
关键字来定义一个结构体。
1 | type Person struct { |
这段代码定义了一个名为 Person
的结构体,它有两个字段:Name
和 Age
。
创建结构体实例
有几种方法可以创建结构体的实例。
方法一:直接声明
1 | var p Person |
方法二:使用 new 关键字
使用 new
关键字将创建一个指向结构体的指针。
1 | p := new(Person) |
方法三:使用结构体字面量
这种方式允许在创建实例的同时初始化字段值。
1 | p := Person{Name: "John Doe", Age: 30} |
或者,如果你省略字段名,就需要按照结构体定义中字段的顺序提供值:
1 | p := Person{"John Doe", 30} |
使用时需要注意的地方
零值: 未显式初始化的结构体字段将被赋予其类型的零值。例如,
int
类型的字段将默认为0
,字符串类型的字段将默认为空字符串""
。结构体比较: 只有当两个结构体的所有字段都是可比较的时候,你才可以使用
==
和!=
直接比较两个结构体变量。例如,包含切片的结构体是不能比较的,因为切片本身不支持直接比较。指针和值的传递: 在函数间传递结构体时,你可以选择传递结构体的值或者是指针。传递值将会复制该结构体,而传递指针则允许你在函数内部修改原始结构体。
匿名字段和嵌入结构体: Go 支持匿名字段和嵌入结构体,它们可以用来实现一种简单的继承机制。
1 | type Base struct { |
- 字段标签(Field Tags): 结构体字段可以有一个关联的标签字符串,这个标签可以通过反射(reflection)API读取。这在处理JSON、数据库操作以及其它需要元数据的场合特别有用。
1 | type Person struct { |
这些基本知识和注意事项应该能帮助你开始使用Go中的结构体。随着使用的深入,你会遇到更多高级用途和技术细节,但掌握这些基础是非常重要的。
Q: 结构体的字段如何变成私有的
在Go语言中,结构体的字段的访问控制(是否为私有或公有)是通过字段名的首字母的大小写来决定的。具体规则如下:
- 如果字段名以大写字母开头,则该字段是公有的(Public),意味着它可以被其他包访问。
- 如果字段名以小写字母开头,则该字段是私有的(Private),意呀着它只能在定义它的包内部被访问,对包外是不可见的。
示例
假设你有一个结构体 Person
,你希望它的某些字段如 name
和 age
为私有字段,只能在同一个包内部被访问和修改。你可以这样定义这个结构体:
1 | package mypackage |
在这个示例中,通过使 name
和 age
字段小写(即私有),我们阻止了直接从包外部访问这些字段。而提供的公有方法(如 GetName
和 SetAge
)允许在维持字段封装性的同时,控制对这些私有字段的访问。
注意
- 通过首字母大小写来控制访问级别是Go语言的一个基本特性,这适用于结构体字段、函数、方法、接口等。
- 通过这种方式可以在设计API和库时精心控制哪些部分是暴露给外部使用的,哪些部分则隐藏在内部,有助于维护代码的清晰结构和安全。
- 私有字段的命名惯例是使用小写字母开头,而公有字段、函数或方法等通常使用大写字母开头。
- 关于值方法接收者和指针方法接收者
方法接收者有两种类型:值接收者和指针接收者。
值接收者:使用值类型的接收者定义的方法,在方法内部对接收者所做的修改不会影响原始值。它是对原始接收者的一个拷贝。
指针接收者:使用指针类型的接收者定义的方法,允许在方法内部修改接收者指向的值。因为指针接收者是对原始接收者的引用,所以它可以修改原始接收者。
1 | package main |
这个代码里体现的一点是,不论是在值类型上调用指针类型的方法,还是在指针类型上调用值类型方法都是可以的,这里其实是语法糖,前者被转为指针,后者被解引用。
方法接收者:
- 值类型,既可以调用值接收者的方法,也可以调用指针接收者的方法
- 指针类型,既可以调用指针接收者的方法,也可以调用值接收者的方法
但是接口的实现,值类型接收者和指针类型接收者不一样: - 以值类型接收者实现接口,类型本身和该类型的指针类型,都实现了该接口
- 以指针类型接收者实现接口,只有对应的指针类型才被认为实现了接口
通常我们使用指针作为方法的接收者的意义: - 使用指针方法能够修改接收者指向的值
- 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效
- 说一下Go的引用,指针,解引用等相关知识
在Go语言中,引用、指针和解引用都是与内存地址和存储值的位置有关的概念。
引用
引用是一种间接访问某个值的方式。在Go语言中,当我们创建一个变量时,可以通过该变量的名称直接访问其值。但是,有时我们可能想要间接地访问这个值,比如,我们可能想要在函数或方法中修改这个值,或者我们可能想要共享这个值,而不是创建一个新的副本。这时,我们就可以使用引用。
在Go语言中,引用的具体形式通常是指针。但是,我们也可以通过其他方式创建引用,比如使用切片或映射。
指针
指针是一种特殊的数据类型,它存储了另一个值在内存中的地址。我们可以通过指针间接地访问、修改这个值。在Go语言中,我们可以使用 &
操作符获取一个变量的地址,然后将这个地址赋值给一个指针变量。
例如:
1 | var x int = 1 |
在这个例子中,p
是一个指针变量,它存储了变量 x
的内存地址。
解引用
解引用是通过指针访问或修改它指向的值的过程。在Go语言中,我们可以使用 *
操作符对指针进行解引用。
例如:
1 | var x int = 1 |
在这个例子中,*p = 2
就是一个解引用的操作。它将 2
赋值给 p
所指向的值,也就是变量 x
。所以,执行这段代码后,x
的值将变为 2
。
注意,在Go语言中,所有的值都是通过值传递的,包括指针。当我们把一个指针传递给一个函数或方法时,实际上是创建了这个指针的一个副本。但是,因为这个副本和原始指针都指向同一个值,所以我们可以通过这个副本修改这个值。这就是所谓的通过指针传递引用的效果。
- 在Go中这两个操作符有什么特别要注意的吗
在Go语言中使用 *
和 &
操作符时,有几点需要特别注意:
空指针解引用:尝试解引用一个空指针(即值为
nil
的指针)会导致运行时错误(panic)。在解引用指针之前,你应该确认它不是nil
。1
2
3
4
5
6var p *int
if p != nil {
*p = 10 // 安全,因为检查了 p 是否为 nil
} else {
fmt.Println("Cannot dereference a nil pointer")
}类型匹配:解引用操作符
*
和地址操作符&
必须与正确的类型一起使用。*
用于指针类型的变量,而&
用于可以取地址的值。1
2
3
4
5
6var x int
var p *int = &x // 正确: p 是一个指向整型的指针
var y int = *p // 正确: *p 解引用 p 指向的整型值
// var z int = &y // 错误: &y 的类型是 *int,不能赋值给 int 类型的变量
// var w = *x // 错误: x 不是一个指针,不能解引用可取地址的变量:只有可取地址的变量才能使用
&
操作符。比如,你不能直接对一个字面量或某些临时结果取地址。1
2
3
4
5// &42 // 错误: 字面量不能取地址
// &(4 + 2) // 错误: 临时表达式结果不能取地址
y := 42
p := &y // 正确: 可以取变量 y 的地址函数参数传递:Go语言使用值传递,当将变量作为参数传递给函数时,实际上是传递变量的副本。如果你传递一个指针,函数内对解引用值的修改将影响原始变量,因为指针的副本指向的是同一内存地址。
1
2
3
4
5
6
7func mutate(num *int) {
*num = 100
}
x := 5
mutate(&x)
fmt.Println(x) // 输出: 100,因为传递了变量 x 的地址指针类型的零值:所有指针类型的零值都是
nil
。在使用指针之前,初始化它指向某个有效的内存地址或检查是否为nil
都是非常重要的。指针算术:Go语言中没有C和C++中那样的指针算术操作。你不能直接对指针进行加减运算,也就是说,你不能增加或减少指针的值来指向内存中的下一个或前一个地址。这是Go设计者有意为之,以增加安全性和简化程序设计。
始终确保当你在Go代码中使用 *
和 &
操作符时,你完全理解它们的含义及其对变量和内存的影响。这将有助于避免常见的错误,并编写出更健壮的代码。