th3ee9ine

会写一点Java、Go、Python、Vue的后端菜鸡

  • 主页
  • 技术笔记
  • 随笔
所有文章 友链 关于我

th3ee9ine

会写一点Java、Go、Python、Vue的后端菜鸡

  • 主页
  • 技术笔记
  • 随笔

Go 入门总结(二)

阅读数:2743次 2022-05-07
字数统计: 21k字   |   阅读时长≈ 93分

摘要:go入门相关内容,参考:Go入门指南,主要总结其中:六到十一章的内容。


一、函数

1、基本介绍

1)特性

  • 函数是基本的代码块。
  • Go 是编译型语言,所以函数编写的顺序是无关紧要的。
  • 最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
  • DRY 原则:即不要重复你自己(Don’t Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次。
  • 当函数执行到代码块最后一行(} 之前)或者 return 语句的时候会退出,其中 return 语句可以带有零个或多个参数。
  • 函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如:f1(f2(a, b))。
  • Go不允许函数重载。

2)不同类型的函数

Go 里面有三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者 lambda 函数
  • 方法

除了 main ()、init () 函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。

3)语法

函数基本语法如下:

1
2
3
func g() {
......
}

函数被调用的基本格式如下:

1
pack1.Function(arg1, arg2, …, argn)

Function 是 pack1 包里面的一个函数,括号里的是被调用函数的 实参(argument):这些值被传递给被调用函数的 形参。函数被调用的时候,这些实参将被复制(简单而言)然后传递给被调用函数。

函数一般是在其他函数里面被调用的,这个其他函数被称为调用函数(calling function)。函数能多次调用其他函数,这些被调用函数按顺序(简单而言)执行,理论上,函数调用其他函数的次数是无限制的(直到函数调用栈被耗尽)。

4)方法

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

Go语言中的 ​​方法(Method)​​ 是一种作用于特定类型变量的函数。这种特定类型变量叫做 ​​接收者(Receiver)​​。接收者的概念就类似于其他语言中的 ​​this​​ 或者 ​​self​​。

方法的定义格式如下:

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

在go语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接受者的函数,方法和函数的写法只有一个区别,那就是 方法在 func 和标识符之间多了一个参数——接受者定义在func关键字和函数名之间。

接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。

方法的接收者什么时候适合指向结构体的指针而不是结构体值?

以C语言为例,我们知道在C中如果对一个指针变量进行修改,那么它的“副本”们也都会被修改。所以在Go中,如果你的方法是需要对结构体本身修改的,请使用指针作为接收器。否则,以值为接收器,改动将无效(类似slice,map特殊结构体)。你的结构体很大,用一个指针传递,会节约资源。为统一起见,如果你的大多方法以指针为接收器,请把其他方法也优先以指针作为接收器,不然调用时可能会提示该方法不属于某结构体的乌龙错误。

5)函数类型

函数也可以以申明的方式被使用,作为一个函数类型,如下所示:

1
2
3
4
5
6
7
// 声明 Greeting 函数类型
type Greeting func(name string) string

// 定义方法
func (g Greeting) say(n string) {
fmt.Println(g(n))
}

GO 是类C语言,也就是面向过程语言。见此
以C语言为例,它是没有类这种概念的,只有结构体。
在C中,编写一个结构体的方法,我们通常采用以下写法:

1
2
3
void func(s *Student, id int){
...
}

把第一个参数做为该方法的 “类对象”,也称为 接受者,receiver。
而 Go只是把C语言中的第一个参数放到方法前面 而已,所以它并不是用来类型转换的,而是一个接收者,说明该方法属于哪个结构体。

2、传递变长参数

如果函数的最后一个参数是采用 …type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变长函数。

1
func myFunc(a, b, arg ...int) {}

这个函数接受一个类似某个类型的 slice 的参数,该参数可以通过 for 循环结构迭代。

如果参数被存储在一个 slice 类型的变量 slice 中,则可以通过 slice… 的形式来传递参数调用变参函数,如下所示:

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 "fmt"

func main() {
x := min(1, 3, 2, 0)
fmt.Printf("The minimum is: %d\n", x)
slice := []int{7,9,3,5,1}
x = min(slice...)
fmt.Printf("The minimum in the slice is: %d", x)
}

func min(s ...int) int {
if len(s)==0 {
return 0
}
min := s[0]
for _, v := range s {
if v < min {
min = v
}
}
return min
}

3、defer 和追踪

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值)。

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源。

示例:

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 "fmt"

func main() {
function1()
}

func function1() {
fmt.Printf("In function1 at the top\n")
defer function2()
fmt.Printf("In function1 at the bottom!\n")
}

func function2() {
fmt.Printf("function2: Deferred until the end of the calling function!")
}


输出:

In Function1 at the top
In Function1 at the bottom!
Function2: Deferred until the end of the calling function!

使用 defer 的语句同样可以接受参数,下面这个例子就会在执行 defer 语句时打印 0:

1
2
3
4
5
6
func a() {
i := 0
defer fmt.Println(i)
i++
return
}

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出):

1
2
3
4
5
6
7
8
9
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
}

输出:

4 3 2 1 0

1)使用 defer 语句实现代码追踪

一个基础但十分实用的实现代码执行追踪的方案就是在进入和离开某个函数打印相关的消息,即可以提炼为下面两个函数:

1
2
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

func a() {
trace("a")
defer untrace("a")
fmt.Println("in a")
}

func b() {
trace("b")
defer untrace("b")
fmt.Println("in b")
a()
}

func main() {
b()
}

2)使用 defer 语句来记录函数的参数与返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"io"
"log"
)

func func1(s string) (n int, err error) {
defer func() {
log.Printf("func1(%q) = %d, %v", s, n, err)
}()
return 7, io.EOF
}

func main() {
func1("Go")
}

输出:

Output: 2011/10/04 10:46:11 func1("Go") = 7, EOF

4、内置函数

名称 说明
close 用于管道通信
len、cap len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
new、make new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new (type)、make (type)。new (T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)。make (T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作 new () 是一个函数,不要忘记它的括号
copy、append 用于复制和连接切片
panic、recover 两者均用于错误处理机制
print、println 底层打印函数,在部署环境中建议使用 fmt 包
complex、real、imag 用于创建和操作复数

5、将函数作为参数

函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。

例子如下:

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() {
callback(1, Add)
}

func Add(a, b int) {
fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}

func callback(y int, f func(int, int)) {
f(y, 2) // this becomes Add(1, 2)
}

输出:

The sum of 1 and 2 is: 3

6、匿名函数(闭包,函数式语言的术语)

当我们不希望给函数起名字的时候,可以使用匿名函数,例如:

1
2
3
4
5
6
7
func(x, y int) int { return x + y }。

这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:

fplus := func(x, y int) int { return x + y }

然后通过变量名对函数进行调用:fplus(3,4)。

当然,也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)。

具体语法如下所示:

1
2
3
4
5
6
func() {
sum := 0
for i := 1; i <= 1e6; i++ {
sum += i
}
}()

表示参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。

具体例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
f()
}
func f() {
for i := 0; i < 4; i++ {
g := func(i int) { fmt.Printf("%d ", i) } //此例子中只是为了演示匿名函数可分配不同的内存地址,在现实开发中,不应该把该部分信息放置到循环中。
g(i)
fmt.Printf(" - g is of type %T and has value %v\n", g, g)
}
}

我们可以看到变量 g 代表的是 func(int),变量的值是一个内存地址。

所以我们实际上拥有的是一个函数值:匿名函数可以被赋值给变量并作为值使用。

二、数组与切片

1、数组(概念和java相同,略过)

Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建: var arr1 = new([5]int)。

那么这种方式和 var arr2 [5]int 的区别是什么呢?arr1 的类型是 *[5]int,而 arr2 的类型是 [5]int。

这样的结果就是当把一个数组赋值给另一个时,需要再做一次数组内存的拷贝操作。例如:

1
2
arr2 := *arr1
arr2[2] = 100

这样两个数组就有了不同的值,在赋值后修改 arr2 不会对 arr1 生效。

所以在函数中数组作为参数传入时,如 func1(arr2),会产生一次数组拷贝,func1 方法不会修改原始的数组 arr2。

如果你想修改原数组,那么 arr2 必须通过 & 操作符以引用方式传过来,例如 func1 (&arr2),例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func f(a [3]int) {
fmt.Println(a)
a[0] = 1
}

func fp(a *[3]int) {
fmt.Println(a)
a[0] = 1
}

func main() {
var ar [3]int
f(ar) // passes a copy of ar
fp(&ar) // passes a pointer to ar
fmt.Println(ar)
}

1)数组初始化

例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
// var arrAge = [5]int{18, 20, 15, 22, 16}
// var arrLazy = [...]int{5, 6, 7, 8, 22}
// var arrLazy = []int{5, 6, 7, 8, 22}
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
// var arrKeyValue = []string{3: "Chris", 4: "Ron"}

for i := 0; i < len(arrKeyValue); i++ {
fmt.Printf("Person at %d is %s\n", i, arrKeyValue[i])
}
}

2)将数组传递给函数

把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:

  • 传递数组的指针
  • 使用数组的切片
传递数组的指针

例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
array := [3]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
// to pass a pointer to the array
fmt.Printf("The sum of the array is: %f", x)
}

func Sum(a *[3]float64) (sum float64) {
for _, v := range *a { // derefencing *a to get back to the array is not necessary!
sum += v
}
return
}

输出结果:

The sum of the array is: 24.600000

但这在 Go 中并不常用,通常使用切片。

使用数组的切片

例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
array := [3]float64{7.0, 8.5, 9.1}
x := sum(array[:])
fmt.Printf("The sum of the array is: %f", x)
}

func sum(a []float64) float64 {
s := 0.0
for i := 0; i < len(a); i++ {
s += a[i]
}
return s
}

2、切片

1)概念

切片(slice)是对数组一个 连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以 切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。

特点如下所示:

  • 切片是一个引用类型
  • 切片是可索引的,并且可以由 len() 函数获取长度。
  • 给定项的切片索引可能比相关数组的相同元素的索引小。
  • 和数组不同的是,切片的长度可以在运行时修改,最小为 0 ,最大为相关数组的长度:切片是一个长度可变的数组。
  • 切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片从第一个元素开始,到相关数组末尾的元素个数。对于 切片 s 来说该不等式永远成立:0 <= len(s) <= cap(s)。
  • 多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。

声明切片的格式,如下所示:

1
var identifier []type(不需要说明长度)

一个切片在未初始化之前默认为 nil,长度为 0。

切片的初始化格式,如下所示:

1
var slice1 []type = arr1[start:end]

这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 slice1[0] 就等于 arr1[start]。这可以在 arr1 被填充前就定义好。

如果某个人写:var slice1 []type = arr1[:] 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是 arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是:slice1 = &arr1。

arr1[2:] 和 arr1[2:len(arr1)] 相同,都包含了数组从第三个到最后的所有元素。

arr1[:3] 和 arr1[0:3] 相同,包含了从第一个到第三个元素(不包括第四个 / 不包含下标为三的元素)。

如果你想去掉 slice1 的最后一个元素,只要 slice1 = slice1[:len(slice1)-1]。

一个由数字 1、2、3 组成的切片可以这么生成:s := [3]int{1,2,3}[:] 甚至更简单的 s := []int{1,2,3}。

s2 := s[:] 是用切片组成的切片,拥有相同的元素,但是 仍然指向相同的相关数组。

一个切片 s 可以这样扩展到它的大小上限:s = s[:cap(s)],如果再扩大的话就会导致运行时错误。

切片也可以用类似数组的方式初始化:var x = []int{2, 3, 5, 7, 11}。这样就创建了一个长度为 5 的数组并且创建了一个相关切片。

2)切片的内存结构

切片在内存中的组织方式实际上是一个有 3 个域的结构体:

  • 指向相关数组的指针
  • 切片长度:切片的长度就是它所包含的元素个数。
  • 切片容量:切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。

一个长度为 2,容量为 4 的切片 y,如下所示:

切片内存结构

1
y[0] = 3 且 y[1] = 5。

切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。

注意: 绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!

3)创建切片

当相关数组还没有定义时,我们可以使用 make () 函数来创建一个切片 同时创建好相关数组:var slice1 []type = make([]type, len)。

也可以简写为 slice1 := make([]type, len),这里 len 是数组的长度并且也是 slice 的初始长度。

所以定义 s2 := make([]int, 10),那么 cap(s2) == len(s2) == 10。

make 接受 2 个参数:元素的类型以及切片的元素个数。

如果你想创建一个 slice1,它不占用整个数组,而只是占用以 len 为个数个项,那么只要:slice1 := make([]type, len, cap)。

make 的使用方式是:func make([]T, len, cap),其中 cap 是可选参数。

所以下面两种方法可以生成相同的切片:

1
2
make([]int, 50, 100)
new([100]int)[0:50]

使用 make 方法生成的切片的内存结构,如下图所示:

make方法生成的切片的内存结构

例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"

func main() {
var slice1 []int = make([]int, 10)
// load the array/slice:
for i := 0; i < len(slice1); i++ {
slice1[i] = 5 * i
}

// print the slice:
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
fmt.Printf("\nThe length of slice1 is %d\n", len(slice1))
fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
}

4)new () 和 make () 的区别

看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。

  • new (T) 为每个新的类型 T 分配一片内存,初始化为 0 并且返回类型为 * T 的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
  • make(T) 返回一个类型为 T 的初始值,它只适用于 3 种内建的引用类型:切片、map 和 channel。

换言之,new 函数分配内存,make 函数初始化;下图给出了区别:

new和make的区别

5)多维切片

Go语言中同样允许使用多维切片,声明一个多维数组的语法格式如下:

1
var sliceName [][]...[]sliceType

下面以二维切片为例,声明一个二维切片并赋值,代码如下所示:

1
2
3
4
5
6
7
8
9
//声明一个二维切片
var slice [][]int
//为二维切片赋值
slice = [][]int{{10}, {100, 200}}
上面的代码也可以简写为下面的样子。
// 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)

6)遍历切片

语法如下所示:

1
2
3
for ix, value := range slice1 {
...
}

例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
var slice1 []int = make([]int, 4)

slice1[0] = 1
slice1[1] = 2
slice1[2] = 3
slice1[3] = 4

for ix, value := range slice1 {
fmt.Printf("Slice at %d is: %d\n", ix, value)
}

// 如果你只需要索引,你可以忽略第二个变量
for ix := range slice1 {
fmt.Printf("%d", ix)
}
}

遍历多维切片:

1
2
3
4
5
for row := range screen {
for column := range screen[row] {
screen[row][column] = 1
}
}

7)切片的追加

1
func append(s[]T, x ...T) []T

其中 append 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。append 方法总是返回成功,除非系统内存耗尽了。

如果你想将切片 y 追加到切片 x 后面,只要将第二个参数扩展成一个列表即可:x = append(x, y…)。

例子如下所示:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
sl3 := []int{1, 2, 3}
sl3 = append(sl3, 4, 5, 6)
fmt.Println(sl3)
}

8)切片的复制

1
func copy(dst, src []T) int

copy 方法将类型为 T 的切片从源地址 src 拷贝到目标地址 dst,覆盖 dst 的相关元素,并且返回拷贝的元素个数。

源地址和目标地址可能会有重叠。拷贝个数是 src 和 dst 的长度最小值。如果 src 是字符串那么元素类型就是 byte。如果你还想继续使用 src,在拷贝结束后执行 src = dst。

例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
sl_from := []int{1, 2, 3}
sl_to := make([]int, 10)

n := copy(sl_to, sl_from)
fmt.Println(sl_to)
fmt.Printf("Copied %d elements\n", n) // n == 3

st := "stest"
st_arr := make([]byte, 100)
copy(st_arr, st)

test := string(st_arr)
fmt.Println(test)
}

三、Map

map 是引用类型,可以使用如下声明:

1
2
3
4
var map1 map[keytype]valuetype

例如:
var map1 map[string]int
  • 在声明的时候不需要知道 map 的长度,map 是可以动态增长的。
  • 未初始化的 map 的值是 nil。
  • key 可以是任意可以用 == 或者!= 操作符比较的类型,比如 string、int、float。
  • 切片和结构体不能作为 key (译者注:含有数组切片的结构体不能作为 key,只包含内建类型的 struct 是可以作为 key 的,但是指针和接口类型可以。)
  • value 可以是任意类型的;通过使用空接口类型,我们可以存储任意值,但是使用这种类型作为值时需要先做一次类型断言。
  • map 传递给函数的代价很小:在 32 位机器上占 4 个字节,64 位机器上占 8 个字节,无论实际上存储了多少数据。
  • map 也可以用函数作为自己的值,这样就可以用来做分支结构:key 用来选择要执行的函数。

1)map初始化

1
2
3
4
5
6
7
8
var map1 = make(map[keytype]valuetype)

或者简写为:map1 := make(map[keytype]valuetype)

例如:
mapCreated := make(map[string]float32)

相当于:mapCreated := map[string]float32{}。

注意:不要使用 new,永远用 make 来构造 map

1
2
3
4
5
6
7
如果你错误的使用 new () 分配了一个引用对象,你会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:

mapCreated := new(map[string]float32)

接下来当我们调用:mapCreated["key1"] = 4.5 的时候,编译器会报错:

invalid operation: mapCreated["key1"] (index of type *map[string]float32).

2)map容量

和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。

但是你也可以选择标明 map 的初始容量 capacity,就像这样:make(map[keytype]valuetype, cap)。例如:

1
map2 := make(map[string]float32, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value 对,map 的大小会自动加 1。所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明

3)用切片作为 map 的值

既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?例如,当我们要处理 unix 机器上的所有进程,以父进程(pid 为整型)作为 key,所有的子进程(以所有子进程的 pid 组成的切片)作为 value。通过将 value 定义为 []int 类型或者其他类型的切片,就可以优雅的解决这个问题。

例子如下所示:

1
2
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

4)测试键值对是否存在及删除元素

1
val1, isPresent = map1[key1]

isPresent 返回一个 bool 值:如果 key1 存在于 map1,val1 就是 key1 对应的 value 值,并且 isPresent 为 true;如果 key1 不存在,val1 就是一个空值,并且 isPresent 会返回 false。

如果你只是想判断某个 key 是否存在而不关心它对应的值到底是多少,你可以这么做:

1
_, ok := map1[key1] // 如果key1存在则ok == true,否则ok为false

或者和 if 混合使用:

1
2
3
if _, ok := map1[key1]; ok {
// ...
}

从 map1 中删除 key1:

1
delete(map1, key1)

如果 key1 不存在,该操作不会产生错误。

例子如下所示:

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
package main
import "fmt"

func main() {
var value int
var isPresent bool

map1 := make(map[string]int)
map1["New Delhi"] = 55
map1["Beijing"] = 20
map1["Washington"] = 25
value, isPresent = map1["Beijing"]
if isPresent {
fmt.Printf("The value of \"Beijing\" in map1 is: %d\n", value)
} else {
fmt.Printf("map1 does not contain Beijing")
}

value, isPresent = map1["Paris"]
fmt.Printf("Is \"Paris\" in map1 ?: %t\n", isPresent)
fmt.Printf("Value is: %d\n", value)

// delete an item:
delete(map1, "Washington")
value, isPresent = map1["Washington"]
if isPresent {
fmt.Printf("The value of \"Washington\" in map1 is: %d\n", value)
} else {
fmt.Println("map1 does not contain Washington")
}
}

输出结果:

The value of "Beijing" in map1 is: 20
Is "Paris" in map1 ?: false
Value is: 0
map1 does not contain Washington

5)for-range 的配套用法

遍历 map:

1
2
3
for key, value := range map1 {
...
}

第一个返回值 key 是 map 中的 key 值,第二个返回值则是该 key 对应的 value 值;这两个都是仅 for 循环内部可见的局部变量。其中第一个返回值 key 值是一个可选元素。如果你只关心值,可以这么使用:

1
2
3
for _, value := range map1 {
...
}

如果只想获取 key,你可以这么使用:

1
2
3
for key := range map1 {
fmt.Printf("key is: %d\n", key)
}

示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
for key, value := range map1 {
fmt.Printf("key is: %d - value is: %f\n", key, value)
}
}

输出结果:

key is: 3 - value is: 3.000000
key is: 1 - value is: 1.000000
key is: 4 - value is: 4.000000
key is: 2 - value is: 2.000000

注意:map 不是按照 key 的顺序排列的,也不是按照 value 的序排列的。

6)map 的排序

map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序。

如果你想为 map 排序,需要将 key(或者 value)拷贝到一个切片,再对切片排序,然后可以使用切片的 for-range 方法打印出所有的 key 和 value。

例子如下所示:

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
package main
import (
"fmt"
"sort"
)

var (
barVal = map[string]int{"alpha": 34, "bravo": 56, "charlie": 23,
"delta": 87, "echo": 56, "foxtrot": 12,
"golf": 34, "hotel": 16, "indio": 87,
"juliet": 65, "kili": 43, "lima": 98}
)

func main() {
fmt.Println("unsorted:")
for k, v := range barVal {
fmt.Printf("Key: %v, Value: %v / ", k, v)
}
keys := make([]string, len(barVal))
i := 0
for k, _ := range barVal {
keys[i] = k
i++
}
sort.Strings(keys)
fmt.Println()
fmt.Println("sorted:")
for _, k := range keys {
fmt.Printf("Key: %v, Value: %v / ", k, barVal[k])
}
}

输出结果:

unsorted:
Key: bravo, Value: 56 / Key: echo, Value: 56 / Key: indio, Value: 87 / Key: juliet, Value: 65 / Key: alpha, Value: 34 / Key: charlie, Value: 23 / Key: delta, Value: 87 / Key: foxtrot, Value: 12 / Key: golf, Value: 34 / Key: hotel, Value: 16 / Key: kili, Value: 43 / Key: lima, Value: 98 /
sorted:
Key: alpha, Value: 34 / Key: bravo, Value: 56 / Key: charlie, Value: 23 / Key: delta, Value: 87 / Key: echo, Value: 56 / Key: foxtrot, Value: 12 / Key: golf, Value: 34 / Key: hotel, Value: 16 / Key: indio, Value: 87 / Key: juliet, Value: 65 / Key: kili, Value: 43 / Key: lima, Value: 98 /

7)map 类型的切片

假设我们想获取一个 map 类型的切片,我们必须使用两次 make() 函数,第一次分配切片,第二次分配 切片中每个 map 元素。例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"

func main() {
// Version A:
items := make([]map[int]int, 5)
for i:= range items {
items[i] = make(map[int]int, 1)
items[i][1] = 2
}
fmt.Printf("Version A: Value of items: %v\n", items)

// Version B: NOT GOOD!
items2 := make([]map[int]int, 5)
for _, item := range items2 {
item = make(map[int]int, 1) // item 只是切片元素的副本。
item[1] = 2 // 这个“项目”将在下一次迭代中丢失。
}
fmt.Printf("Version B: Value of items: %v\n", items2)
}

四、结构体

结构体定义的一般方式如下:

1
2
3
4
5
type identifier struct {
field1 type1
field2 type2
...
}

使用 new 创建一个结构体:

1
2
3
4
5
6
7
8
9
第一种:
var t *T = new(T)

第二种:
var t *T
t = new(T)

第三种:
t := new(T)

上面的语句表示,变量 t 是一个指向 T 的指针,此时结构体字段的值是它们所属类型的零值。

声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型 T。在这几种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。

例子如下所示:

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
package main
import "fmt"

type struct1 struct {
i1 int
f1 float32
str string
}

func main() {
ms := new(struct1)
ms.i1 = 10
ms.f1 = 15.5
ms.str= "Chris"

fmt.Printf("The int is: %d\n", ms.i1)
fmt.Printf("The float is: %f\n", ms.f1)
fmt.Printf("The string is: %s\n", ms.str)
fmt.Println(ms)
}

输出:

The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}

就像在面向对象语言所作的那样,可以使用点号符给字段赋值:structname.fieldname = value。

同样的,使用点号符可以获取结构体字段的值:structname.fieldname。

在 Go 语言中这叫 选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation) 来引用结构体的字段:

1
2
3
4
5
type myStruct struct { i int }
var v myStruct // v是结构体类型变量
var p *myStruct // p是指向一个结构体类型变量的指针
v.i
p.i

1)初始化一个结构体

1
2
3
4
5
// 此时ms的类型是 *struct1
ms := &struct1{10, 15.5, "Chris"}

var ms struct1
ms = struct1{10, 15.5, "Chris"}

混合字面量语法(composite literal syntax)&struct1{a, b, c} 是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。

表达式 new(Type) 和 &Type{} 是等价的。

例子如下所示:

1
2
3
4
5
6
7
8
9
10
type Interval struct {
start int
end int
}

初始化方式:

intr := Interval{0, 3} (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)

2)结构体类型的内存布局

1
type Point struct { x, y int }

使用 new 初始化:

使用new初始化

作为结构体字面量初始化:

作为结构体字面量初始化

3)三种不同的调用方式

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
package main
import (
"fmt"
"strings"
)

type Person struct {
firstName string
lastName string
}

func upPerson(p *Person) {
p.firstName = strings.ToUpper(p.firstName)
p.lastName = strings.ToUpper(p.lastName)
}

func main() {
// 1-struct as a value type:
var pers1 Person
pers1.firstName = "Chris"
pers1.lastName = "Woodward"
upPerson(&pers1)
fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)

// 2—struct as a pointer:
pers2 := new(Person)
pers2.firstName = "Chris"
pers2.lastName = "Woodward"
(*pers2).lastName = "Woodward" // 这是合法的
upPerson(pers2)
fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)

// 3—struct as a literal:
pers3 := &Person{"Chris","Woodward"}
upPerson(pers3)
fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}

输出结果:

The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD

4)结构体转换

Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型时,此结构体类型和它的 alias 类型都有相同的底层类型,它们可以互相转换,同时需要注意其中非法赋值或转换引起的编译错误。

例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"

type number struct {
f float32
}

type nr number // alias type

func main() {
a := number{5.0}
b := nr{5.0}
// var i float32 = b // compile-error: cannot use b (type nr) as type float32 in assignment
// var i = float32(b) // compile-error: cannot convert b (type nr) to type float32
// var c number = b // compile-error: cannot use b (type nr) as type number in assignment
// needs a conversion:
var c = number(b)
fmt.Println(a, b, c)
}

输出:

{5} {5} {5}

5)使用工厂方法创建结构体实例

Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂” 方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 new 或 New 开头。假设定义了如下的 File 结构体类型:

1
2
3
4
type File struct {
fd int // 文件描述符
name string // 文件名
}

下面是这个结构体类型对应的工厂方法,它返回一个指向结构体实例的指针:

1
2
3
4
5
6
7
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}

return &File{fd, name}
}

然后这样调用它:

1
f := NewFile(10, "./test.txt")

在 Go 语言中常常像上面这样在工厂方法里使用初始化来简便的实现构造函数。

如果 File 是一个结构体类型,那么表达式 new(File) 和 &File{} 是等价的。

我们可以说是工厂实例化了类型的一个对象,就像在基于类的 OOP 语言中那样。

如果想知道结构体类型 T 的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})。

5)如何强制使用工厂方法

通过应用可见性规则就可以禁止使用 new 函数,强制用户使用工厂方法,从而使类型变成私有的,就像在面向对象语言中那样。

例子如下所示:

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

type matrix struct {
id int
name string
}

func NewMatrix(id int, name string) *matrix {
m := matrix{id, name}
return &m
}

在其他包里使用工厂方法:

package main

import (
"fmt"
"test1/main/factory"
)

func main() {
//wrong := new(matrix.matrix) // 编译失败(matrix 是私有的)
right := factory.NewMatrix(1, "tst") // 实例化 matrix 的唯一方式

fmt.Println(right)
}

下面的例子说明了在映射上使用 new 和 make 的区别以及可能发生的错误:

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

type Foo map[string]string
type Bar struct {
thingOne string
thingTwo int
}

func main() {
// OK
y := new(Bar)
(*y).thingOne = "hello"
(*y).thingTwo = 1

// NOT OK
z := make(Bar) // 编译错误:cannot make type Bar
(*z).thingOne = "hello"
(*z).thingTwo = 1

// OK
x := make(Foo)
x["x"] = "goodbye"
x["y"] = "world"

// NOT OK
u := new(Foo)
(*u)["x"] = "goodbye" // 运行时错误!! panic: assignment to entry in nil map
(*u)["y"] = "world"
}

试图 make() 一个结构体变量,会引发一个编译错误,这还不是太糟糕,但是 new() 一个映射并试图使用数据填充它,将会引发运行时错误! 因为 new(Foo) 返回的是一个指向 nil 的指针,它尚未被分配内存。所以在使用 map 时要特别谨慎。

6)带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。

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

import (
"fmt"
"reflect"
)

type TagType struct { // tags
field1 bool "An important answer"
field2 string "The name of the thing"
field3 int "How much there are"
}

func main() {
tt := TagType{true, "Barak Obama", 1}
for i := 0; i < 3; i++ {
refTag(tt, i)
}
}

func refTag(tt TagType, ix int) {
ttType := reflect.TypeOf(tt)
ixField := ttType.Field(ix)
fmt.Printf("%v\n", ixField.Tag)
}

输出:

An important answer
The name of the thing
How much there are

7)匿名字段和内嵌结构体

结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。

例子如下所示:

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"

type innerS struct {
in1 int
in2 int
}

type outerS struct {
b int
c float32
int // anonymous field
innerS //anonymous field
}

func main() {
outer := new(outerS)
outer.b = 6
outer.c = 7.5
outer.int = 60
outer.in1 = 5
outer.in2 = 10

fmt.Printf("outer.b is: %d\n", outer.b)
fmt.Printf("outer.c is: %f\n", outer.c)
fmt.Printf("outer.int is: %d\n", outer.int)
fmt.Printf("outer.in1 is: %d\n", outer.in1)
fmt.Printf("outer.in2 is: %d\n", outer.in2)

// 使用结构体字面量
outer2 := outerS{6, 7.5, 60, innerS{5, 10}}
fmt.Println("outer2 is:", outer2)
fmt.Println("outer2.innerS is:", outer2.innerS)
}

输出:

outer.b is: 6
outer.c is: 7.500000
outer.int is: 60
outer.in1 is: 5
outer.in2 is: 10
outer2 is: {6 7.5 60 {5 10}}
outer2.innerS is: {5 10}

通过类型 outer.int 的名字来获取存储在匿名字段中的数据,于是可以得出一个结论:在一个结构体中对于每一种数据类型只能有一个匿名字段。

8)命名冲突

当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢?

  • 外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式;
  • 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。

例子如下所示:

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 "fmt"

type A struct{ a int }
type B struct{ a, b int }
type D struct {
B
b int
}

type C struct {
A
B
}

func main() {
c := C{A{1}, B{2, 3}}
//fmt.Println(c.a) // ambiguous selector c.a 编译错误
fmt.Println(c.A.a)
d := D{B{1, 3}, 2}
fmt.Println(d.b) // 这个是允许的,会加载 D.b
fmt.Println(d.B.b)
}

五、方法

Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。

定义方法的一般格式如下:

1
2
3
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

recv 就像是面向对象语言中的 this 或 self,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this 或 self 作为 receiver 的名字。

接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。

但是接收者不能是一个接口类型,因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:invalid receiver type…。

最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。

一个类型加上它的方法等价于面向对象中的一个类。

一个重要的区别是:在 Go 中,类型的代码 和绑定在它上面的 方法的代码 可以不放置在一起,它们 可以存在在不同的源文件,唯一的要求是: 它们必须是同一个包的。

因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:

1
2
func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix

例子如下所示:

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

import "fmt"

type TwoInts struct {
a int
b int
}

// AddThem 结构体上的简单方法的例子
func (tn *TwoInts) AddThem() int {
return tn.a + tn.b
}

// AddToParam 结构体上的简单方法的例子
func (tn *TwoInts) AddToParam(param int) int {
return tn.a + tn.b + param
}

type IntVector []int

// Sum 非结构体类型上方法的例子
func (v IntVector) Sum() (s int) {
for _, x := range v {
s += x
}
return
}

func main() {
two1 := new(TwoInts)
two1.a = 12
two1.b = 10

fmt.Printf("The sum is: %d\n", two1.AddThem())
fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))

two2 := TwoInts{3, 4}
fmt.Printf("The sum is: %d\n", two2.AddThem())

fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6
}

错误示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "container/list"

func (p *list.List) Iter() {
// ...
}

func main() {
lst := new(list.List)
for _= range lst.Iter() {
}
}

编译错误:cannot define new methods on non-local type list.List

类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法。

但是有一个间接的方式:可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。

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

import (
"fmt"
"time"
)

type myTime struct {
time.Time //anonymous field
}

func (t myTime) first3Chars() string {
return t.Time.String()[0:3]
}
func main() {
m := myTime{time.Now()}
// 调用匿名Time上的String方法
fmt.Println("Full time now:", m.String())
// 调用myTime.first3Chars
fmt.Println("First 3 chars:", m.first3Chars())
}

输出:

Full time now: 2022-06-09 14:09:56.3957554 +0800 CST m=+0.025340201
First 3 chars: 202

1)函数和方法的区别

函数将变量作为参数:Function1(recv)

方法在变量上被调用:recv.Method1()

在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。

不要忘记 Method1 后边的括号 (),否则会引发编译器错误:method recv.Method1 is not an expression, must be called

接收者必须有一个显式的名字,这个名字必须在方法中被使用。

receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。

在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。

方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。

2)指针或值作为接收者

鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。

如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。

例子如下所示:

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

import (
"fmt"
)

type B struct {
thing int
}

func (b *B) change() { b.thing = 1 }

func (b B) write() string { return fmt.Sprint(b) }

func main() {
var b1 B // b1是值
b1.change()
fmt.Println(b1.write())

b2 := new(B) // b2是指针
b2.change()
fmt.Println(b2.write())
}

输出:
{1}
{1}

指针方法和值方法都可以在指针或非指针上被调用。

如下面程序所示,类型 List 在值上有一个方法 Len(),在指针上有一个方法 Append(),但是可以看到两个方法都可以在两种类型的变量上被调用。

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"
)

type List []int

func (l List) Len() int { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }

func main() {
// 值
var lst List
lst.Append(1)
fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1)

// 指针
plst := new(List)
plst.Append(2)
fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1)
}

3)方法和未导出字段

如果类型 Person 被明确的导出了,但是它的字段没有被导出。则 p.firstName 就是错误的。该如何在另一个程序中修改或者只是读取一个 Person 的名字呢?

这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package person

type Person struct {
firstName string
lastName string
}

func (p *Person) FirstName() string {
return p.firstName
}

func (p *Person) SetFirstName(newName string) {
p.firstName = newName
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"./person"
"fmt"
)

func main() {
p := new(person.Person)
// p.firstName undefined
// (cannot refer to unexported field or method firstName)
// p.firstName = "Eric"
p.SetFirstName("Eric")
fmt.Println(p.FirstName()) // Output: Eric
}

4)内嵌类型的方法和继承

当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 继承 了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果,也类似 Ruby 中的混入(mixin)。

例子如下所示:

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 (
"fmt"
"math"
)

type Point struct {
x, y float64
}

func (p *Point) Abs() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}

type NamedPoint struct {
Point
name string
}

func main() {
n := &NamedPoint{Point{3, 4}, "Pythagoras"}
fmt.Println(n.Abs()) // 打印5
}

内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法 “晋升” 成为了外层类型的方法。

当然类型可以有只作用于本身实例而不作用于内嵌 “父” 类型上的方法,可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。

基于上面的代码,加上如下方法:

1
2
3
4
5
func (n *NamedPoint) Abs() float64 {
return n.Point.Abs() * 100.
}

现在 fmt.Println(n.Abs()) 会打印 500。

结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。

5)如何在类型中嵌入功能

例子如下所示:

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

import (
"fmt"
)

type Log struct {
msg string
}

type Customer struct {
Name string
log *Log
}

func main() {
// shorter
c := &Customer{"Barak Obama", &Log{"1 - Yes we can!"}}
// fmt.Println(c) &{Barak Obama 1 - Yes we can!}
c.Log().Add("2 - After me the world will be a better place!")
//fmt.Println(c.log)
fmt.Println(c.Log())

}

func (l *Log) Add(s string) {
l.msg += "\n" + s
}

func (l *Log) String() string {
return l.msg
}

func (c *Customer) Log() *Log {
return c.log
}

输出:

1 - Yes we can!
2 - After me the world will be a better place!

6)多重继承

多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++ 和 Python 例外)。

因为在类继承层次中,多重继承会给编译器引入额外的复杂度。

但是在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。

作为一个例子,假设有一个类型 CameraPhone,通过它可以 Call(),也可以 TakeAPicture(),但是第一个方法属于类型 Phone,第二个方法属于类型 Camera。

只要嵌入这两个类型就可以解决问题,如下所示:

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

import (
"fmt"
)

type Camera struct{}

func (c *Camera) TakeAPicture() string {
return "Click"
}

type Phone struct{}

func (p *Phone) Call() string {
return "Ring Ring"
}

type CameraPhone struct {
Camera
Phone
}

func main() {
cp := new(CameraPhone)
fmt.Println("Our new CameraPhone exhibits multiple behaviors...")
fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture())
fmt.Println("It works like a Phone too: ", cp.Call())
}

输出:
Our new CameraPhone exhibits multiple behaviors...
It exhibits behavior of a Camera: Click
It works like a Phone too: Ring Ring

7)类型的 String() 方法和格式化描述符

如果类型定义了 String() 方法,它会被用在 fmt.Printf() 中生成默认的输出:等同于使用格式化描述符 %v 产生的输出。还有 fmt.Print() 和 fmt.Println() 也会自动使用 String() 方法。

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 main

import (
"fmt"
"strconv"
)

type TwoInts struct {
a int
b int
}

func main() {
two1 := new(TwoInts)
two1.a = 12
two1.b = 10
fmt.Printf("two1 is: %v\n", two1)
fmt.Println("two1 is:", two1)
fmt.Printf("two1 is: %T\n", two1)
fmt.Printf("two1 is: %#v\n", two1)
}

func (tn *TwoInts) String() string {
return "(" + strconv.Itoa(tn.a) + "/" + strconv.Itoa(tn.b) + ")"
}

输出:

two1 is: (12/10)
two1 is: (12/10)
two1 is: *main.TwoInts
two1 is: &main.TwoInts{a:12, b:10}

从上面的例子也可以看到,格式化描述符 %T 会给出类型的完全规格,%#v 会给出实例的完整输出,包括它的字段。

注意:不要在 String() 方法里面调用涉及 String() 方法的方法,它会导致意料之外的错误

比如下面的例子,它导致了一个无限迭代(递归)调用(TT.String() 调用 fmt.Sprintf,而 fmt.Sprintf 又会反过来调用 TT.String()…),很快就会导致内存溢出。

1
2
3
4
5
6
type TT float64

func (t TT) String() string {
return fmt.Sprintf("%v", t)
}
t. String()

8)垃圾回收和 SetFinalizer

Go 开发者不需要写代码来释放程序中不再使用的变量和结构占用的内存,在 Go 运行时中有一个独立的进程,即垃圾收集器(GC),会处理这些事情,它搜索不再使用的变量然后释放它们的内存。

通过调用 runtime.GC() 函数可以显式的触发 GC,但这只在某些罕见的场景下才有用,比如当内存资源不足时调用 runtime.GC(),它会在此函数执行的点上立即释放一大片内存,此时程序可能会有短时的性能下降(因为 GC 进程在执行)。

如果想知道当前的内存状态,可以使用:

1
2
3
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d Kb\n", m.Alloc / 1024)

如果需要在一个对象 obj 被从内存移除前执行一些特殊操作,比如写到日志文件中,可以通过如下方式调用函数来实现:

1
runtime.SetFinalizer(obj, func(obj *typeObj))

六、接口

接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

通过如下格式定义接口:

1
2
3
4
5
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}

特点如下:

  • 类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。
  • 实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
  • 一个类型可以实现多个接口。
  • 接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。

例子如下所示:

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 main

import "fmt"

type Shaper interface {
Area() float32
}

type Square struct {
side float32
}

func (sq *Square) Area() float32 {
return sq.side * sq.side
}

func main() {
sq1 := new(Square)
sq1.side = 5

var areaIntf Shaper
areaIntf = sq1
// shorter,without separate declaration:
// areaIntf := Shaper(sq1)
// or even:
// areaIntf := sq1
fmt.Printf("The square has area: %f\n", areaIntf.Area())
}

输出:

The square has area: 25.000000

现在接口变量包含一个指向 Square 变量的引用,通过它可以调用 Square 上的方法 Area()。

当然也可以直接在 Square 的实例上调用此方法,但是在接口实例上调用此方法更令人兴奋,它使此方法更具有一般性。接口变量里包含了接收者实例的值和指向对应方法表的指针。

如果 Square 没有实现 Area() 方法,编译器将会给出清晰的错误信息:

1
2
cannot use sq1 (type *Square) as type Shaper in assignment:
*Square does not implement Shaper (missing Area method)

如果 Shaper 有另外一个方法 Perimeter(),但是 Square 没有实现它,即使没有人在 Square 实例上调用这个方法,编译器也会给出上面同样的错误。

1)接口命名

接口的名字由方法名加 er 后缀组成,例如 Printer、Reader、Writer、Logger、Converter 等等。

还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。

2)接口值

不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个接口值 :

1
2
3
4
5
var ai Namer

ai 是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针。

指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。

3)多态

多态是面向对象编程中一个广为人知的概念:根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上似乎表现出不同的行为。

例子如下所示:

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

import "fmt"

type Shaper interface {
Area() float32
}

type Square struct {
side float32
}

func (sq *Square) Area() float32 {
return sq.side * sq.side
}

type Rectangle struct {
length, width float32
}

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

func main() {
rectangle := Rectangle{3, 5}
square := &Square{5}

shapers := []Shaper{rectangle, square}

for _, shaper := range shapers {
fmt.Println("Shape details:", shaper)
fmt.Println("Area of this shape is:", shaper.Area())
}
}

输出:

Shape details: {3 5}
Area of this shape is: 15
Shape details: &{5}
Area of this shape is: 25

4)接口嵌套接口

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。

例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}

type Lock interface {
Lock()
Unlock()
}

type File interface {
ReadWrite
Lock
Close()
}

5)类型断言:如何检测和转换接口变量的类型

一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。

通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值:

1
2
3
4
5
6
7
8
v := varI.(T)       // unchecked type assertion

if v, ok := varI.(T); ok { // checked type assertion
Process(v)
return
} else {
// varI is not of type T
}

varI 必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left) 。

例子如下所示:

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
46
47
48
package main

import (
"fmt"
"math"
)

type Shaper interface {
Area() float32
}

type Square struct {
side float32
}

type Circle struct {
radius float32
}

func (sq *Square) Area() float32 {
return sq.side * sq.side
}

func (ci *Circle) Area() float32 {
return ci.radius * ci.radius * math.Pi
}

func main() {
var areaIntf Shaper
sq1 := new(Square)
sq1.side = 5

areaIntf = sq1
// Is Square the type of areaIntf?
if t, ok := areaIntf.(*Square); ok {
fmt.Printf("The type of areaIntf is: %T\n", t)
}
if u, ok := areaIntf.(*Circle); ok {
fmt.Printf("The type of areaIntf is: %T\n", u)
} else {
fmt.Println("areaIntf does not contain a variable of type Circle")
}
}

输出:

The type of areaIntf is: *main.Square
areaIntf does not contain a variable of type Circle

5)类型判断:type-switch

接口变量的类型也可以使用一种特殊形式的 switch 来检测:type-switch。

例子如下所示:

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

import "fmt"

func classifier(items ...interface{}) {
for i, x := range items {
switch x.(type) {
case bool:
fmt.Printf("Param #%d is a bool\n", i)
case float64:
fmt.Printf("Param #%d is a float64\n", i)
case int, int64:
fmt.Printf("Param #%d is a int\n", i)
case nil:
fmt.Printf("Param #%d is a nil\n", i)
case string:
fmt.Printf("Param #%d is a string\n", i)
default:
fmt.Printf("Param #%d is unknown\n", i)
}
}
}

func main() {
classifier(13, -14.3, "BELGIUM", complex(1, 2), nil, false)
}

6)测试一个值是否实现了某个接口

假定 v 是一个值,然后我们想测试它是否实现了 Stringer 接口,可以这样做:

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"

type Stringer interface {
String() string
}

type Square struct {
st string
}

func (s *Square) String() string {
return s.st
}

func main() {
var v Stringer = &Square{"value"}
if sv, ok := v.(Stringer); ok {
fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v
}
}

7)使用方法集与接口

作用于变量上的方法实际上是不区分变量到底是指针还是值的。

当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的。

参考下面的代码:

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
46
47
48
49
50
51
52
53
package main

import (
"fmt"
)

type List []int

func (l List) Len() int {
return len(l)
}

func (l *List) Append(val int) {
*l = append(*l, val)
}

type Appender interface {
Append(int)
}

func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}

type Lener interface {
Len() int
}

func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}

func main() {
// A bare value
var lst List
// compiler error:
// cannot use lst (type List) as type Appender in argument to CountInto:
// List does not implement Appender (Append method has pointer receiver)
// CountInto(lst, 1, 10)
if LongEnough(lst) { // VALID:Identical receiver type
fmt.Printf("- lst is long enough\n")
}

// A pointer value
plst := new(List)
CountInto(plst, 1, 10) //VALID:Identical receiver type
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}

在 lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个 Appender,而它的方法 Append 只定义在指针上。 在 lst 上调用 LongEnough 是可以的因为 ‘Len’ 定义在值上。

在 plst 上调用 CountInto 是可以的,因为 CountInto 需要一个 Appender,并且它的方法 Append 定义在指针上。 在 plst 上调用 LongEnough 也是可以的,因为指针会被自动解引用。

将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。

总结:

在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型 P 直接可以辨识的:

  • 指针方法可以通过指针调用
  • 值方法可以通过值调用
  • 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
  • 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址

Go 语言规范定义了接口方法集的调用规则:

1
2
3
类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
类型 T 的可调用方法集包含接受者为 T 的所有方法
类型 T 的可调用方法集不包含接受者为 *T 的方法

8)相同方法集的接口

如果有多个接口,且这些接口里的方法定义相同,则类型可以互相转换

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

import (
"fmt"
"sort"
)

type day struct {
num int
shortName string
longName string
}

type dayArray struct {
data []*day
}

func (p *dayArray) Len() int { return len(p.data) }
func (p *dayArray) Less(i, j int) bool { return p.data[i].num < p.data[j].num }
func (p *dayArray) Swap(i, j int) { p.data[i], p.data[j] = p.data[j], p.data[i] }

type Sorter interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

func main() {
var c sort.Interface = new(dayArray)

if _, ok := c.(sort.Interface); ok {
fmt.Println("c implements sort.Interface")
}

if _, ok := c.(Sorter); ok {
fmt.Println("c implements Sorter")
}
}

输出:
c implements sort.Interface
c implements Sorter

9)使用 sort.Interface 接口排序

一个很好的例子是来自标准库的 sort 包,要对一组数字或字符串排序,只需要实现三个方法:

  • 反映元素个数的 Len() 方法
  • 比较第 i 和 j 个元素的 Less(i, j) 方法
  • 交换第 i 和 j 个元素的 Swap(i, j) 方法

例子如下所示:

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

import (
"fmt"
"sort"
)

type day struct {
num int
shortName string
longName string
}

type dayArray struct {
data []*day
}

func (p *dayArray) Len() int { return len(p.data) }
func (p *dayArray) Less(i, j int) bool { return p.data[i].num < p.data[j].num }
func (p *dayArray) Swap(i, j int) { p.data[i], p.data[j] = p.data[j], p.data[i] }

func days() {
Sunday := day{0, "SUN", "Sunday"}
Monday := day{1, "MON", "Monday"}
Tuesday := day{2, "TUE", "Tuesday"}
Wednesday := day{3, "WED", "Wednesday"}
Thursday := day{4, "THU", "Thursday"}
Friday := day{5, "FRI", "Friday"}
Saturday := day{6, "SAT", "Saturday"}
data := []*day{&Tuesday, &Thursday, &Wednesday, &Sunday, &Monday, &Friday, &Saturday}
a := dayArray{data}
sort.Sort(&a)
if !sort.IsSorted(&a) {
panic("fail")
}
for _, d := range data {
fmt.Printf("%s ", d.longName)
}
fmt.Printf("\n")
}

func main() {
days()
}

10)Go 中的面向对象

我们总结一下前面看到的:Go 没有类,而是松耦合的类型、方法对接口的实现。

OO(面向对象) 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢?

封装(数据隐藏)

Go 和别的 OO(面向对象) 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层(Go可见性规则):

  • 1)包范围内的:通过标识符首字母小写,对象只在它所在的包内可见

  • 2)可导出的:通过标识符首字母大写,对象对所在包以外也可见

类型只拥有自己所在包中定义的方法

  • 继承:用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现
  • 多态:用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且:接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。

11)接口与动态类型

在经典的面向对象语言(像 C++,Java 和 C#)中数据和方法被封装为 类 的概念:类包含它们两者,并且不能剥离。

Go 没有类:数据(结构体或更一般的类型)和方法是一种松耦合的正交关系。

Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的实现。但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。

和其它语言相比,Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。 该特性允许我们在不改变已有的代码的情况下定义和使用新接口。

接收一个(或多个)接口类型作为参数的函数,其实参数可以是任何实现了该接口的类型。实现了某个接口的类型可以被传给任何以此接口为参数的函数。

类似于 Python 和 Ruby 这类动态语言中的 动态类型(duck typing);这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。

例子如下所示:

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

import "fmt"

type IDuck interface {
Quack()
Walk()
}

func DuckDance(duck IDuck) {
for i := 1; i <= 3; i++ {
duck.Quack()
duck.Walk()
}
}

type Bird struct {
// ...
}

func (b *Bird) Quack() {
fmt.Println("I am quacking!")
}

func (b *Bird) Walk() {
fmt.Println("I am walking!")
}

func main() {
b := new(Bird)
DuckDance(b)
}

输出:

I am quacking!
I am walking!
I am quacking!
I am walking!
I am quacking!
I am walking!

动态方法调用

像 Python,Ruby 这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后在运行时才解析(它们很可能有像 responds_to 这样的方法来检查对象是否可以响应某个方法,但是这也意味着更大的编码量和更多的测试工作)。

Go 的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。 如果方法调用作用于像 interface{} 这样的 “泛型” 上,你可以通过类型断言来检查变量是否实现了相应接口。

例如,你用不同的类型表示 XML 输出流中的不同实体。然后我们为 XML 定义一个如下的 “写” 接口(甚至可以把它定义为私有接口):

1
2
3
type xmlWriter interface {
WriteXML(w io.Writer) error
}

现在我们可以实现适用于该流类型的任何变量的 StreamXML 函数,并用类型断言检查传入的变量是否实现了该接口;如果没有,我们就调用内建的 encodeToXML 来完成相应工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Exported XML streaming function.
func StreamXML(v interface{}, w io.Writer) error {
if xw, ok := v.(xmlWriter); ok {
// It’s an xmlWriter, use method of asserted type.
return xw.WriteXML(w)
}
// No implementation, so we have to use our own function (with perhaps reflection):
return encodeToXML(v, w)
}

// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
// ...
}

因此 Go 提供了动态语言的优点,却没有其他动态语言在运行时可能发生错误的缺点。

接口的提取

提取接口 是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。

Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。

所以你不用提前设计出所有的接口;整个设计可以持续演进,而不用废弃之前的决定。类型要实现某个接口,它本身不用改变,你只需要在这个类型上实现新的方法。

显式地指明类型实现了某个接口

如果你希望满足某个接口的类型显式地声明它们实现了这个接口,你可以向接口的方法集中添加一个具有描述性名字的方法。例如:

1
2
3
4
type Fooer interface {
Foo()
ImplementsFooer()
}

类型 Bar 必须实现 ImplementsFooer 方法来满足 Footer 接口,以清楚地记录这个事实。

1
2
3
type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

大部分代码并不使用这样的约束,因为它限制了接口的实用性。

但是有些时候,这样的约束在大量相似的接口中被用来解决歧义。

空接口和函数重载

在之前的学习中,我们看到函数重载是不被允许的。在 Go 语言中函数重载可以用 可变参数 …T 作为函数 最后一个参数 来实现。

如果我们把 T 换为空接口,那么可以知道任何类型的变量都是满足 T (空接口)类型的,这样就允许我们传递任何数量任何类型的参数给函数,即重载的实际含义。

函数 fmt.Printf 就是这样做的:

1
2
3
fmt.Printf(format string, a ...interface{}) (n int, errno error)

这个函数通过枚举 slice 类型的实参动态确定所有参数的类型。并查看每个类型是否实现了 String() 方法,如果是就用于产生输出信息。

接口的继承

当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)的指针时,这个类型就可以使用(另一个类型)所有的接口方法。

例如:

1
2
3
4
type Task struct {
Command string
*log.Logger
}

这个类型的工厂方法像这样:

1
2
3
func NewTask(command string, logger *log.Logger) *Task {
return &Task{command, logger}
}

当 log.Logger 实现了 Log() 方法后,Task 的实例 task 就可以调用该方法:

1
task.Log()

类型可以通过继承多个接口来提供像 多重继承 一样的特性:

1
2
3
4
type ReaderWriter struct {
*io.Reader
*io.Writer
}

上面概述的原理被应用于整个 Go 包,多态用得越多,代码就相对越少。这被认为是 Go 编程中的重要的最佳实践。

有用的接口可以在开发的过程中被归纳出来。添加新接口非常容易,因为已有的类型不用变动(仅仅需要实现新接口的方法)。

七、空接口

空接口或者最小接口 不包含任何方法,它对实现不做任何要求:

1
type Any interface {}

任何其他类型都实现了空接口(它不仅仅像 Java/C# 中 Object 引用类型),any 或 Any 是空接口一个很好的别名或缩写。

空接口类似 Java/C# 中所有类的基类: Object 类,二者的目标也很相近。

可以给一个空接口类型的变量 var val interface {} 赋任何类型的值。

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
package main
import "fmt"

var i = 5
var str = "ABC"

type Person struct {
name string
age int
}

type Any interface{}

func main() {
var val Any
val = 5
fmt.Printf("val has the value: %v\n", val)
val = str
fmt.Printf("val has the value: %v\n", val)
pers1 := new(Person)
pers1.name = "Rob Pike"
pers1.age = 55
val = pers1
fmt.Printf("val has the value: %v\n", val)
switch t := val.(type) {
case int:
fmt.Printf("Type int %T\n", t)
case string:
fmt.Printf("Type string %T\n", t)
case bool:
fmt.Printf("Type boolean %T\n", t)
case *Person:
fmt.Printf("Type pointer to Person %T\n", t)
default:
fmt.Printf("Unexpected type %T", t)
}
}

输出:

val has the value: 5
val has the value: ABC
val has the value: &{Rob Pike 55}
Type pointer to Person *main.Person

在上面的例子中,接口变量 val 被依次赋予一个 int,string 和 Person 实例的值,然后使用 type-switch 来测试它的实际类型。

每个 interface {} 变量在内存中占据两个字长:

  • 一个用来存储它包含的类型。
  • 另一个用来存储它包含的数据或者指向数据的指针。

1)构建通用类型或包含不同类型变量的数组

在之前的例子中我们看到了能被搜索和排序的 int 数组、float 数组以及 string 数组,那么对于其他类型的数组呢,是不是我们必须得自己编程实现它们?

现在我们知道该怎么做了,就是通过使用空接口。让我们给空接口定一个别名类型 Element:type Element interface{}

然后定义一个容器类型的结构体 Vector,它包含一个 Element 类型元素的切片:

1
2
3
4
5
type Element interface{}

type Vector struct {
a []Element
}

Vector 里能放任何类型的变量,因为任何类型都实现了空接口,实际上 Vector 里放的每个元素可以是不同类型的变量。我们为它定义一个 At() 方法用于返回第 i 个元素:

1
2
3
func (p *Vector) At(i int) Element {
return p.a[i]
}

再定一个 Set() 方法用于设置第 i 个元素的值:

1
2
3
func (p *Vector) Set(i int, e Element) {
p.a[i] = e
}

Vector 中存储的所有元素都是 Element 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。

注意:The compiler rejects assertions guaranteed to fail,类型断言总是在运行时才执行,因此它会产生运行时错误。

练习:创建一个 Miner 接口并实现一些必要的操作。函数 Min 接受一个 Miner 类型变量的集合,然后计算并返回集合中最小的元素。

例子如下所示:

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 main

import "fmt"

type Element interface{}

type Miner interface {
Len() int
ElemIx(ix int) Element
Less(i, j int) bool
}

type IntArray []int

func (p IntArray) Len() int { return len(p) }
func (p IntArray) ElemIx(ix int) Element { return p[ix] }
func (p IntArray) Less(i, j int) bool { return p[i] < p[j] }

func Min(data Miner) Element {
min := data.ElemIx(0)
for i := 1; i < data.Len(); i++ {
if data.Less(i, i-1) {
min = data.ElemIx(i)
}
}
return min
}

func main() {
intarr := IntArray{1, 2, 3, 5, -1}
fmt.Println(Min(intarr))
}

2)复制数据切片至空接口切片

假设你有一个 myType 类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:

1
2
var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = dataSlice

可惜不能这么做,编译时会出错:cannot use dataSlice (type []myType) as type []interface { } in assignment。

原因是它们俩在内存中的布局是不一样的。

必须使用 for-range 语句来一个一个显式地复制:

1
2
3
4
5
var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}

3)通用类型的节点数据结构

列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。

下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 NewNode 方法,及设置数据的 SetData 方法:

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

import "fmt"

type Node struct {
le *Node
data interface{}
ri *Node
}

func NewNode(left, right *Node) *Node {
return &Node{left, nil, right}
}

func (n *Node) SetData(data interface{}) {
n.data = data
}

func main() {
root := NewNode(nil, nil)
root.SetData("root node")
// make child (leaf) nodes:
a := NewNode(nil, nil)
a.SetData("left node")
b := NewNode(nil, nil)
b.SetData("right node")
root.le = a
root.ri = b
fmt.Printf("%v\n", root)
}

3)接口到接口

一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。

这个转换是在运行时进行检查的,转换失败会导致一个运行时错误:这是 ‘Go’ 语言动态的一面,可以拿它和 Ruby 和 Python 这些动态语言相比较。

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
46
47
48
49
50
51
52
package main

import (
"fmt"
"math"
)

type AbsInterface interface {
Abs() interface{}
}

type SqrInterface interface {
Sqr() float32
}

type Point struct {
value int
}

func (p *Point) Abs() interface{} {
return math.Abs(float64(p.value))
}

func (p *Point) Sqr() float32 {
return float32(p.value * p.value)
}

func main() {
var ai AbsInterface
var si SqrInterface

pp := new(Point) // say *Point implements Abs, Sqr
pp.value = -5
var empty interface{}

empty = pp // everything satisfies empty
fmt.Println(empty)
ai = empty.(AbsInterface) // underlying value pp implements Abs()
fmt.Println(ai.Abs())
// (runtime failure otherwise)
si = ai.(SqrInterface) // *Point has Sqr() even though AbsInterface doesn’t
fmt.Println(si.Sqr())
empty = si // *Point implements empty set
fmt.Println(empty)
// Note: statically checkable so type assertion not necessary.
}

输出:
&{-5}
5
25
&{-5}

八、反射

反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。

反射可以在运行时检查类型和变量,例如它的大小、方法和 动态的调用这些方法。这对于没有源代码的包尤其有用。

变量的最基本信息就是类型和值:反射包的 Type 用来表示一个 Go 类型,反射包的 Value 为 Go 值提供了反射接口。

两个简单的函数,reflect.TypeOf 和 reflect.ValueOf,返回被检查对象的类型和值。

例如:

1
2
3
4
5
6
// x 被定义为
var x float64 = 3.4

reflect.TypeOf(x) 返回 float64

reflect.ValueOf(x) 返回 <float64 Value>

实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来:

1
2
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value

接口的值包含一个 type 和 value。

反射可以从接口值反射到对象,也可以从对象反射回接口值。

reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。一个重要的例子是 Value 有一个 Type 方法返回 reflect.Value 的 Type。另一个是 Type 和 Value 都有 Kind 方法返回一个常量来表示类型:Uint、Float64、Slice 等等。同样 Value 有叫做 Int 和 Float 的方法可以获取存储在内部的值(跟 int64 和 float64 一样)

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
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)

对于 float64 类型的变量 x,如果 v:=reflect.ValueOf(x),那么 v.Kind() 返回 reflect.Float64 ,所以下面的表达式是 true

1
v.Kind() == reflect.Float64

Kind 总是返回底层类型:

1
2
3
4
5
6
type MyInt int
var m MyInt = 5
v := reflect.ValueOf(m)

方法 v.Kind() 返回 reflect.Int。
变量 v 的 Interface() 方法可以得到还原(接口)值,所以可以这样打印 v 的值:fmt.Println(v.Interface())

示例代码:

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

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
v := reflect.ValueOf(x)
fmt.Println("value:", v)
fmt.Println("type:", v.Type())
fmt.Println("kind:", v.Kind())
fmt.Println("value:", v.Float())
fmt.Println(v.Interface())
fmt.Printf("value is %5.2e\n", v.Interface())
y := v.Interface().(float64)
fmt.Println(y)
}

输出:

type: float64
value: 3.4
type: float64
kind: float64
value: 3.4
3.4
value is 3.40e+00
3.4

1)通过反射修改 (设置) 值

假设我们要把 x 的值改为 3.1415。Value 有一些方法可以完成这个任务,但是必须小心使用:v.SetFloat(3.1415)。

这将产生一个错误:reflect.Value.SetFloat using unaddressable value。

为什么会这样呢?问题的原因是 v 不是可设置的(这里并不是说值不可寻址)。是否可设置是 Value 的一个属性,并且不是所有的反射值都有这个属性:可以使用 CanSet() 方法测试是否可设置。

在例子中我们看到 v.CanSet() 返回 false: settability of v: false

当 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x。要想 v 的更改能作用到 x,那就必须传递 x 的地址 v = reflect.ValueOf(&x)。

通过 Type () 我们看到 v 现在的类型是 *float64 并且仍然是不可设置的。

要想让其可设置我们需要使用 Elem() 函数,这间接的使用指针:v = v.Elem()

现在 v.CanSet() 返回 true 并且 v.SetFloat(3.1415) 设置成功了!

例子如下所示:

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

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
// setting a value:
// v.SetFloat(3.1415) // Error: will panic: reflect.Value.SetFloat using unaddressable value
fmt.Println("settability of v:", v.CanSet())
v = reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of v:", v.Type())
fmt.Println("settability of v:", v.CanSet())
v = v.Elem()
fmt.Println("The Elem of v is: ", v)
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(3.1415) // this works!
fmt.Println(v.Interface())
fmt.Println(v)
}

输出:

settability of v: false
type of v: *float64
settability of v: false
The Elem of v is: <float64 Value>
settability of v: true
3.1415
<float64 Value>

2)反射结构体

NumField() 方法返回结构体内的字段数量;通过一个 for 循环用索引取得每个字段的值 Field(i)。

我们同样能够调用签名在结构体上的方法,例如,使用索引 n 来调用:Method(n).Call(nil)。

例子如下所示:

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

import (
"fmt"
"reflect"
)

type NotknownType struct {
s1, s2, s3 string
}

func (n NotknownType) String() string {
return n.s1 + " - " + n.s2 + " - " + n.s3
}

// variable to investigate:
var secret interface{} = NotknownType{"Ada", "Go", "Oberon"}

func main() {
value := reflect.ValueOf(secret) // <main.NotknownType Value>
typ := reflect.TypeOf(secret) // main.NotknownType
// alternative:
//typ := value.Type() // main.NotknownType
fmt.Println(typ)
knd := value.Kind() // struct
fmt.Println(knd)

// iterate through the fields of the struct:
for i := 0; i < value.NumField(); i++ {
fmt.Printf("Field %d: %v\n", i, value.Field(i))
// error: panic: reflect.Value.SetString using value obtained using unexported field
//value.Field(i).SetString("C#")
}

// call the first method, which is String():
results := value.Method(0).Call(nil)
fmt.Println(results) // [Ada - Go - Oberon]
}

输出:

main.NotknownType
struct
Field 0: Ada
Field 1: Go
Field 2: Oberon
[Ada - Go - Oberon]

但是如果尝试更改一个值,会得到一个错误:

1
panic: reflect.Value.SetString using value obtained using unexported field

这是因为结构体中 只有被导出字段(首字母大写)才是可设置的。

来看下面的例子:

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

import (
"fmt"
"reflect"
)

type T struct {
A int
B string
}

func main() {
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
}
输出:

0: A int = 23
1: B string = skidoo
t is now {77 Sunset Strip}

参考文章

  • Go 入门指南
  • 本文作者: th3ee9ine
  • 本文链接: https://www.blog.ajie39.top/2022/05/07/go入门总结(二)/
  • 版权声明: 本博客所有文章除特别声明外,均采用 LICENSE 下的许可协议。转载请注明出处!
  • go
  • go基础
  • go

扫一扫,分享到微信

Go 入门总结(三)
Go 入门总结(一)
© 2019-2023 th3ee9ine
GitHub:hexo-theme-yilia-plus by Litten
本站总访问量2844次 | 本站访客数1615人
  • 所有文章
  • 友链
  • 关于我

tag:

  • 中间件
  • A/B测试
  • 面试
  • 设计模式
  • Git
  • 开发规范
  • go
  • go基础
  • 工程化
  • Jackson
  • 问题总结
  • Java
  • JVM
  • Markdown
  • MyBatis
  • Mybatis
  • ORM
  • Mysql
  • Redis
  • Reactor模型
  • YAML
  • feign
  • rpc
  • git
  • maven
  • 配置
  • linux
  • python
  • 数据可视化
  • redis
  • 协程
  • channel
  • 杂项
  • Hexo
  • github
  • Netty
  • IM
  • WebSocket
  • 测试
  • 开发基础
  • 问题and坑点
  • tools
  • 算法
  • 数据库
  • 推理
  • 博弈
  • Map
  • Java基础
  • 源码分析
  • 微服务
  • RPC
  • HBase
  • Jooq
  • JavaWEB
  • SSM
  • Spring
  • 分布式锁

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia-plus根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • 解密MySQL:索引—优化数据库性能的关键(一)

    2023-09-17

    #Mysql

  • 解密MySQL:深入剖析一条SQL语句的执行过程

    2023-07-02

    #Mysql

  • Git分支规范

    2023-05-28

    #Git#开发规范

  • Git多账号配置

    2023-05-28

    #Git#配置

  • 办公软件安装指南

    2022-12-18

    #tools

  • feign坑点总结

    2022-12-18

    #Java#feign#rpc

  • 基于 Netty 实现在线聊天系统(实战篇一)

    2022-07-06

    #Java#Netty#IM

  • 基于 Netty 实现在线聊天系统(原理篇二)

    2022-07-05

    #Java#Netty#IM#WebSocket

  • 基于 Netty 实现在线聊天系统(原理篇一)

    2022-07-03

    #Java#Netty#IM

  • redis 大 key 问题总结(附Go小工具)

    2022-06-22

    #go#问题总结#redis

  • Go 项目结构(草稿)

    2022-06-19

    #go#工程化

  • git——清除所有历史提交记录

    2022-06-19

    #git

  • 逗号 ok 模式与 defer 模式总结

    2022-06-18

    #go#go基础

  • Go 中常见的陷阱与错误

    2022-06-18

    #go#go基础

  • 一个有意思的协程例子

    2022-06-18

    #go#协程#channel

  • Go 协程与通道(channel)

    2022-06-16

    #go#go基础#协程#channel

  • Go 入门总结(三)

    2022-06-12

    #go#go基础

  • Go 入门总结(二)

    2022-05-07

    #go#go基础

  • Go 入门总结(一)

    2022-03-06

    #go

  • git--Fork代码更新与同步

    2022-02-02

    #git

  • 重写TreeMap的比较器(Comparator)引发的问题(源码分析)

    2022-01-16

    #问题总结#Map#Java基础#源码分析

  • Jackson序列化时间类型的数据导致的相关问题

    2021-12-02

    #Jackson#问题总结

  • 算法--排序奇升偶降链表

    2021-11-21

    #面试#算法

  • Java 面试题目总结(非完整版,待补充)

    2021-11-14

    #面试

  • 如何写容易测试的代码(结尾附带单元测试神器推荐)

    2021-10-24

    #测试#开发基础

  • Redis 之从面试题到原理(基础篇)

    2021-06-29

    #面试#Redis

  • 自用开发工具汇总

    2021-06-19

    #杂项

  • Mysql 数据库相关知识点总结

    2021-05-16

    #面试#Mysql

  • Java面试之Hashmap

    2021-05-16

    #面试

  • 数据库基础复习

    2021-05-16

    #面试#Java#数据库

  • Redis 是单线程的???

    2021-05-09

    #Redis#Reactor模型

  • Redis 之从面试题到原理(拓展篇)

    2021-05-05

    #面试#Redis

  • Redis 之从面试题到原理(进阶篇)

    2021-05-05

    #面试#Redis

  • 分布式锁原理分析与应用

    2021-05-05

    #分布式锁

  • YAML的常用语法

    2021-05-05

    #YAML

  • Spring中常用的注解

    2021-05-05

    #Java#Spring

  • python之操作Excel模块

    2021-05-05

    #python

  • python数据可视化

    2021-05-05

    #python#数据可视化

  • python基础

    2021-05-05

    #python

  • ORM之他暂时可能只是个弟弟——Jooq

    2021-05-05

    #Java#ORM#Jooq

  • MyBatis常见面试题总结

    2021-05-05

    #面试#MyBatis

  • Mybatis笔记

    2021-05-05

    #Java#Mybatis#ORM

  • Markdown的常用语法

    2021-05-05

    #Markdown

  • linux深入浅出

    2021-05-05

    #linux

  • Lambda表达式

    2021-05-05

    #Java

  • Java依赖注入详解

    2021-05-05

    #面试

  • Java面试之JVM自动内存管理机制

    2021-05-05

    #面试#Java#JVM

  • java面试之三大特征、六大基本原则

    2021-05-05

    #面试#Java

  • Java基础复习

    2021-05-05

    #面试#Java

  • Java核心技术卷1复习笔记

    2021-05-05

    #面试#Java

  • HBase学习笔记

    2021-05-05

    #HBase

  • GRPC学习笔记

    2021-05-05

    #微服务#RPC

  • Google Java编程风格规范(中文)

    2021-05-05

    #Java

  • flagr入门

    2021-05-05

    #中间件#A/B测试

  • 有趣的海盗分金问题

    2021-05-05

    #推理#博弈

  • 面试题目总结

    2021-05-05

    #面试#Java基础

  • 面试基础题之《volatile》

    2021-05-05

    #面试#Java基础

  • 面试基础题之《面向对象》

    2021-05-05

    #面试#Java基础

  • 美化Blog

    2021-05-05

    #Hexo

  • 利用Hexo和Github搭建Blog(详细版)

    2021-05-05

    #Hexo#github

  • 从零开始学习lambda表达式

    2021-05-05

    #Java

  • 彩笔的一天之面试总结

    2021-05-05

    #面试

  • 如何保证类线程安全

    2021-05-05

    #面试

  • 9种单例模式总结

    2021-05-05

    #面试#设计模式

  • SSM框架整合总结

    2021-05-01

    #JavaWEB#SSM

  • 架构图

    2021-05-01

    #杂项

  • git和maven常用命令

    2020-10-25

    #git#maven

  • 乌班图安装Tools等教程

    2020-10-25

    #杂项

  • 专业术语汇总

    2020-05-31

    #杂项

  • 安装Oracle12C坑点总结

    2019-04-07

    #杂项#问题and坑点

  • 算法竞赛知识点总结(JAVA版)

    2019-04-07

    #算法

  • Pantheon
  • zhuscat
  • threedr3am
教练,我想学技术!
我主要是以后端技术为主,偶尔看看网络安全相关的内容,或者编写一些有趣的脚本!