《GO语言圣经》读书笔记(三):复合数据类型

ch4:复合数据类型

​ 数组和结构体都是有固定内存大小的数据结构,而对于切片slicemap来说,它们是动态的数据结构,它们可以根据需要进行动态增长。

4.1 数组

​ 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。不过Go中很少使用数组,因为数组无法根据需要进行动态扩展,我们常使用切片slice代替数组的使用。

​ Go中的数组和其他语言中的数组有很多共通的地方,下面来总结和回顾一下:

  • 数组中的每个元素可以通过索引下标来访问,如果数组的长度用变量len表示,那么索引下标的范围就是[0,len-1],Go中提供了len函数可以帮助我们统计数组中元素的个数。

  • 默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型说就是0,当然也可以使用一组值来初始化数组。这里还是举个例子,进一步的说明这句话是什么意思吧:

    var q [3]int=[3]int{1,2,3}
    var r [3]int=[3]int{1,2}
    fmt.Println(r[2])  //"0"
    
  • 如果声明数组的时候不想写它的长度怎么办,可以在表示数组长度位置的地方用“…”替代,比如q:=[...]int{1,2,3}

​ 要特别说明的是,数组的长度是数组类型的一个组成部分,所以[3]int[4]int是两种不同的数组类型,由于数组的长度需要在编译期确定,所以长度必须是常量表达式。

​ 下面定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其他没有指定初始值的元素都是用零值来初始化。

r:=[...]int{99:-1}

​ 如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,我们可以通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候,数组才是相等的。

​ 当调用一个函数的时候,函数的每个调用参数都会被赋值给函数内部的参数变量,函数的参数变量实际上接收的是一个副本,函数内对副本的修改不会对原始的变量造成影响,对于传递参数为数组的情况,也是这样的,如果给函数传入一个指针,那么对副本进行修改的同时,也会影响调用时原始的数组变量。

func main() {
	var ptr [4]byte = [4]byte{1: 1}
	fmt.Println("before zero ptr:", ptr)
	zero(ptr)
	fmt.Println("after zero ptr:", ptr)

	fmt.Println("before zero2 ptr:", ptr)
	zero2(&ptr)
	fmt.Println("after zero2 ptr:", ptr)
}

func zero(ptr [4]byte) {
	for i := range ptr {
		ptr[i] = 0
	}
	fmt.Println("zero func,ptr:", ptr)
}

func zero2(ptr *[4]byte) {
	for i := range ptr {
		ptr[i] = 0
	}
	fmt.Println("zero2 func,ptr:", *ptr)
}

Output:

before zero ptr: [0 1 0 0]
zero func,ptr: [0 0 0 0]
after zero ptr: [0 1 0 0]
before zero2 ptr: [0 1 0 0]
zero2 func,ptr: [0 0 0 0]
after zero2 ptr: [0 0 0 0]

​ 虽然用指针来传递数组参数允许在函数内部修改数组的值,但是如果我们想传入一个[16]byte类型的数组指针,就不可以了。因为数组的长度是固定的,其次长度也是数组类型的一部分。所以,一般使用slice替代数组。

4.2 Slice

Slice和数组很像,不过Slice的长度是可以动态扩展的,同时,序列中的每个元素都有相同的类型。

​ Go中的切片有以下几种初始化的方式:

  • 使用字面量:slice := []int{1, 2, 3}

    这就相当于在编译期间做了这样几件事情:

    1. 根据切片中的元素数量对底层数组的大小进行推断并创建一个数组;
    2. 将这些字面量元素存储到初始化的数组中;
    3. 创建一个同样指向 [3]int 类型的数组指针;
    4. 将静态存储区的数组 vstat 赋值给 vauto 指针所在的地址;
    5. 通过 [:] 操作(使用下标创建切片)获取一个底层使用 vauto 的切片;
    var vstat [3]int
    vstat[0] = 1
    vstat[1] = 2
    vstat[2] = 3
    var vauto *[3]int = new([3]int)
    *vauto = vstat
    slice := vauto[:]
    
  • 使用make关键字:slice := make([]int, 10),10是切片的容量。

​ 使用切片时访问元素呢?

  • 通过切片的长度和容量,或者直接访问其中的元素

  • range遍历切片。

    arr := []int{1, 2, 3, 4}
    for i := 0; i < len(arr); i++ {
        fmt.Println(arr[i])
    }
    println("------------------------")
    for i := 0; i < cap(arr); i++ {
        fmt.Println(arr[i])
    }
    println("------------------------")
    fmt.Println(arr[2])
    println("------------------------")
    for k, v := range arr {
        fmt.Println(k, v)
    }
    

Slice支持访问数组的子序列或者全部元素,在底层引用了一个数组对象。可以从三个部分描述Slice,指针、长度和容量。

  • 指针指向第一个slice元素对应的底层数组元素的地址,slice的第一个元素并不一定是数组的第一个元素,长度对应slice中元素的数目,长度不能超过容量,容量是指从切片的开始位置到底层数据结构的结尾位置,go提供的内置len()cap()函数分别返回切片的长度和容量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v9kwUHmp-1593333966355)(C:\Users\nayelyawang\AppData\Roaming\Typora\typora-user-images\image-20200618111349667.png)]

​ 如果切片操作超出cap(s)的上限,将导致panic异常,但是超出了len(s)则意味着扩展了slice,因为新slice的长度会变大。

slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的指针。

arr := [4]int{1, 2, 3, 4}
slice := arr[0:2]
fmt.Println(arr)
slice[1] = 155
fmt.Println(arr)

输出:

[1 2 3 4]
[1 155 3 4]

可以看到数组arr[1]的值从2变成了155,所以切片可以修改底层数组的值。

注意:

  • slice之间不能比较,因为不能使用==操作,但是可以使用bytes.Equal函数判断字节型slice,其他类型的slice需要自己展开每个元素比较。一个固定的slice不同时间可能包含不同的元素,因为底层指向的数组元素可能会被修改,所以这也是不能比较的原因指引。

  • 一个零值的slice等于nil,一个nil值的slice没有底层数组。

  • append函数可以向slice追加元素。

    var runes []rune
    for _, r := range "Hello, 世界" {
    runes = append(runes, r)
    } f
    mt.Printf("%q\n", runes) 
    // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']
    

4.3 Map

​ 在Go中,一个map是一个哈希表的引用,map类型可以写为map[K]V,其中K和V就是key和value。一个map中,所有key都是相同的类型,所有value也都是相同的类型,key和value可以是两个不同的数据类型。

​ 如果想测试某个key是不是存在的,可以通过==来判断。

​ 怎么创建map呢,第一种方式可以通过内置的make函数创建一个map

ages:=make(map[string]int)

​ 也可以通过字面值的方式创建,与此同时,还可以指定一些key-value对。

ages:=map[string]int{
    "alice":31,
    "lolo":34,
}

​ 上面这种通过字面值的方式相当于:

ages:=make(map[string]int)	
ages["alice"]=31
ages["charlie"]=34

​ 可以通过ages["alice"]去访问对应的值,使用delete(ages,"alice")可以删除元素。

​ 知道怎么创建、赋值和访问之后,问题来了,如果要遍历mao中的全部key/value的话,要怎么做?可以使用range风格的for循环实现。

for name,age:=range ages{
    fmt.Printf("%s\t%d\n",name,age)
}

​ 这里特别要说的是,**Map的迭代顺序是不确定的!**这里结合一个例子来说下,由于不清楚map迭代顺序导致的坑!

​ 再重构儿童服务的时候,需要顺序遍历存有接种信息的map,要求key是顺序的(后来问了测试的同学才知道在前端正确的显示方式是什么样的),然而一开始不清楚golang中map的迭代顺序是不确定的,直接使用了两层for range处理map,导致返回给前端的data数据没有按照年龄结合接种状态进行显示,今天早上发现了golang这个坑人的地方,于是找到了一种解决方法。

​ 如果要按顺序遍历键值对,需要显式对key排序,使用sort包中的函数就可以了,这里对类型为int的key排序,选择sort包中的Ints()函数就可以了。

​ 这里用sorted_keys表示一个int类型的切片,然后再第一个range循环中,只关心map中的key,所以第二个循环遍历可以忽略,第二个range中只关心sorted_keys的值,所以第一个循环遍历用_表示。

package main

import (
	"fmt"
	"sort"
)

func main() {
	m := make(map[int]string)
	m[0] = "echo hello"
	m[1] = "echo world"
	m[2] = "echo go"
	m[3] = "echo is"
	m[4] = "echo cool"

	sorted_keys := make([]int, 0)
	for k, _ := range m {
		sorted_keys = append(sorted_keys, k)
	}
	sort.Ints(sorted_keys)

	for _, k := range sorted_keys {
		fmt.Printf("k=%v, v=%v\n", k, m[k])
	}
	println("------------------------")
	for k, v := range m {
		fmt.Printf("k=%v, v=%v\n", k, v)
	}

}

输出:

[root@cdb63e69049f ~/go/src/MathDemoServer/MathDemoServer]# go run main.go
k=0, v=echo hello
k=1, v=echo world
k=2, v=echo go
k=3, v=echo is
k=4, v=echo cool
------------------------
k=3, v=echo is
k=4, v=echo cool
k=0, v=echo hello
k=1, v=echo world
k=2, v=echo go

​ 如果需要map的value是个map或者slice也是可以的,比如map<string,map<string,int>>使用go表示起来就是map[string]map[string]bool

使用注意汇总:

  • map中的元素不是一个变量,不能对map的元素进行取址操作;_=&ages["alice"],map可能随着元素数量的增长而重新分配更大的内存空间,从而导致之前的地址无效。

  • 如果访问一个不存在的key或者删除一个不存在的key是不会出现panic。如果访问一个不存在的零值,那么将会得到value类型对应的零值。比如ages["Bob"]的结果就是0。

    func main(){
    	ages := make(map[string]int)
    	ages["alice"] = 31
    	ages["charlie"] = 34
    	fmt.Println(ages["Bob"])
    	delete(ages, "852")
    	fmt.Println(ages)
    }
    

    输出:

    0
    map[alice:31 charlie:34]
      ```
    
    问题又来了,如果有个key它是存在于map中的,但是它对应的value是0 ,那么和不存在的key对应的零值岂不是一样的。在这种情况下,我们可以通过下面这种方式去解决:
    
    ```go
    age,ok:=ages["bob"]
    if !ok{
        //bob is not a key in this map
    }
    

    使用上面这种解法,ok代表的是一个布尔值,用于表示这个元素是否真的存在,布尔变量命名一般是ok

    还需要补充的是,虽然大部分操作在map上的操作不会引发panic,由于map是引用类型,因此必须显示初始化,否则默认值是nil。像一个nil值的map存入元素就会导致panic。所以存数据之前必须先创建map,之前使用GORM进行CRUD的时候就吃过这个问题的亏233。

    var ages map[string]int
    fmt.Println(ages==nil)  //true
    fmt.Println(len(ages)==0) //true
    ages["kiki"]=21
    

    golint会显示SA5000: assignment to nil map (staticcheck)go-lint问题,运行也会报错。

  • map和map不能进行比较,唯一例外的是和nil进行比较,要判断两个map是否包含相同的key和value,可以通过循环实现。

    func equal(x,y map[string]int) bool{
        if len(x)!=len(y){
            return false
        }
        for k,sv:=range x{
            if yv,ok:=y[k];!ok||yv!=xy{
                return false
            }
        }
        return true
    }
    

​ 这个地方在判断的时候不能缺失!ok,为啥呢,需要用它来区分元素不存在还是相等的。

  • 如果需要map的key是slice类型的,要怎么解决?因为map的key必须是可比较的类型,而切片之前也提到过了,是不满足这个条件的。首先可以定一个辅助函数k,k函数的作用是将slice转化为map对应的string类型的key,在每次对map操作的时候先用辅助函数把slice转化为string类型。

    var m=make(map[string]int)
    
    func k(list []string) string{
        return fmt.Sprintf("%q",list)
    }
    func Add(list []string){
        m[k(list)]++
    }
    func Count(list []string) int{
        return m[k(list)]
    }
    

​ 举一反三,这样的处理方法不仅仅适用于切片,其他不可比较的key类型都可以。

4.4 结构体

​ 结构体可以由零个或多个任意类型的值(理解是结构体的成员也可以)共同组成。

type Employee struct{
    ID int
    Name string
    Address string
    Salary int
}

var dilbert Employee

​ 定义了一个Employee的结构体类型,然后声明了一个Employee类型的变量dilbert。访问该变量的成员通过.就行。

dilbert.Id=200  //给成员变量赋值
address:=&dilbert.Address //对成员取地址
*address="Senior"+*address //通过指针访问

​ 声明map的时候提到可以使用字面值,那结构体也是可以使用字面值的,例子如下:

type Point struct{
    X int
    Y int
}
p:=Point{1,2} //根据结构体成员定义的顺序为结构体成员指定值

​ 不过显然这就要求编码同学记住顺序了,更常用的方法是以成员的名字和相应的值来初始化:

p:=Point{
    X:1,
    Y:2,
}

​ 这种方式,可以包含全部或者部分的成员,被忽略的默认是零值,出现的顺序也无所谓。

​ 再来说下结构体可以干啥,结构体可以作为函数的参数和返回值。较大的结构体通常会使用指针的方式传入和返回。

func Bonus(e *Employee,percent int)int{
    return e.Salary*percent/100
}

​ 两个结构体是不是可以比较的呢?如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用或!=运算符进行比较,相等比较运算符会比较结构的每个成员。

type Point struct{ X, Y int }
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
fmt.Println(p == q)//false 等价于上面那一条语句

说明:

  • 结构体类型可以做map的key类型。

  • 函数内部修改结构体的成员,那么也是要必须传入指针的。因为是函数参数是值传递的。

  • 一个命名为S的结构体类型不能再包含S类型的成员,因为一个聚合的值不能包含自身(数组同理),但是可以包含*s指针类型的成员,基于此可以实现链表和树。

  • 如果结构体成员名字是以大写字母开头的,那么该成员可以在其他包中读写,否则就不可以了,可以在其他包中读写,我们称之为导出。一个结构体可以有导出的也可以不导出的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值