学习Go好久了,知识很多都遗忘了,这篇记录下写kv系统时遇到的Go的问题。全文由我和ChatGPT组成。

Q: Go Modules是什么?

Go Modules 是 Go 语言的官方依赖管理系统,从 Go 1.11 版本开始引入。它允许你明确地管理项目的依赖版本,包括自动添加新的依赖到你的模块和自动升级或降级已有的依赖。

在 Go Modules 中,每个项目都是一个模块,每个模块都有一个 go.mod 文件在其根目录。这个文件描述了模块的名称、Go 的版本以及模块的依赖。

例如,一个 go.mod 文件可能看起来像这样:

1
2
3
4
5
6
7
8
module github.com/my/awesomeproject

go 1.14

require (
github.com/some/dependency v1.2.3
github.com/another/dependency v4.5.6
)

在这个例子中,github.com/my/awesomeproject 是模块的名称,go 1.14 是使用的 Go 版本,require 部分列出了模块的依赖及其版本。

当你在模块中运行 Go 命令(如 go buildgo testgo 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
2
3
module github.com/yourusername/yourreponame

go 1.14

这个文件将会被 Go 工具链用来解析和管理你的项目的依赖。

Q: 介绍下Go的结构体,让我快速学会使用,并告诉我使用时需要注意的地方

Go语言中的结构体是一种复合数据类型,允许你将数据项组织成一个单独的结构。这些数据项可以是不同类型的。结构体特别适用于那些需要模拟真实世界对象属性和行为的场景。以下是一些关于如何定义和使用结构体的基础知识,以及使用时的注意事项。

定义结构体

你可以使用 typestruct 关键字来定义一个结构体。

1
2
3
4
type Person struct {
Name string
Age int
}

这段代码定义了一个名为 Person 的结构体,它有两个字段:NameAge

创建结构体实例

有几种方法可以创建结构体的实例。

方法一:直接声明

1
2
3
var p Person
p.Name = "John Doe"
p.Age = 30

方法二:使用 new 关键字

使用 new 关键字将创建一个指向结构体的指针。

1
2
3
p := new(Person)
p.Name = "John Doe"
p.Age = 30

方法三:使用结构体字面量

这种方式允许在创建实例的同时初始化字段值。

1
p := Person{Name: "John Doe", Age: 30}

或者,如果你省略字段名,就需要按照结构体定义中字段的顺序提供值:

1
p := Person{"John Doe", 30}

使用时需要注意的地方

  1. 零值: 未显式初始化的结构体字段将被赋予其类型的零值。例如,int 类型的字段将默认为 0,字符串类型的字段将默认为空字符串 ""

  2. 结构体比较: 只有当两个结构体的所有字段都是可比较的时候,你才可以使用 ==!= 直接比较两个结构体变量。例如,包含切片的结构体是不能比较的,因为切片本身不支持直接比较。

  3. 指针和值的传递: 在函数间传递结构体时,你可以选择传递结构体的值或者是指针。传递值将会复制该结构体,而传递指针则允许你在函数内部修改原始结构体。

  4. 匿名字段和嵌入结构体: Go 支持匿名字段和嵌入结构体,它们可以用来实现一种简单的继承机制。

1
2
3
4
5
6
7
8
9
10
11
12
type Base struct {
ID int
}

type Derived struct {
Base
Name string
}

d := Derived{}
d.ID = 1 // 直接访问 Base 结构体中的 ID 字段
d.Name = "Test"
  1. 字段标签(Field Tags): 结构体字段可以有一个关联的标签字符串,这个标签可以通过反射(reflection)API读取。这在处理JSON、数据库操作以及其它需要元数据的场合特别有用。
1
2
3
4
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}

这些基本知识和注意事项应该能帮助你开始使用Go中的结构体。随着使用的深入,你会遇到更多高级用途和技术细节,但掌握这些基础是非常重要的。

Q: 结构体的字段如何变成私有的

在Go语言中,结构体的字段的访问控制(是否为私有或公有)是通过字段名的首字母的大小写来决定的。具体规则如下:

  • 如果字段名以大写字母开头,则该字段是公有的(Public),意味着它可以被其他包访问。
  • 如果字段名以小写字母开头,则该字段是私有的(Private),意呀着它只能在定义它的包内部被访问,对包外是不可见的。

示例

假设你有一个结构体 Person,你希望它的某些字段如 nameage 为私有字段,只能在同一个包内部被访问和修改。你可以这样定义这个结构体:

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

// Person 结构体的定义
type Person struct {
name string // 私有字段,包外不可访问
age int // 私有字段,包外不可访问
}

// NewPerson 是一个构造函数,用于创建 Person 实例
func NewPerson(name string, age int) *Person {
return &Person{name: name, age: age}
}

// GetName 是一个公有方法,允许外部包获取 Person 实例的 name 字段值
func (p *Person) GetName() string {
return p.name
}

// GetAge 是一个公有方法,允许外部包获取 Person 实例的 age 字段值
func (p *Person) GetAge() int {
return p.age
}

// SetName 是一个公有方法,允许外部包设置 Person 实例的 name 字段值
func (p *Person) SetName(name string) {
p.name = name
}

// SetAge 是一个公有方法,允许外部包设置 Person 实例的 age 字段值
func (p *Person) SetAge(age int) {
p.age = age
}

在这个示例中,通过使 nameage 字段小写(即私有),我们阻止了直接从包外部访问这些字段。而提供的公有方法(如 GetNameSetAge)允许在维持字段封装性的同时,控制对这些私有字段的访问。

注意

  • 通过首字母大小写来控制访问级别是Go语言的一个基本特性,这适用于结构体字段、函数、方法、接口等。
  • 通过这种方式可以在设计API和库时精心控制哪些部分是暴露给外部使用的,哪些部分则隐藏在内部,有助于维护代码的清晰结构和安全。
  • 私有字段的命名惯例是使用小写字母开头,而公有字段、函数或方法等通常使用大写字母开头。
  • 关于值方法接收者和指针方法接收者

方法接收者有两种类型:值接收者和指针接收者。

值接收者:使用值类型的接收者定义的方法,在方法内部对接收者所做的修改不会影响原始值。它是对原始接收者的一个拷贝。

指针接收者:使用指针类型的接收者定义的方法,允许在方法内部修改接收者指向的值。因为指针接收者是对原始接收者的引用,所以它可以修改原始接收者。

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

import "fmt"

type Person struct {
age int
}

func (p Person) howOld() int {
return p.age
}

func (p *Person) growUp() {
p.age += 1
}

func main() {
// qcrao 是值类型
qcrao := Person{age: 18}

// 值类型 调用接收者也是值类型的方法
fmt.Println(qcrao.howOld())

// 值类型 调用接收者是指针类型的方法
qcrao.growUp()
fmt.Println(qcrao.howOld())

// ----------------------

// stefno 是指针类型
stefno := &Person{age: 100}

// 指针类型 调用接收者是值类型的方法
fmt.Println(stefno.howOld())

// 指针类型 调用接收者也是指针类型的方法
stefno.growUp()
fmt.Println(stefno.howOld())
}

这个代码里体现的一点是,不论是在值类型上调用指针类型的方法,还是在指针类型上调用值类型方法都是可以的,这里其实是语法糖,前者被转为指针,后者被解引用。

方法接收者:

  • 值类型,既可以调用值接收者的方法,也可以调用指针接收者的方法
  • 指针类型,既可以调用指针接收者的方法,也可以调用值接收者的方法
    但是接口的实现,值类型接收者和指针类型接收者不一样:
  • 以值类型接收者实现接口,类型本身和该类型的指针类型,都实现了该接口
  • 以指针类型接收者实现接口,只有对应的指针类型才被认为实现了接口
    通常我们使用指针作为方法的接收者的意义:
  • 使用指针方法能够修改接收者指向的值
  • 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效
  • 说一下Go的引用,指针,解引用等相关知识

在Go语言中,引用、指针和解引用都是与内存地址和存储值的位置有关的概念。

引用

引用是一种间接访问某个值的方式。在Go语言中,当我们创建一个变量时,可以通过该变量的名称直接访问其值。但是,有时我们可能想要间接地访问这个值,比如,我们可能想要在函数或方法中修改这个值,或者我们可能想要共享这个值,而不是创建一个新的副本。这时,我们就可以使用引用。

在Go语言中,引用的具体形式通常是指针。但是,我们也可以通过其他方式创建引用,比如使用切片或映射。

指针

指针是一种特殊的数据类型,它存储了另一个值在内存中的地址。我们可以通过指针间接地访问、修改这个值。在Go语言中,我们可以使用 & 操作符获取一个变量的地址,然后将这个地址赋值给一个指针变量。

例如:

1
2
var x int = 1
var p *int = &x

在这个例子中,p 是一个指针变量,它存储了变量 x 的内存地址。

解引用

解引用是通过指针访问或修改它指向的值的过程。在Go语言中,我们可以使用 * 操作符对指针进行解引用。

例如:

1
2
3
var x int = 1
var p *int = &x
*p = 2

在这个例子中,*p = 2 就是一个解引用的操作。它将 2 赋值给 p 所指向的值,也就是变量 x。所以,执行这段代码后,x 的值将变为 2

注意,在Go语言中,所有的值都是通过值传递的,包括指针。当我们把一个指针传递给一个函数或方法时,实际上是创建了这个指针的一个副本。但是,因为这个副本和原始指针都指向同一个值,所以我们可以通过这个副本修改这个值。这就是所谓的通过指针传递引用的效果。

  • 在Go中这两个操作符有什么特别要注意的吗

在Go语言中使用 *& 操作符时,有几点需要特别注意:

  1. 空指针解引用:尝试解引用一个空指针(即值为 nil 的指针)会导致运行时错误(panic)。在解引用指针之前,你应该确认它不是 nil

    1
    2
    3
    4
    5
    6
    var p *int
    if p != nil {
    *p = 10 // 安全,因为检查了 p 是否为 nil
    } else {
    fmt.Println("Cannot dereference a nil pointer")
    }
  2. 类型匹配:解引用操作符 * 和地址操作符 & 必须与正确的类型一起使用。* 用于指针类型的变量,而 & 用于可以取地址的值。

    1
    2
    3
    4
    5
    6
    var x int
    var p *int = &x // 正确: p 是一个指向整型的指针
    var y int = *p // 正确: *p 解引用 p 指向的整型值

    // var z int = &y // 错误: &y 的类型是 *int,不能赋值给 int 类型的变量
    // var w = *x // 错误: x 不是一个指针,不能解引用
  3. 可取地址的变量:只有可取地址的变量才能使用 & 操作符。比如,你不能直接对一个字面量或某些临时结果取地址。

    1
    2
    3
    4
    5
    // &42 // 错误: 字面量不能取地址
    // &(4 + 2) // 错误: 临时表达式结果不能取地址

    y := 42
    p := &y // 正确: 可以取变量 y 的地址
  4. 函数参数传递:Go语言使用值传递,当将变量作为参数传递给函数时,实际上是传递变量的副本。如果你传递一个指针,函数内对解引用值的修改将影响原始变量,因为指针的副本指向的是同一内存地址。

    1
    2
    3
    4
    5
    6
    7
    func mutate(num *int) {
    *num = 100
    }

    x := 5
    mutate(&x)
    fmt.Println(x) // 输出: 100,因为传递了变量 x 的地址
  5. 指针类型的零值:所有指针类型的零值都是 nil。在使用指针之前,初始化它指向某个有效的内存地址或检查是否为 nil 都是非常重要的。

  6. 指针算术:Go语言中没有C和C++中那样的指针算术操作。你不能直接对指针进行加减运算,也就是说,你不能增加或减少指针的值来指向内存中的下一个或前一个地址。这是Go设计者有意为之,以增加安全性和简化程序设计。

始终确保当你在Go代码中使用 *& 操作符时,你完全理解它们的含义及其对变量和内存的影响。这将有助于避免常见的错误,并编写出更健壮的代码。