Go 语言圣经学习笔记-4.复合数据类型

2019-06-24

本博客所有文章采用的授权方式为 自由转载-非商用-非衍生-保持署名 ,转载请务必注明出处,谢谢。

声明:
本博客欢迎转发,但请保留原作者信息!
博客地址:王超的博客;
内容系本人学习、研究和总结,如有雷同,实属荣幸!

4.1 数组

数组具有固定长度,且拥有零个或者多个相同数据类型元素的序列。 数组中的每个元素通过索引访问,索引从0到长度减1,内置的len可以返回数组中元素的个数。 默认情况,数组中元素的初始值为元素类型的零值。数组字面量中,如果省略号出现在数组的位置,那么数组的长度由初始化的元素个数决定。

q := [...]int{1,2,3}

数组长度是数组类型的一部分,两个长度的数组是两种类型。数组也可以通过索引和索引对应的值,进行初始化:

r := [...]int{0:1,1:15,2:10,3:100}

如果数组的元素类型可比较,那么数组也是可以比较的,比较的结果为两边的元素的值是否完全相同。

函数调用时,参数会创建一个副本,而不是原始的参数。如果数组较大,传递就会低效,而且函数内的修改,仅修改副本,而不是原始数组。可以通过显示的传递一个数组的指针给函数。

func zero(ptr *[32]byte){
    *ptr = [32]byte{}
}

4.2 slice

slice表示拥有相同类型元素的可变长度的序列,看上去像没有长度的数组类型。 slice是一种轻量级的数据结构,用来访问数组的部分或者全部的元素。slice有三个属性:指针、长度和容量。指针指向数组第一个可以从slice访问的元素,长度是slice中的元素个数,不能超过slice的容量,容量的大小通常是从slice的起始元素到底层数组的最后一个元素间的个数。len和cap用来返回slice的长度和容量。

4.2 Slice

slice 表示一个拥有相同类型元素的可变长度的序列。slice是和数组紧密关联的,slice是一种轻量的数据结构,可以用来访问数组的部分或者全部的元素,而这个数组称为slice的底层数组。 slice有三个属性:指针、长度和容量。指针指向数组第一个可以从slice中访问的元素,而这个元素不一定是数组的第一个元素,长度是指slice中元素的个数,它不能超过slice的容量。容量的大小一般是底层数组的大小。Go的内置函数len和cap可以返回slice的长度和容量。

如果slice的引用超过被引用对象的容量,就会Panic,但是如果只是超出了被引用对象的长度,即len(s),那么最终slice会比原slice长。

因为slice包含指向数组元素的指针,所以将slice传递给函数的是和,可以在函数内部修改底层数组的的元素。 注意slice的初始化表达式和数组的区别。slice没有指定长度,这种隐式的区别是创建有固定长度的数组和创建指向数组长度的slice。

和数组不同的是,slice不能做比较,因此不能通过==来测试两个slice是否相同。这里有两个原因: 首先,slice的元素是非直接的,有可能slice可以包含自身,虽然可以处理这种特殊情况,但是没有一种是简单、高效、直观的。 其次,因为元素不是直接的,如果底层数组改变,同一slice在不同的时间,会拥有不同的元素。由于散列表(例如map)仅对元素的键做浅拷贝,这就要求散列表的键在整个生命周期内保持不变。因为slice需要深度比较,所以不能用作Map的键。所以最安全的方法就是不允许直接比较slice。 slice唯一允许的比较是和nil做比较,但是也有非nil的slice长度和容量是0的,所以如果检查一个slice是否为空,建议使用len(s) == 0,而不是s == nil。 make可以创建一个具有指定元素类型、长度和容量的slice.

make([]T,len)
make([]T,len,cap)

4.2.1 append函数

内置的append用来将元素追加到slice的后面。 append函数对于对于理解slice的工作原理很重要,每次append都必须检查是否仍有足够的容量存储数组中的新元素,如果容量足够,那么就定义一个新的slice(任然引用原始底层数组),然后插入新的元素,并返回。 如果容量不够,则必须创建一个拥有足够容量的新的底层数组来存储新元素,然后将元素从旧的slice复制到新数组,然后追加新元素。处于效率的考虑,新创建的数组容量会扩展一倍,来减少内存分配的次数。 内置的copy函数用来为两个拥有相同类型元素的slice复制元素。

为了正确的使用slice,必须记住,虽然底层数组的元素是间接引用的,但是slice的指针、长度和容量不是。要更新一个slice的指针,长度或容量必须显式赋值。

4.2.2 slice就地修改

对于slice的一些简单修改,可以重用底层数组,这样每一个输入值的slice最对中能有一个输出的slice结果。 slice[:0] 可以表示医用原始slice的新的零长度的slice。

4.3 map

散列表是设计精妙、用途广泛的数据结构之一。它是一个拥有键值对元素的无序集合。键值是唯一的,键对应的值可以通过键来获取、更新或者移除,无论散列表有多大,都可以通过常量时间的键比较完成。

go中,所有的键拥有相同的数据类型,值也拥有相同的数据类型,但是键和值的类型不一定相同。键的类型,必须是可以通过操作符==来进行比较的数据类型。 可以通过make创建一个map,也可以通过字面量创建一个字典:

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

空的map的表达方式是:map[string]int{}

可以通过内置delete函数从字典中移除一个元素:

delete(ages,"alice")

即使键不在map中,做增删改查也是安全的,map用键查找元素,如果不存在,则返回类型的零值。 但是map不是一个变量,不可以获取它的地址,因为map的增长有可能导致元素被重新散列到新的位置,导致获取的地址无效。

可以通过for循环遍历map,但是元素中的迭代顺序是不固定的,不同的方法会使用不同的散列算法,得到不同的顺序。

map类型的零值是nil,大多数map的操作都可以安全的在零值的map上执行,和空map的行为一致,但是向零值map插入元素会导致错误。

如果需要能够辨别一个元素是否存在,还是恰好这个元素的值是0,可以通过:

if age,ok := ages["bob"];!ok

通过下标访问map,第二个值是一个布尔值,来报告该元素是否存在。 和slice一样,map不可比较,唯一合法的比较就是和nil做比较,为了比较map是否拥有相同的键和值,必须写一个循环。

go没有提供集合类型,但是既然map的键都是唯一的,那么就可以使用map实现这个功能。

有时候,需要有一个Map,并且它的键是slice,但是slice不可比较,这个时候可以分两步走:1.定义一个帮助函数k将每一个键都映射为字符串,保证当x,y相同时,k(x)==k(y)。然后可以使用string作为map的key。

var m = make(map[string]int)

func k(list []string) string{ return fmt.Sprinf("%q",list)}

func Add(list []string){ m[k(list)]++ }
func Count(list []string){ return m[k(list)] }

同样的方法适用于任何不可直接比较的键类型,不仅仅局限于slice。同样,k(x)的类型不一定是字符串类型,任何能够得到想要的比较结果的类型都可以,例如整数、数组或者结构体。

map的值类型本身可以是复合数据类型,例如是map或slice。

4.4 结构体

结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型。每个变量都叫做结构体的成员。

type Employee struct{
    ID int
    Name string
    Address string
}
var dilbert Employee

可以通过dilbert.Name += “test”来给成员赋值,也可以通过指针来访问它:

name := &dilbert.Name
*name = "Test " + *name

点号也可以使用在结构体指针上:

var employee *Employee = &dilbert
employee.Name += "test"

结构体通常一行写一个,但是相同类型的连续成员变量可以写在一行,成员的变量顺序对于结构体的同一性很重要。 如果一个结构体的成员变量是首字母大小的,那么这个变量是可导出的。

命名结构体类型S不可以定义一个相同类型的成员变量,也就是一个聚合类型不可以包含自己。但是可以定义一个S的指针类型,实现递归数据结构,例如链表和树。

没有任何成员变量的结构体称为空结构体,写作struct{},它没有长度,也不携带任何信息,但是有时候会有用。

4.4.1 结构体字面量

有两种格式的结构体字面量,第一种,按照正确的顺序,为每个成员变量赋值,这会给开发和阅读代码的人造成负担。

type Point struct{X,Y int}
p := Point(1,2)

用的最多的是第二种,通过指定部分或者全部成员变量的名称来初始化。

anim := gif.GIFP{LoopCount: nframe}

如果某个变量没有指定,那么值就是该变量类型的零值。 结构体类型的值可以作为参数传递给函数或者作为函数的返回值,但是出于效率的考虑,大型 的结构体通常使用结构体指针的方式传递给函数或者从函数返回。

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

这种方式在修改结构体内容时也是有用的,因为go是按值调用,调用函数接受的是实参的一个副本,并不是实参的引用。

4.4.2 结构体比较

如果结构体的所有变量是可以比较的,那么这个结构体就是可比较的。比较结构体,会按照顺序比较结构体变量的成员变量。

4.4.3 结构体嵌套和匿名成员

Go允许我们定义不带名称的结构体成员,只需要指定类型即可,这种结构体成员称作匿名成员。下面的Circle和Wheel都拥有一个匿名成员,这里称Point被嵌套到Circle中,Circle被嵌套到Wheel中。

type Circle struct{
    Point
    Radius int
}
type Wheel struct{
    Circle
    Spokes int
}
var w Wheel
w.X = 8 //等价w.Circle.Point.X=8
w.Radius = 5 //等价w.Circle.Radius = 5

当我们访问最终的变量的时候,可以省略中间的所有的匿名成员。 但是遗憾的是,结构体字面量并没有快捷的办法来初始化结构体:

 w = Wheel{8,8,5,20} //编译错误
 w = Wheel{X:8,Y:8,Radius:5,Spokes:20} //编译错误
 w = Wheel{Circle{Point{8,8},5},20}

因为匿名成员拥有隐式的名字,所有一个结构体不能出现两个相同类型的你们成员。由于匿名成员的名字是由它们的类型决定的,因此可导出性也是由它们的类型决定的。 即使匿名成员的结构体是不可导出的(circle和point),但是仍然可以使用快捷方式:

w.X = 8
w.circle.point.X = 8//不允许

以快捷方式访问匿名成员内部变量同样适用于访问匿名成员的内部方法。

4.5 JSON

Go对象转为JSON还是从JSON转换为Go对象都很容易。把Go的数据结构转换为JSON成为marshal。 marshal是通过json.Marshal来实现的。

data,err := json.Marshal(movies)
if err != nil{
    log.Fatalf("JSON marshaling failed:%s",err)
}
fmt.Printf("%s\n",data)

只有可导出的成员可以转换为JSON字段。字段的转换是通过成员标签定义管理的。 成员标签定义是结构体成员编译器关联的一些元信息:

Year   int  `json:"released"`
Color  bool `json:"color,omitempty"`

成员标签定义可以是任意字符串,但是按照习惯,是由一串空格分开的标签键值对key:”value”组成。omitempty表示如果成员的值是零值或者空,则不输出到JSON。

marshal的逆操作是unmarshal。

4.6 文本和HTML模板

要求格式和代码彻底分离的场景,可以通过text/template包和html/template包里的方法来实现,这两个包提供了一种机制,可以将程序变量的值代入到文本或者HTML模板中。

模板是一个字符串或者文件,它包含一个或者多个两边用双大括号包围的单元,这称为操作。 每个操作在模板中对应一个表达式,提供功能包括:输出值,选择结构体成员,调用函数和方法,描述控制逻辑(不如If-else和range循环),实例化其他模板。

文章评论

comments powered by Disqus


章节列表