Go派生类型基础

结构体

概述

结构体struct是一个由相同或者不同类型的数据所构成的集合。

集合中的的元素被称为结构体的成员member,不过在Go中经常被称为字段field实际上是同一个概念。

定义

结构体使用如下语法来声明

1
2
3
4
5
6
type struct_variable_type struct {
member def
member def
...
member def
}

结构体是一种自定义数据结构,type语句声明了这个结构体的类型,这个类型是由用户自定义的,同时在结构体的内部我们需要声明这个结构体所拥有的成员的名称和相应的类型

示例1:struct.go

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
fmt.Println(Vertex{1,2})
}

编译运行

1
{1 2}

如果我们需要访问或操作结构体的成员,这个时候需要使用点号来实现

示例2:struct-field.go

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

import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
v.X = 4 // 对结构体v的成员X赋值
fmt.Println(v.X)
}

编译运行

1
4

通过指针同样可以访问结构体的成员,如果我们定义了一个指针p指向结构体,那么我们便可以通过(*p).X的形式来访问相应的成员

不过这种方式过于啰嗦,通常我们可以隐式地引用,即使用p.X的形式就可以

结构体文法

创建一个结构体我们只需要直接列出成员的值便可以,使用name:则可以仅列出部分成员,像下面这样

1
2
3
4
5
6
7
8
9
10
// 声明一个结构体
type Vertex struct {
X, Y int
}

// 初始化一个Vertex类型的结构体
v1 = Vertex{1, 2}
// 隐式赋值
v2 = Vertex{X: 1} // Y: 0被隐式地赋予
v3 = Vertex{} // X:0,Y:0均被隐式赋予

与基本变量相同,结构体同样可以使用&来创建一个指向该结构体的指针

1
2
// 初始化一个*Vertex类型的结构体(指针)
p = &Vertex{1, 2}

数组

概述

数组是一个具有相同且唯一类型的一组编号的、长度固定的数据序列,数组的类型可以是任意类型。

定义

数组使用[n]T的形式声明,表示这是一个拥有nT类型的值的数组,就像这样

1
var a [10]int // 初始化一个数组a,a拥有10个int类型的数据

数组的长度是它类型的一部分,所以不能改变它的大小

示例3:array.go

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

import "fmt"

func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)

primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}

编译运行

1
2
3
Hello World
[Hello World]
[2 3 5 7 11 13]

上面的示例实际上还展示了访问数组中特定元素的方法,即通过索引来访问,数组的索引从0开始

切片

概述

上面提到数组的大小是不能改变的,因此Go语言提供了另外一种能够动态、灵活地使用数组的方式,这就是切片

定义

切片使用[]T来声明一个T类型的切片,切片的边界由两个下标:上界和下界来界定,上下界由冒号分隔a[low : high]

切片可以包含任何类型,甚至包括其他的切片
即:

1
2
3
4
5
>s := [ ][ ]int {
[ ]int
[ ]int
[ ]int
>}

这里的s是一个2维切片(数组)

这是一个半开区间,它包括第一个元素但不包括最后一个元素,即a[1 : 4]包含索引从1到3的元素

示例4:slices.go

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}

var s []int = primes[1:4]
fmt.Println(s)
}

可以看到我们首先声明并初始化了一个数组prime,它的长度是6,接下来我们声明并初始化一个切片s,s拥有数组中索引1到3的元素,编译并运行,我们可以看到运行结果正如我们的期待

1
[3 5 7]

上面的例子我们可以看到切片正如其名,它就像是从数组中选择一部分切下一样,或者说切片像是对数组的引用。

实际上切片并不存储任何的数据,它是对底层数组的其中一段的描述,如果我们修改切片中的数据,数组中相应的数据也会被修改,并且与这个切片共享一个数组的其他切片也会受到影响,下面这个示例便展示这种影响

示例5:slice-pointers.go

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() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)

a := names[0:2]
b := names[1:3]
fmt.Println(a, b)

b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}

我们首先初始化一个数组names,接着初始化两个切片ab,它们共享同一个数组names,这时我们修改b中的一个元素的值,我们期望观察到对b的修改也影响到了a和数组primes,即aprimes中对应的值也被修改了

那么我们编译并运行

1
2
3
4
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]

可以看到正如我们期望的一样

切片的文法

切片文法类似于没有长度的数组文法,如下初始化一个数组

1
[3]bool{true, true, false}

而像下面这样没有长度的写法则会创建一个和上面相同的数组,然后构建一个引用这个数组的切片

1
[]bool{true, true, false}

初始化一个切片我们也可以利用它的默认行为来忽略上忽略上下界,切片下界默认为0,上界则是该切片的长度,对于下面这个数组

1
var a [10]int

有以下切片与其等价

1
2
3
4
a[0:10]
a[:10]
a[0:]
a[:]

示例6:slice-bound.go

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

import "fmt"

func main() {
s := []int{2, 3, 5, 7, 11, 13}

s = s[1:4]
fmt.Println(s)

s = s[:2]
fmt.Println(s)

s = s[1:]
fmt.Println(s)
}

编译运行

1
2
3
[3 5 7]
[3 5]
[5]

切片的长度与容量

切片的长度就是它所包含的元素的个数,获取一个切片的长度可以使用len(s)表达式

切片的容量是从他的第一个元素开始数,到其底层数组元素末尾的的元素个数,获取一个切片的容量可以使用cap(s)表达式

示例7:slice-len-cap.go

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() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)

// 截取切片使其长度为 0
s = s[:0]
printSlice(s)

// 拓展其长度
s = s[:4]
printSlice(s)

// 舍弃前两个值
s = s[2:]
printSlice(s)
}

func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

示例7演示了如何重新切片来扩展切片的长度

编译并运行

1
2
3
4
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]

切片的零值是nil

一个nil切片的长度和容量都是0,而且nil切片没有底层数组

通过make创建切片

创建一个切片也可以Go语言的内建函数make,它会分配一个元素值为0的数组并返回一个引用该数组的切片

1
2
// 创建一个长度为5的切片
a := make([]int, 5)

传入make函数的两个参数分别为切片类型和长度,在上面的例子中切片类型为[]int,切片长度为5,如果想要指定创建的切片的容量则需要传入第三个参数

1
2
// 创建一个长度为0,容量为5的切片
b := make([]int , 0, 5)

未指定容量的情况下创建的切片容量等于其长度
示例8:making-slices.go

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() {
a := make([]int, 5)
printSlice("a", a)

b := make([]int, 0, 5)
printSlice("b", b)

c := b[:2]
printSlice("c", c)

d := c[2:5]
printSlice("d", d)
}

func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}

编译运行

1
2
3
4
a len=5 cap=5 [0 0 0 0 0]
b len=0 cap=5 []
c len=2 cap=5 [0 0]
d len=3 cap=3 [0 0 0]

可以看出在没有指定容量的情况下make函数创建的切片a的容量等于其长度,均为5

向切片内追加元素

有些时候我们需要向一个切片内加入新的数据,这种情况我们需要使用append函数

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

其中第一个参数s是一个元素类型为T的切片,其余的类型为T的参数的之会追加到这个切片的末尾
示例9:append.go

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() {
var s []int
printSlice(s)

// 添加一个空切片
s = append(s, 0)
printSlice(s)

// 这个切片会按需增长
s = append(s, 1)
printSlice(s)

// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
}

func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

编译运行

1
2
3
4
len=0 cap=0 []
len=1 cap=2 [0]
len=2 cap=2 [0 1]
len=5 cap=8 [0 1 2 3 4]

可以看到append的结果是一个包含原切片所有元素加上新添加元素的切片,而且当这个切片的底层数组过小时,Go会自动分配一个更大的数组,返回的切片也会指向这个新的数组。

切片的遍历

使用for循环的range形式可以对一个切片中的所有元素进行遍历

1
2
3
for i,v := range s {
...
}

使用range会使每次迭代返回两个值,第一个值是当前元素的索引,第二个值是该索引对应的元素的一份副本

对应在上面的例子中i是切片s的索引,而v是i对应的元素的值

我们也可以通过赋予_来忽略索引或值

1
2
3
4
5
6
7
8
// 忽略值
for i,_ := range s {
...
}
// 忽略索引
for _,v := range s {
...
}

若只需要索引,则可以直接省略第二个变量

1
2
3
for i := range s {
...
}

示例10:range.go

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

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}

编译运行

1
2
3
4
5
6
7
8
2**0 = 1
2**1 = 2
2**2 = 4
2**3 = 8
2**4 = 16
2**5 = 32
2**6 = 64
2**7 = 128

练习

实现Pic函数,它返回一个长度为dy的切片,其中每个元素是一个长度为dx,类型为uint8的切片

程序运行时,它会将每个整数解释为灰度值,并显示它所对应的图像

图像可以在一下几个函数中选择:

  1. (x+y)/2
  2. x*y
  3. x^y
  4. x*log(y)
  5. x%(y+1)

提示:

需要使用循环来分配[ ][ ]uint8中的每个[ ]uint8;

请使用uint8(intValue)在类型之间转换;

可能会用到math包中的函数;

练习1:exercise-slices.go

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

import "golang.org/x/tour/pic"
import "math"

func Pic(dx, dy int) [][]uint8 {
pic_col := make([][]uint8, dy)
for i := range pic_col {
pic_row := make([]uint8, dx)
for j := range pic_row {
gray := i*math.Log(float64(j))
pic_row[j] = uint8(gray)
}
pic_col[i] = pic_row
}
return pic_col
}

func main() {
pic.Show(Pic)
}

编译并运行后显示如下图像

result

更换一下函数再试试

  1. (x+y)/2:

result

  1. x*y:

result

  1. x^y:

result

  1. x%(y+1)

result

总结

切片基于数组构建,但是相比于数组切片的使用则灵活得多。正是由于这种灵活性,在实际的使用中,切片的使用也常常要比数组广泛得多

作为数组的抽象,实际上切片是动态数组的一种实现

映射

概述

映射是一个集合,它会将key映射到value,可以通过key来快速检索数据,类似于索引

定义

映射使用map语句声明

1
var m map[T]v

或者使用make函数

1
m :=make(map[T]v)

声明一个T类型映射并初始化,上述两种形式是等价的

映射的零值为nil

nil映射没有key,也无法添加key

示例11:maps.go

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

import "fmt"

type Vertex struct {
Lat, Long float64
}

var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}

func main() {
fmt.Println(m)
}

编译运行

1
map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]

我们可以对映射进行多种操作,包括:

  1. 插入&修改元素

    1
    2
    3
    4
    5
    6
    m[key] = elem
    ```

    2. 删除元素
    ``` code
    delete(m, key)
  2. 检测某个键是否存在

    1
    elem, ok = m[key]

如果该key在映射m中,则ok为true,反之则为false,
如果该key不在映射中,则elem是该映射元素类型的零值。

练习

实现函数WordCount,它应当返回一个映射,其中包含字符串中每个单词的个数。wc.Test函数则会对此函数执行测试并输出成功还是失败。

strings.Fields会很有帮助

练习2:exercise-maps.go

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

import (
"golang.org/x/tour/wc"
"strings"
)

func WordCount(s string) map[string]int {
v := strings.Fields(s)
m := make(map[string]int)
for i := range v {
m[v[i]] += 1
}
return m
}

func main() {
wc.Test(WordCount)
}

编译运行

1
2
3
4
5
6
7
8
9
10
11
12
PASS
f("I am learning Go!") =
map[string]int{"Go!":1, "I":1, "am":1, "learning":1}
PASS
f("The quick brown fox jumped over the lazy dog.") =
map[string]int{"The":1, "brown":1, "dog.":1, "fox":1, "jumped":1, "lazy":1, "over":1, "quick":1, "the":1}
PASS
f("I ate a donut. Then I ate another donut.") =
map[string]int{"I":2, "Then":1, "a":1, "another":1, "ate":2, "donut.":2}
PASS
f("A man a plan a canal panama.") =
map[string]int{"A":1, "a":2, "canal":1, "man":1, "panama.":1, "plan":1}

函数

函数值

函数同样也是值,它可以像其他值一样被传递,也可以用作函数的参数或者返回值

示例12:function-values.go

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

import (
"fmt"
"math"
)

func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}

func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))

fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}

编译运行

1
2
3
13
5
81

第一个输出值为函数hypot(5,12)的返回值,返回值为(55+1212)的平方根;

第二个输出值为函数compute(hypot)的返回值,其中传递给compute的参数hypot是一个函数,返回值为hypot(3,4),这是一个函数,继续返回则得到最终结果(33+44)的平方根;

第三个输出值的形式与第二个类似,最终输出3^4;

函数的闭包

Go函数可以是一个闭包。

闭包是一个函数值,它引用了函数体之外的变量,并且该函数可以访问并赋予引用变量的值。

闭包可以理解为“定义在一个函数内部的函数”

示例13:closures.go

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 adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}

编译运行

1
2
3
4
5
6
7
8
9
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

在上面的示例中adder函数便返回了一个闭包,每个闭包与各自的sum变量绑定

练习

实现函数fibonacci,他返回一个函数(闭包),这个闭包返回一个斐波那契数列。

什么是斐波那契数列

斐波那契数列可以用递归的方法来定义:

  • F_0 = 0
  • F_1 = 1
  • F_n = F_n-1 + F_n-2 (n >= 2)

练习3:exercise-fibonacci-closure.go

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"

// 返回一个“返回int的函数”
func fibonacci() func() int {
num := -1
s := make([]int,10)
return func() int {
num += 1
if num < 2 {
s[num] = num
return s[num]
} else {
s[num] = s[num-1] + s[num-2]
return s[num]
}
return s[num]
}
}

func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}

编译运行

1
2
3
4
5
6
7
8
9
10
0
1
1
2
3
5
8
13
21
34
作者

Luc_41

发布于

2020-03-22

更新于

2020-04-02

许可协议

评论