七叶笔记 » golang编程 » 大白话 golang 教程-08-学习复杂数据类型

大白话 golang 教程-08-学习复杂数据类型

把有限的相同数据类型的变量集合到一起就叫数组(有些语言的数组元素类型可以不同),组成数组的个体叫元素,元素的顺序编号称为下标,一般下标从 0 开始(有些编程语言从 1 开始),数组有固定的长度,分布在一段连续的内存空间中,因为是连续的,通过下标来访问数组中的元素很高效(计算内存地址偏移很简单)。

下面申明一个整数数组:

 var arr [3]int // 申明
var arr [3]int = [3]int{1, 2, 3} // 申明并设置初始值
var := [3]int{1, 2, 3} // 推导
arr := [...]int{1, 2, 3} // 根据初始值来分配  

go 的数组还有一个特殊的申明:

 // 等效于 [5]int{0, 2, 3, 0, 0}
arr := [5]int{1:2, 2:3} // 数组的大小是5,第 2 个元素的值是 2,第 3 个元素的值是 3,没有指定的就是 int 零值(0)可以  

可以使用内置函数 len 获得数组的大小。

 func initialSpecial() [5]int {
  arr := [5]int{1: 2, 2: 3}
  fmt.Println(len(arr))   // 输出 5
  fmt.Printf("%T\n", arr) // 输出 [5]int
  return arr
}  

go 的数组是一个值类型,把一个数组直接传递给函数,将发生数据拷贝,函数内对参数数组的改变不会影响到原数组。

 func updateFailed(arr [3]int) {
  fmt.Printf("%p\n", &arr) // 0xc000014340
  arr[0] = 1
  arr[1] = 1
  arr[2] = 1
  fmt.Println(arr) // [1 1 1]
}

func Test_updateFailed(t *testing.T) {
  arr := [3]int{1, 2, 3}
  fmt.Printf("%p\n", &arr) // 0xc000014320
  updateFailed(arr)
  fmt.Println(arr) // [1 2 3]
  if !reflect.DeepEqual(arr, [3]int{1, 2, 3}) {
    t.Fatal("函数改变了原数组")
  }
}  

和所有的值类型一样,要修改数组的值,需要传递指针。

 func updateSuccessed(ptrArr *[3]int) {
  fmt.Printf("%p\n", ptrArr) // 0xc0000cc140
  ptrArr[0] = 1
  ptrArr[1] = 1
  ptrArr[2] = 1
  fmt.Println(*ptrArr) // [1 1 1]
}

func Test_updateSuccessed(t *testing.T) {
  arr := [3]int{1, 2, 3}
  fmt.Printf("%p\n", &arr) // 0xc0000cc140
  updateSuccessed(&arr)
  fmt.Println(arr) // [1 1 1]
  if !reflect.DeepEqual(arr, [3]int{1, 1, 1}) {
    t.Fatal("函数改变了原数组")
  }
}  

当数组特别大的时候,直接传递给函数性能会非常低下。

 func printHugeArray1(arr [10000000]int) {
  // 啥也不干
}

func printHugeArray2(arr *[10000000]int) {
  // 啥也不干
}

func Benchmark_printHugeArray1(b *testing.B) {
  arr := [10000000]int{}
  for i := 0; i < b.N; i++ {
    printHugeArray1(arr)
  }
}

// 75                 15533465 ns/op81069791 B/op       1 allocs/op

func Benchmark_printHugeArray2(b *testing.B) {
  arr := [10000000]int{}
  for i := 0; i < b.N; i++ {
    printHugeArray2(&arr)
  }
}

// 1000000000         0.298    ns/op       0 B/op       0 allocs/op  

这里 testing.B 对对函数进行基准测试,它会运行目标代码 b.N 次,保证测试函数运行足够上的时间。直接传递数组的情况下,需要拷贝一次数组的数据,所以循环了 75 次每次循环的时间是 15533465 纳秒(约等于 0.015 秒,1 秒等于 10 的 9 次方纳秒),而传递数组指针拷贝的是数组指针的值,循环了 1000000000 次每次循环的时间是 0.298 纳秒。

由于数组具有固定的大小,引用的时候不方便,go 把数组作为底层的数据结构作为引用,定了切片类型,也就说切片是对数组一个连续片段的引用,可以是整个数组,也可以是子集。之前我们提到过 StringHeader 字符串的结构,字符串就是一个特殊的数组,而切片的结构定义其实比 StringHeader 多了一个 cap 字段。

 // SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
  Data uintptr
  Len  int
  Cap  int
}  

从这个定义可以看出切片的大小在 64 位 CPU 上是 8 + 8 + 8 = 24 个字节,也就说当函数传递切片参数时,将发生 24 个字节的拷贝。

 func sliceSize() {
  s1 := []int{} // 构造空的切片
  fmt.Println(unsafe.Sizeof(s1)) // 24
}  

如上构造一个切片对象和数组的区别就是,不表明数组元素的大小,既然切片是对数组的引用,那么就可以通过数组来构造切片。

 func createSliceFromArray() {
  arr1 := [5]int{1, 2, 3, 4, 5}
  s1 := arr1[:]
  fmt.Printf("%T %T\n", arr1, s1) // [5]int []int
}  

arr1[:] 引用了完整的数值,还有以下方式:

 s3 := arr1[2:3]               // 索引从2开始到3
fmt.Println(s3)               // [3]
fmt.Println(len(s3), cap(s3)) // 1 3

s4 := arr1[2:3:4]
fmt.Println(s4)               // [3]
fmt.Println(len(s4), cap(s4)) // 1 2  

函数 len 返回切片当前有多少个元素,cap 表明切片最多能容纳多少个元素。arr1[2:3] 含义是从索引 2 开始到 3 引用了 1 个元素,切片的容量延续到原引用的最大值 5 但不包含2-3-4 => 3;arr1[2:3:4] 表示从索引 2 开始到 3 引用了 1 个元素,切片的容量一直延续到 4 (不包含),所以容量是 2-3 => 2。结论是 slice = slice[low : high : max] ,low 为截取的起始下标, high 为截取的结束下标(不包含 high 元素),max 为切片保留的原数组容量大小(不包含),如果 max 超过原数组的的大小,会发生溢出错误。

 s5 := arr1[2:3:5] // ok
fmt.Println(s5)
fmt.Println(len(s5), cap(s5)) // 1 3

// s6 := arr1[2:3:6] // invalid slice index 6 (out of bounds for 5-element array)
// fmt.Println(s6)  

可以使用 append 和 copy 内置函数,修改切片对应的底层数组。

 s5 = append(s5, 4)
fmt.Println(len(s5), cap(s5)) // 2 3
s5 = append(s5, 5, 6, 7)         // 超过了原来的容量,会自动扩容
fmt.Println(s5)               // [3 4 5 6 7]
fmt.Println(len(s5), cap(s5)) // 4 7

s7 := []int{2, 3, 4}
fmt.Println(len(s7), cap(s7)) // 3 3
copy(s7, []int{1, 2, 3, 4, 5})
fmt.Println(s7)               // [1 2 3] 超出的元素没有,不会自动扩容
fmt.Println(len(s7), cap(s7)) // 3 3  

还有三种写法来创建切片。

 s8 := []int{4: 5}
fmt.Println(s8)               // [0 0 0 0 5]
fmt.Println(len(s8), cap(s8)) // 5 5

const size = 5
const max = 10
s9 := make([]int, size, max)
fmt.Println(s9)               // [0 0 0 0 0]
fmt.Println(len(s9), cap(s9)) // 5 10

s10 := new([]int)
*s10 = append(*s10, 1, 2, 3)
fmt.Println(*s10)                 // [1 2 3]
fmt.Println(len(*s10), cap(*s10)) // 3 4  

make 是内置函数,创建根据类型分配内存和初始化返回类型变量,new 分配内存后返回指向的内存地址–指针,下面是它们的函数签名:

 func make(t Type, size ...IntegerType) Type
func new(Type) *Type  

把切片传递给函数的基准测试数据,依然高效:

 func Benchmark_printHugeSlice(b *testing.B) {
  arr := [10000000]int{}
  for i := 0; i < b.N; i++ {
    printHugeSlice(arr[:])
  }
}

// 1000000000           0.295 ns/op         0 B/op        0 allocs/op  

key-value 也是经常用的数据类型,go 用 map 来表示无序的键值对,就像字典。

 func mapOperate() {
  myDict := map[string]string{}
  myDict["one"] = "1"
  myDict["two"] = "2"
  myDict["three"] = "3"

  fmt.Println(myDict)        // map[one:1 three:3 two:2]
  fmt.Println(myDict["one"]) // 1

  delete(myDict, "one")
  str, ok := myDict["one"]
  fmt.Println(ok)  // false
  fmt.Println(str) // 空
}  

map 的 key 键必须是唯一的,因为其内部是哈希表实现来高效搜索,请注意 map 是引用类型,使用的时候应该避免 nil,作为函数参数,对它的修改将反应到调用者。

 var scores map[string]int // nil 
scores := make(map[string]int)
scores["语文"] = 90
scores["数学"] = 88

var scores map[string]int = map[string][int]{"语文": 90, "数学": 88}
scores := map[string][int]{"语文": 90, "数学": 88}

func updateDict(dict map[string]string) {
  dict["four"] = "4"
  delete(dict, "one")
}

func Test_updateDict(t *testing.T) {
  dict := map[string]string{"one": "1", "two": "2", "three": "3"}
  updateDict(dict)
  fmt.Println(dict) // map[four:4 three:3 two:2]
}  

可以使用 len 函数获得 map 的键个数,delete 来删除 map 中的元素,对 map 取值有个特殊的语义:

 delete(scores, "数学")
fmt.Println(len(scores))

score := scores["语文"]
score, ok := scores["语文"] // 不存在 ok 为 false  

使用 for-range 很方便的遍历 map 的元素,注意不能保证顺序:

 for key, value := range myDict {
  fmt.Println(key, value)
}

for key := range myDict {
  fmt.Println(key, myDict[key])
}  

可以使用 map 去表达一个复杂对象,比如对于一本书来说,有作者、名称、出版时间等,但是语义上就不会那么清晰,而且 map 的键必须是相同的类型,对于出版时间,明显使用日期时间数据类型更好,表达这类数据最好的办法是使用 struct 结构体:

 func defineBook() {
  book := struct {
    Name    string
    Author  string
    Publish time.Time
  }{"算法导论", "大神", time.Now()}
  fmt.Printf("%T\n", book) // struct { Name string; Author string; Publish time.Time }
  fmt.Println(book)
}  

为了更好的表达语义,可使用 type 定一个 Book 类型:

 //Book 书
type Book struct {
  Name    string
  Author  string
  Publish time.Time
}  
 func useBookStruct() {
  book1 := Book{}
  fmt.Println(book1)

  book2 := Book{
    "算法导论", "大神", time.Now(),
  }
  fmt.Println(book2)

  book3 := Book{
    Name: "算法导论", Author: "大神", Publish: time.Now(),
  }
  book3.Name = "计算机发展史"
  fmt.Println(book3)
}  

struct 结构体是值类型,作为函数参数将发生拷贝。

 func changeBookNameFailed(book Book) {
  book.Name = "无字天书"
  fmt.Printf("%p\n", &book)
  fmt.Println(book)
}

func changeBookNameSuccessed(ptrBook *Book) {
  ptrBook.Name = "无字天书"
  fmt.Printf("%p\n", ptrBook)
  fmt.Println(*ptrBook)
}

book := Book{
    "算法导论", "大神", time.Now(),
  }
  fmt.Printf("%p\n", &book)

  changeBookNameFailed(book)
  fmt.Println(book) // 算法导论

  changeBookNameSuccessed(&book)
  fmt.Println(book) // 无字天书
}  

下面的代码使用 for-range 来遍历数组结构体,请注意 for-range 中迭代循环变量始终是一个,它每次迭代的时候拷贝结构体的值,要获得原结构体的引用需要使用 index 来配合。

 //person 人
type person struct {
  Name string
  Age  int
}

func forRangeStruct() {
  persons := []person{
    {"zhangsan", 20},
    {"lisi", 21},
  }

  for index, item := range persons {
    fmt.Printf("%p\n", &item)           // 0xc00012a0a0
    fmt.Printf("%p\n", &persons[index]) // 0xc000108360, 0xc000108378
    fmt.Println(index, item)
  }
}  

结构体在 go 语言里非常重要,后面还需要继续深入学习它,还有 2 个类型: 接口 interface、chan 通道,先简单的介绍。接口 interface 用来定义一系列共性的方法,比如猫和狗都可以跑和跳,那么就可以用一个接口 Pet 来表示他们的行为:

 type Pet interface {
  Run()
  Jump()
}  

在 go 里有一个接口可以表示世间万物,就是空接口,它没有定义任何行为:

 // interface{} 世间万物
func emptyInterface() {
  var any interface{} = 0 // 给空接口对象初值,否则默认是 nil 
  fmt.Printf("%T\n", any) // int

  any = "test"
  fmt.Printf("%T\n", any) // string

  any = 3.1415926
  fmt.Printf("%T\n", any) // float64

  any = []int{1, 2, 3}
  fmt.Printf("%T\n", any) // []int

  any = struct{}{}
  fmt.Printf("%T\n", any) // struct {}
}  

这段代码的 struct{} 也表示空结构体,struct {} 在 go 里非常的特殊。

 func lookupEmptyStruct() {
  a := struct{}{}
  b := struct{}{}

  if reflect.DeepEqual(a, b) {
    fmt.Printf("%p\n", &a) // 0x125a7d0
    fmt.Printf("%p\n", &b) // 0x125a7d0
    fmt.Println("空结构体都相等")
    fmt.Printf("%d\n", unsafe.Sizeof(a)) // 0
    fmt.Printf("%d\n", unsafe.Sizeof(b)) // 0
  }
}  

发现空结构体的地址居然是一样的,而且占用的内存大小是0,很显然是被编译器优化过了,结构体其实可以嵌套,但是空结构体嵌入空结构体,仍然不占用空间。空接口还能表示世间万物,那空结构体有什么用处呢? 下面是一个节省内存的例子,使用 map 来实现 set 结构,set 是指元素不会重复的集合,由于空结构体不占用内存,因此它是一个很好的实现。

 func Test_set(t *testing.T) {
  // type Set map[string]struct{}{}
  set := map[string]struct{}{}
  set["one"] = struct{}{}
  set["two"] = struct{}{}
  set["three"] = struct{}{}
  fmt.Println(set["notfound"])
}  

在并发编程中,空结构体还经常作为通道 chan 的值。由于目前还没有学习 “面向对象、并发编程” 的知识,所以对 struct、interface、chan 暂时不再深入了,但是记住他们是 go 最重要的部分。

本章节的代码

相关文章