一个计算机技术爱好者与学习者

0%

好好学Golang:Golang语法拾贝

1. 前言

本文记录使用Golang时遇到的一些语法问题,备忘。

2. 结构体

2.1. 结构体定义方法

在Go语言中,结构体(struct)是一种定义复合数据类型的方式,非常适合用来封装和组织相关属性。

结构体定义了一组具有零个或多个不同类型的变量,这些变量集合在一起,表示一个实体。在Go中,结构体的定义如下:

1
2
3
4
5
type 结构体名称 struct {
    字段1 数据类型
    字段2 数据类型
    // ...
}

例如,我们可以创建一个代表人的结构体:

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

在这个Person结构体中,有三个字段,分别存储着名字(Name)、年龄(Age)和城市信息(City)。

创建结构体实例的一个方法是按照字段的顺序提供字段值:

1
p := Person{"Alice", 30, "London"}

另外也可以通过字段名初始化结构体,这使得顺序不再重要,增加了代码的可读性:

1
2
3
4
5
p := Person{
    Name: "Bob",
    Age: 25,
    City: "New York",
}

在上述两种初始化方式中,如果某些字段被省略,这些字段会被初始化为其类型的零值。

结构体是值类型,在函数调用时会复制相应的值。如果需要在函数间共享结构体数据,通常是通过指针来做的。在Go中,使用&符号可以获取结构体实例的内存地址(指针),然后可以将该指针传递到函数中。

1
2
3
4
5
6
7
func updateAge(p *Person, newAge int) {
    p.Age = newAge
}

// 使用
p := Person{Name: "Charlie", Age: 20, City: "Paris"}
updateAge(&p, 21)

结构体中也可以定义方法。方法是附加到结构体类型上的函数,它可以访问结构体中的字段。方法的定义如下:

1
2
3
func (接收器变量 结构体类型) 方法名(参数列表) 返回参数 {
    // 方法体
}

例如,给Person结构体添加一个方法来打印个人信息:

1
2
3
4
5
6
7
func (p Person) PrintInfo() {
    fmt.Printf("Name: %s, Age: %d, City: %s\n", p.Name, p.Age, p.City)
}

// 使用
p := Person{Name: "Dave", Age: 32, City: "Miami"}
p.PrintInfo()

结构体类型可以嵌套。即,一个结构体的字段可以是另一个结构体类型,这允许构造更复杂的数据模型,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Address struct {
    Street string
    City string
}

type Employee struct {
    Name    string
    Age     int
    Address Address
}

// 使用
e := Employee{
    Name: "Eve",
    Age: 28,
    Address: Address{
        Street: "Main St",
        City: "Springfield",
    },
}

在Go中,结构体以及结构体上的字段和方法,如果首字母大写,则表示它们是导出的(即在包外部可见)。如果首字母小写,则它们是未导出的,只能在定义它们的包内部访问。

Go语言没有提供传统面向对象编程语言中的继承机制,而是通过接口(interface)和结构体嵌套(composition)来实现多态和代码复用。

2.2. 结构体转JSON(序列化)

在Go语言中,encoding/json标准库提供了处理JSON数据的功能,包括将结构体转换为JSON(序列化)以及将JSON转换为结构体(反序列化)。

要将一个Go结构体转换为JSON字符串,可以使用json.Marshal函数。下面是一个简单的示例:

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 (
    "encoding/json"
    "fmt"
    "log"
)

type Person struct {
    Name string `json:"name"`
    Age int    `json:"age"`
    City string `json:"city"`
}

func main() {
    p := Person{"Alice", 30, "New York"}
    
    jsonData, err := json.Marshal(p)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Println(string(jsonData))
}

输出将是:

1
{"name":"Alice","age":30,"city":"New York"}

在此示例中,Person结构体定义了JSON字段名通过使结构体标签。当json.Marshal被调用时,它会检查每个结构体字段的这些标签,并使用它们作为JSON对象中的键名。

如果不想让某些字段出现在JSON中,可以使用json:"-"来排除这些字段,或者使字段名的首字母小写,这样在默认情况下它不会被序列化。

2.3. JSON转结构体(反序列化)

将JSON数据转换为Go结构体的操作,是通过json.Unmarshal函数来完成的。首先让我们看一个如何实现的示例:

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 (
    "encoding/json"
    "fmt"
    "log"
)

type Person struct {
    Name string `json:"name"`
    Age int    `json:"age"`
    City string `json:"city"`
}

func main() {
    jsonData := []byte(`{"name":"Bob","age":25,"city":"Los Angeles"}`)
    
    var p Person
    err := json.Unmarshal(jsonData, &p)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("%+v\n", p)
}

输出将是:

1
{Name:Bob Age:25 City:Los Angeles}

在上面这个例子中,我们首先定义了一个JSON字符串,然后将其转换为字节切片(因为json.Unmarshal函数需要一个字节切片作为输入)。接下来创建了Person类型的变量p,并将其地址传递给json.Unmarshal,它将解析json数据,并填充到相应的结构体字段中。

值得注意的是,字段名在结构体标签中指定的JSON键必须与实际的JSON数据匹配。此外,如果JSON中的数据类型和结构体字段的数据类型不匹配,json.Unmarshal会报错。

使用结构体标签,我们可以控制序列化和反序列化的行为,例如忽略空值或者只在序列化时包含字段等:

  • omitempty选项会让字段在序列化JSON时,如果是空值,则忽略它。
  • omitempty也可以用在反序列化JSON时,如果JSON中某个字段缺失,则会保持该字段的零值。

下面的示例中,如果City字段为空,在序列化时它将不会出现在JSON中:

1
2
3
4
5
type Person struct {
    Name string `json:"name"`
    Age int    `json:"age"`
    City string `json:"city,omitempty"`
}

处理结构体转换为JSON和JSON转换为结构体时,总是推荐进行错误检查,以防数据格式不正确或是类型不匹配,避免潜在的运行时问题。

2.4. 自定义结构体打印格式

使用 fmt.Println 或者 logrus.Info 来打印一个结构体,它将默认调用结构体的 String() 方法(结构体实现了 fmt.Stringer 接口)或者直接打印结构体的值,不会直接打印字段名(key)。
因此,如果想要自定义结构体的输出格式,那么需要重写 String() 方法。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Person struct {
Name string
Age int
}

p := Person{"Alice", 25}

// 打印结构体,只输出值
fmt.Println(p)

func (p Person) String() string {
return fmt.Sprintf("Name: %s, Age: %d", p.Name, p.Age)
}

// 将会调用 p.String() 并输出 "Name: Alice, Age: 25"
fmt.Println(p)

2.5. 结构体的替代

在Go语言中,结构体(struct)是一种自定义的数据类型,它允许我们组合不同的字段(属性),这些字段可以是不同的数据类型。使用结构体是一种强类型的方法来表示一组相关的数据。不过,在某些场景中,我们可能需要一种更灵活的数据结构来处理动态类型的数据或未知的数据模式。这时,可以使用map[string]interface{}unstructured作为结构体的替代方案。

2.5.1. map[string]interface{}

使用这种类型的映射来存储键值对,其中键是字符串类型,代表属性的名称,而值是interface{}类型,可以接受任何类型的值。这种方式的好处是我们可以存储任何结构的数据,但缺点是失去了类型安全,需要在使用时进行类型断言。

例子:

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 main() {
// 创建一个map[string]interface{}的实例
person := make(map[string]interface{})

// 向map中添加数据
person["name"] = "张三"
person["age"] = 25
person["hobbies"] = []string{"读书", "旅游"}

// 打印map的内容
fmt.Println(person)

// 读取map中的特定值
name := person["name"].(string) // 类型断言
fmt.Println("名字:", name)
}

在这个例子中,我们创建了一个关于 person 的map,可以存储人名、年龄和爱好的数据。显然,这种方式比较灵活,但在读取map时,每次都需要对取出的值做类型断言,这样会带来运行时的风险。

2.5.2. unstructured

unstructured是来自Kubernetes的client-go库的一个类型,专门用来处理Kubernetes的API资源。它也是建立在map[string]interface{}基础上的,但提供了一套更为丰富的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
40
41
42
43
44
45
package main

import (
"fmt"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)

func main() {
// 创建一个Unstructured对象
u := unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "example-pod",
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "web",
"image": "nginx",
},
},
},
},
}

// 获取groupVersionKind信息
gvk := schema.GroupVersionKind{
Version: "v1",
Kind: "Pod",
}
u.SetGroupVersionKind(gvk)

// 打印Unstructured对象的内容
fmt.Printf("Unstructured对象: %+v\n", u)

// 获取和访问spec中的容器信息
containers, _, _ := unstructured.NestedSlice(u.Object, "spec", "containers")
for _, container := range containers {
fmt.Printf("容器: %+v\n", container)
}
}

在这个例子中,我们创建了一个Kubernetes Pod资源的unstructured.Unstructured对象,并设置了它的API版本、资源类型和其他元信息。然后,我们通过特定的函数访问了spec.containers字段的内容。unstructured.Unstructured提供了一系列辅助函数来处理嵌套的字段,这使得访问和修改数据变得更加方便。

事实上,unstructured的核心仍然是一个map[string]interface{},但是它提供了那些使得处理非结构化数据更易用的方法。而普通的map[string]interface{}则更为基础,适用于那些没有特殊库辅助方法的情况。

3. 函数与接口

3.1. 函数与接口的定义方法

Go 语言中的函数和方法是两个不同的概念,虽然它们的声明语法很类似,但是它们在 Go 中的作用和使用方式有所不同。

函数
Go 语言定义函数使用关键字 func,下面是 Go 语言函数定义的通用语法:

1
2
3
func functionname(parametername type) returntype {  
    // 函数体(具体的执行代码)
}

示例:

1
2
3
func add(x int, y int) int {  
    return x+y
}

在上面的代码片段中,函数 add 接收两个 int 类型的输入参数,返回一个 int 类型的值。

方法
方法是附加在一个给定类型的值上的一个函数,它能访问该类型的值的所有字段和方法。方法定义的语法如下:

1
2
3
func (t Type) methodName(parametername type) returntype {  
    // 方法体(具体的执行代码)
}

Type 是 Go 语言中的一个类型,可以是内置的类型,例如 intfloat32 等,也可以是用户自定义的类型。

示例:

1
2
3
4
5
6
7
8
type Rectangle struct {  
    length int
    width int
}

func (r Rectangle) Area() int {
    return r.length*r.width
}

在上面的代码片段中,Area 是作用在 Rectangle 类型上的一个方法,它使用 Rectangle 类型的 lengthwidth 字段计算矩形的面积。

当我们有一个类型的实例时,我们可以使用 . 来调用它的方法:

1
2
3
4
5
6
r := Rectangle{
    length: 10,
    width: 5,
}

fmt.Println(r.Area()) // 输出 50

3.2. 指针接收器

(t Type)(t *Type) 都是 Go 语言方法的接收器(也称为接收者)类型,前者是值接收器,后者是指针接收器。

对于值接收器 (t Type),在调用 t 的任何方法时,会将 t 的值复制一份,这意味着在方法体内对 t 的任何修改,只会影响复制的那份,不会影响原来的值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
type myType struct {
    value int
}

func (t myType) setValue(val int) {
    t.value = val
}

func main() {
    x := myType{value: 10}
    x.setValue(20)
    fmt.Println(x.value) // 输出 10
}

使用指针接收器 (t *Type) 时,不会复制原始值,而是直接获取到对象的内存地址,因此,在方法体内对 t 所做的任何修改,都会直接反映在原始值上。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
type myType struct {
    value int
}

func (t *myType) setValue(val int) {
    t.value = val
}

func main() {
    x := &myType{value: 10}
    x.setValue(20)
    fmt.Println(x.value) // 输出 20
}

选择使用值接收器还是指针接收器,主要取决于我们是否需要在方法内修改接收器的状态,以及我们是否希望避免复制(例如,当类型的值很大时,复制可能会很昂贵)。

4. 单元测试

4.1. 运行测试

假设 go.md 中的 module 为 pod-data-restorer , xxx_test.go 中 package 为 podwebhook

1、测试运行整个包

1
go test pod-data-restorer/podwebhook

2、测试运行包中某个测试函数

1
go test -run TestFunctionName pod-data-restorer/podwebhook

4.2. 单元测试时输出日志

在Go的单元测试中,标准的方式是使用来自 testing 包的 T 结构的 Log 方法和其变种如 LogfErrorErrorfFatalFatalf 等。

这是一个例子:

1
2
3
4
func TestExample(t *testing.T) {
    t.Log("This is a log message during testing.")
    t.Logf("Formatted log message with value: %d", 42)
}

注意:如果用 go test 运行测试,正常的 t.Logt.Logf 输出是不会被显示的,只有当测试失败或者我们用 go test -v 运行测试,t.Log 的输出才会被显示。

如果我们需要日志在任何情况下都显示,考虑使用 t.Errort.Errorf。这会使当前的测试失败,并输出我们提供的日志信息。

例如:

1
2
3
4
func TestExample(t *testing.T) {
    t.Error("This is an error message. The test will fail.")
    t.Errorf("Formatted error message with value: %d", 42)
}

FatalFatalf 方法也会使测试失败,但是它们会立即停止当前的测试函数,不会执行后面的代码。而 ErrorErrorf 不会停止测试函数的执行。

例如:

1
2
3
4
func TestExample(t *testing.T) {
    t.Fatal("This is a fatal error message. The test will fail and stop.")
    t.Fatalf("This will not be displayed because the test already stopped.")
}

5. 协程Goroutine

参考文档:《好好学Golang:协程Goroutine》