Go语言学习(四)
第4章 复合数据类型
4.1 数组
数组时具有固定长度且拥有零个或多个相同数据类型的序列。由于数组的长度固定,所以在Go里面很少直接使用。
- 创建
var var_name [length]type
- 初始化可以使用数组字面量根据一组值来初始化一个数组
var q [3]int = [3]int{1, 2, 3}
- 索引访问
- 内置函数len返回数组元素个数
- 在数组字面量中,如果省略号“……”出现在数组长度的位置,那么数组的长度由初始化数组的元素个数决定。实例如下:
q := [...]int{1,2,3}
- 数组长度是数组类型的一部分,所以
[3]int
和[4]int
是两种不同的数组类型。数组的长度必须是常量表达式,也就是说,这个表达式的值在程序编译时就可以确定。q := [3]int{1, 2, 3} q = [4]int{1, 2, 3, 4}//编译错误:不可以将[4]int赋值给[3]int
- 数组、slice、map和结构体的字面语法都是相似的。创建数组的另一种写法如下
type Currency int const ( USD Currency = iota EUR GBP RMB ) symbol := [...]string{USD:"$", EUR:"€",GBP:"£",RMB:"¥"} fmt.Println(RMB,symbol[RMB]) // "3 ¥"
在这种情况下,索引可以按照任意顺序出现,并且有的时候还可以省略,未指定的位置的值为对应数据类型的零值
r := [...]int{99: -1}//定义了一个100各元素的数组r,除了最后一个元素是-1外,该数组中的其他元素值都是零
- 如果一个数组的元素类型是可以比较的,那么这个数组也是可以比较的,使用
==
和!=
进行比较
4.2 slice
slice标识一个拥有相同类型元素的可变长度的序列。slice通常写成[]T,其中元素的类型都是T;它看上去像没有长度的数组类型。
- slice是一种轻量级的数据结构,可以用来访问数组的部分或者全部的元素,而这个数组称为slice的底层数组
- slice有三个属性:指针、长度和容量。
- 指针指向数组的第一个可以从slice中访问的元素,这个元素并不一定是数组的第一个元素。
- 长度是指slice中的元素个数,他不能超过slice的容量。
- 容量的大小通常是从slice的起始元素到底层数组的最后一个元素间元素的个数
- Go的内置函数len和cap用来返回slice的长度和容量
- 一个底层数组可以对应多个slice,这些slice可以引用数组的任何位置,彼此之间的元素还可以重叠。
slice操作符s[i:j](其中0≤i≤j≤cap(s))创建了一个新的slice,这个新的slice引用了序列s中从i到j-1索引位置的所有元素,这里的s既可以是数组或者是指向数组的指针,也可以是slice。新的slice的元素个数是j-1个。
- 如果slice的引用超过了被引用对象的
容量
,即cap(s),那么会导致程序崩溃;但是如果slice的引用超过了被引用对象的长度
,即len(s),那么最终slice会比原slice长 - 在这里要注意的是字符串(string)的子串操作和对字节slice([]byte)做slice操作这两者的相似性。它们都写作x[m:n],并且都返回原始字节的一个子序列,同时它们的底层引用方式也是相同的,所以两个操作都耗费常量时间。区别在于:如果x是字符串,那么x[m:n]返回的是一个字符串;如果x是字节slice,那么返回的结果是字节slice
- slice包含了指向数组元素的指针,所以将一个slice传递给函数的时候,可以在函数内部修改底层数组的元素。创建一个数组的slice相当于为数组创建了一个别名。
- 注意初始化slice s的表达式和初始化数组 a的表达方式的区别:初始化slice的
[]
中没有数字表示不定长度,初始化数组的[]
中需要数组,表示定长。 这种隐式区别的结果分别是创建you固定长度的数组和创建指向数组的slice - 和数组不同的是,slice无法做比较,因此不能用==来测试两个slice是否拥有相同的元素。标准库里面提供了高度优化的函数
bytes.Equal
来比较两个字节slice([]byte)
。但是对于其他类型的slice,我们必须自己写比较函数,如下:func equal(x, y []string) bool{ if len(x) != len(y) { return false } for i :range x { if x[i] != y[i] { return false } } return true }
为什么
slice
不能直接使用==
比较?- a.和数组元素不同,slice的元素是非直接的,有可能slice包含它自身。虽然有办法处理这种特殊的情况,但是没有一种方法是简单、高效、直观的。
- b.因为slice的元素不是直接的,所以底层数组元素改变,同一个slice在不同的时间会拥有不同的元素。由于散列表(例如Go的map类型)仅对元素的键做浅拷贝,这就要求散列表里面键在散列表的整个生命周期内不许保持不变。因为slice需要深度比较,所以就不能用slice作为map的键。对于引用类型,例如指针和通道,操作符检查的是
引用相等性
,即他们是否指向相同的元素。如果有一个相似的slice相等性比较功能,它或许会比较有用,也能解决slice作为map键的问题,但是如果操作符对slice和数组的行为不一致,会带来困扰。所以最安全的方法就是不允许直接比较slice
- c.slice唯一允许的比较操作是和nil作比较
要想检查一个slice是否为空,那么使用len(s) == 0,而不是s == nil,因为s != nil 的情况下,slice也有可能是空。
除了可以和nil作比较之外,值为nil的slice表现和其他长度为零的slice一样。例如,reverse函数调用reverse(nil)也是安全的。除非文档上面写明了与此相反,否则无论值是否为nil,Go的函数都应该以相同的方式对待所有长度为零的slice - 内置函数make可以创建一个具有指定元素类型、长度和容量的slice。其中容量参数可以省略,在这种情况下,slice的长度和容量相等。
其实make创建了一个无名数组并返回了它的一个slice;这个数组仅可以通过这个slice来访问。在上面的第一行代码中,所返回的slice引用了整个数组。在第二行代码中,slice只引用了数组的前len个元素,但它的容量是数组的长度,这为未来的slice元素留出空间。make([]T,len) make([]T,len,cap) // 和 make([]T, cap)[:len]功能相同
4.2.1 append函数
- 内置函数append用来将元素追加到slice的后面
var runes []rune for _, r := range "Hello, 世界" { runes = append(runes, r) } fmt.Printf("%q\n",runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
- 一个解释append基本原理的例子
func appendInt(x []int, y int) []int { var z []int zlen := len(x) + 1 if zlen <= cap(x){ //slice仍有增长空间,扩展slice内容 z = x[:zlen] ] else { //slice已无空间,为它分配一个新的底层数组 //为了达到分摊线性复杂度,容量扩展一倍 //重点:当slice长度不足时,更新后的容量将会翻倍增长 zcap := zlen if zcap < 2*len(x){ zcap = 2 * len(x) } z = make([]int, zlen, zcap) copy(z, x) //内置copy函数 } z[len(x)] = y return z }
- 内置的append函数使用了比这里的appendInt更复杂的增长策略。通常情况下我们并不清楚一次的append调用会不会导致一次新的内存分配,所以我们不能假设原始的slice和调用append后的结果slice指向同一个底层数组,也无法证明他们就指向不同的底层数组。同样,我们也无法假设旧slice上对元素的操作会不会影响新的slice元素。所以我们将append的调用结果再次赋值给传入append函数的slice:
runes = append(runes, r)
不仅仅是在调用append函数的情况下需要更新slice变量。另外,对于任何函数,只要有可能改变slice的长度或者容量,抑或是使得slice指向不同的底层数组,都需要更新slice变量。为了正确的使用slice,必须记住,虽然底层数组的元素是间接引用的,但是slice的指针长度和容量不是。要更新一个slice的指针,长度或容量必须使用
runes = append(runes, r)
的显式赋值。从这个角度看,slice并不是纯引用类型,而是像下面这种聚合类型:type IntSlice struct{ ptr *int len,cap int }
- append实例
var x []int x = append(x, 1) x = append(x, 2, 3) x = append(x, 4, 5, 6) x = append(x, x...) //追加x中的所有元素 fmt.Println(x) //"[1 2 3 4 5 6 1 2 3 4 5 6]"
append函数的第二个参数支持接收一个
可变长度参数列表
这里使用的 … 表示将一个slice转换为参数列表
4.2.2 slice就地修改
- slice可以用来实现栈
- 给定一个空的slice元素stack
stack =append(stack, v) //push v top := stack[len(stack)-1] //栈顶 stack = stack[:len(stack)-1] //pop //从slice中间移除一个元素,使用copy函数 func remove(slice []int, i int) []int { copy(slice[i:],slice[i+1:]) return slice[:len(slice)-1] } func main(){ s := []int{5, 6, 7, 8, 9} fmt.Println(remove(s,2)) // "[5 6 8 9]" } //从slice中间移除一个元素,如果不需要维持slice中剩余元素的顺序, //可以简单地将slice的最后一个元素赋值给被移除元素所在的索引位置 func remove(slice []int, i int) []int { slice[i] = slice[len(slice)-1] return slice[:len(slice)-1] }
- 给定一个空的slice元素stack
4.3 map
散列表是一个拥有键值对元素的无序集合。在这个集合中,键的值是唯一的,键对应的值可以通过键来获取、更新或移除。无论这个散列表有多大,这些操作基本上是通过常量时间的键比较就可以完成。
-
内置函数make可以用来创建一个map:
ages = make(map[string]int) //创建一个从string到int的mao
使用map的字面量来新建一个带初始化键值对元素的字典:
ages := map[string]int{ "alice": 31 "charlie": 34 } //等价于 ages = make(map[string]int) ages["alice"] = 31 ages["charlie"] = 34
map元素的访问也是通过下标
使用内置函数delete性字典中根据键移除一个元素:delete(ages, "alice") //移除元素
即使键不在map中,上面的操作也是安全的。map使用给定的键来查找元素,如果对应的元素不存在,就返回值类型的零值。如下:
ages["Bob"] = ages["Bob"] + 1
快捷赋值方式(x+=y和x++)对map元素同样适用
ages["Bob"] += 1 ages["Bob"] ++
-
map元素不是一个变量,不可以获取它的地址,这样是不对的:
_ = &ages["Bob"] //编译错误,无法获取map元素的地址
原因:map的增长可能会导致已有元素被重新散列到新的存储位置,这样可能使得获取的地址无效
-
使用for循环遍历map
for name, age := range ages
import "sort" var names []string for name := range ages { names = append(names, name) } sort.Strings(names) for _, name := range names { fmt.Printf("%s\t%d\n",name,ages[name]) }
HINT:因为一开始就知道slice name的长度,所以指定一个slice的长度会更加高效(slice长度不足时,内存分配翻倍增长)。如下
names := make([]string, 0, len(ages))
创建了一个初始元素为空但是容量足够容纳ages map中所有键的slice -
map类型的零值是nil
大多数的map操作都可以安全地在map的零值nil上执行,包括查找元素,删除元素,获取map的元素个数(len),执行range循环,因为这和空map的行为一致,但是向零值map中设置元素会导致错误:
ages["carol"]=21
报错:我的理解是这样的,零值nil相当于是声明了一个散列表指针(map是散列表的引用),但是并没有和相应的内存挂钩,但是空值map已经和某块内存建立起了联系
所以设置元素之前必须进行初始化 -
通过下标访问map总是会得到一个值(零值或真正存在的值),但是如何确定一个元素在不在map中?
age, ok := ages["Bob"] if !ok {/*"Bob"不是字典中的键,age == 0*/} //通常这两条语句合并为一条语句,如下: if age, ok := ages["Bob"]; !ok {/*"Bob"不是字典中的键,age == 0*/}
这种方式下访问map,返回两个值,第一个是对应的值age,另一个是一个Boolean类型的值,用来报告该元素是否存在。一般使用变量名ok来表示(约定俗成)
-
和slice一样,map中唯一合法的比较是和nil作比较
func equal(x, y map[string]int) bool { if len(x) != len(y) { return false } for k, xv := range x { if yv, ok := y[k]; !ok || yv != xv { return false } } return true }
HINT:重点是使用ok判断map中是否存在一个元素
-
Go中没有提供集合类型,但是map的键是唯一的,我们可以使用map实现集合类型功能。
func main() { seen := make(map[string]bool) //字符串集合 input := bufio.NewScanner(os.Stdin) for input.Scan() { line := input.Text() if !seen[line] { seen[line] = true fmt.Println(line) } } if err := input.Err(); err != nil { fmt.Fprintf(os.Stderr,"dedup: %v\n", err) os.Exit(1) } }
HINT:使用map的键储存字符串,map键对应的值储存这个字符串是否存在,bool类型的零值是false
-
如果map的键是slice类型,我们需要使用建立一个帮助函数k将每一个键对应的slice转换为字符串(map的键必须是一个可比较类型的元素,但是slice不可比较),将对应的字符串作为map的键,查询map[slice]可以使用map[k(slice)]
同样的方法使用不其他不可直接比较的类型
实例:使用map统计输入中Unicode代码点出现的次数
package main import ( "bufio" "fmt" "io" "os" "unicode" "unicode/utf8" ) func main() { counts := make(map[rune]int) //type rune = int32 counts储存Unicode字符数量 var utflen [utf8.UTFMax + 1]int //UTF-8编码的长度 invalid := 0 //非法UTF-8字符数量 in :=bufio.NewReader() for { r, n, err := in.ReadRune() //返回rune、nbytes、error if err == io.EOF { break } if err != nil { fmt.Fprintf(os.Stderr, "charcount: %v\n", err) os.Exit(1) } if r == unicode.ReplacementChar && n==1 { invalid++ continue } count[r]++ utflen[n]++ } fmt.Printf("rune\tcount\n") for c,n := range counts { fmt.Printf("%q\t%d\n",c,n) } fmt.Print("\nlen\tcount\n") for i,n := range utflen { if i > 0 { fmt.Printf("%d\t%d\n", i, n) } } if invalid > 0 { fmt.Printf("\n%d invalid UTF-8 characters\n", invalid) } }
-
map的值类型可以是复合数据类型,例如map或slice
var graph = make(map[string]map[string]bool)
键是
string
类型,值是map[string]bool
类型
4.4 结构体
结构体是将零个或多个任意类型的命名变量组合在一起的聚合数据类型。每个变量都叫做结构体的成员。
下面的语句定义了一个叫Employee的结构体和一个结构体变量dilbert:
type Employee struct {
ID int
Name string
Address string
Dob time.Time
Position string
Salary int
MangerID int
}
var dilbert Employee
-
dilbert的每一个成员通过点号方式来访问和修改,
dilbert.Name
-
或者使用指针访问
position := &dilbert.Position *position = "Senior " + *Position
-
点号同样可以使用在结构体指针上
var employeeOfTheMonth *Employee = &dilbert employeeOfTheMonth.Position += " (proactive team player)" //后面一条语句等价于: (*employeeOfTheMonth).Position += " (proactive team player)"
-
函数返回结构体指针
-
结构体的成员变量通常一行一个,变量的名称在类型的前面,但是相同类型的连续成员变量可以写在一行上
type Employee struct { Name,Address string }
成员变量的顺序对于结构体同一性很重要,如果两个结构体类型有相同的成员变量但是成员变量的顺序不同,这两个结构体类型是不相同的
-
如果一个结构体的成员变量名称首字母是大写的,那么这个变量是可导出的,这是Go最主要的访问控制机制
-
结构体类型为两种,一种是匿名结构体类型(每次使用都得重写一遍定义),另一种是命名结构体类型(type <name> struct {结构体定义})
-
命名结构体类型s不可以定义一个拥有相同结构体类型s的成员变量,也就是一个聚合类型不可以包含它自己(同样的限制对数组也适用)。但是s中可以定义一个s的指针类型,即*s,这样就可以创建一些递归数据结构,比如链表和树
type tree struct{ value int left,right *tree }
-
结构体的零值由结构体成员的零值组成。
-
空结构体
4.4.1 结构体字面量
结构体类型的值可以通过结构体字面量
来设置
//第一种结构体字面量
type Point struct{ X, Y int }
p := Point{1, 2}
//这种结构体字面量必须按照正确的顺序赋值
//第二种是关键字参数传递
p := Point{X:2}
//这是p.X=2,,p.Y没有被赋值,p.Y=0(int 类型的零值)
- 结构体类型的值可以作为参数传递给函数或者座位函数的返回值
- 出于效率的考虑,大型的结构体通常都使用结构体指针的方式直接传递给函数或从函数中返回
- 由于通常接固体都使用指针的方式调用,因此可以使用一种简单的方式来创建、初始化一个struct类型的变量并获取它的地址:
pp := &Point{1,2}
//等价于
pp := new(Point)
*pp = Point{1,2}
//&Point{1,2}这种方式可以直接使用在一个表达式中,例如函数调用。
4.4.2 结构体比较
- 如果结构体的所有成员变量都可以比较,那么这个结构体就是可以比较的。两个结构体的比较可以使用
==
或!=
。 - 其中
==
操作符按照顺序比较两个结构体变量的成员变量,这就解释了为什么成员变量定义顺序不同的两个结构体是不同的结构体 - 可比较的结构体类型可以作为map的键类型
4.4.3 结构体的嵌套和匿名成员
结构体嵌套机制
可以让我们将一个命名结构体当做另一个结构体类型的匿名成员
使用;并提供了一种方便的语法,使用简单的表达式(比如x.f)就可以代表连续的成员(x.d.e.f)
type Point struct {
X, Y int
}
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Cirlcle
SPokes int
}
//访问Wheel的成员:
var w Wheel
w.Circle.Center.X //麻烦
//-----------------------
我们使用匿名成员来构建Wheel结构体
//匿名成员
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Cirlcle
SPokes int
}
//访问Wheel的成员
var w Wheel
w.X = 8 //很方便
但是,结构体字面量没有快捷方式初始化结构体,所以下面的语句错误:
w = Wheel{8,8,5,20}
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20}
结构体字面量必须遵循类型的定义,下面的两种方式初始化结构体类型:
//一
w = Wheel{Circle{Point{8,8}, 5}, 20}
//二
w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, //注意,尾部的逗号是必须的
}
fmt.Printf("%#v\n", w)
//输出
//Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
1.副词#使Printf的格式化符号%v以类似Go语法的方式输出对象,这个方式里面包含了成员变量的名字
2."匿名成员"拥有隐式的名字,所以不能在一个结构体里面定义两个相同类型的匿名成员,否则会因起冲突。
4.5 JSON
JavaScript对象表示法(JSON)是一种发送和接收格式化信息的标准。JSON不是唯一的标准,XML、ASN.1和Google的Protocol Buffer都是相似的标准,各自有适用的场景。但是因为JSON的简单、可读性强并且支持广泛,所以使用得最多。
Go通过标准库encoding/json、encoding.xml、encoding/asn1和其他的库对这些格式的编码和解码提供了非常好的支持,这些库都拥有相同的API。
JSON是JavaScript值的Unicode编码,这些值包括字符串、数字、布尔值、数组和对象。JSON十几本数据类型和复合数据类型的一种高效的、可读性强的表示方法。
JSON最基本的类型是数字(以十进制或者科学计数法表示)、布尔值(true或false)和字符串。字符串是用双引号括起来的Unicode代码点的序列,是用反斜杠作为转义字符,通过和Go类似的方式访问成员。JSON里面的\uhh数字转义得到的是UTF-16编码,而不是Go里面的字符。
-
把Go转换成JSON称为marshal。如下:
data, err := json.Marshal(movies) if err != nil { log.Fatalf("JSON marshaling failed: %s",err) } fmt.Printf("%s\n", data)
Marshal生成了一个字节slice,其中包含一个不带有任何空白多余字符的很长的字符串。
看起来不方便阅读,我们可以使用MarshalIndent的变体可以输出整齐格式化的结果。这个函数有两个参数,一个是定义每行西湖出的前缀字符串,另外一个是定义缩进的字符串。data, err := json,MarshalIndent(movies, "", " ") if err != nil { log.Fatalf("JSON marshaling failed: %s", err) } fmt.Printf("%s\n",data)
上面的代码输出:
这里的omitempty
指的是如果这个成员的值是零值或者为空,则不输出这个成员到JSON中 -
将JSON数据解码为GO数据结构
使用marshal的逆操作,使用unmarshal,使用json.Unmarshal实现的。var titles []struct{ Title string} if err := json.Unmarshal(data, &titles); err != nil { log.Fatalf("JSON unmarshaling failed: %s", err) } fmt.Println(titles) // "[{Casablanca}{Cool Hand Luke}{Bullitt}]"
因为结构体slice的唯一成员是Title,并没有其他的成员,所以JSON中的其他字段将会丢失。
在unmarshal阶段,JSON字段的名称关联到Go结构体成员的名称是忽略大小写的。
除了使用json.Unmarshal来将整个字节slice解码为单个JSON实体之外。还有流式解码器(即json.Decoder),可以使用它来依次从字节流中解码出多个JSON实体,还有一个json.Encoder的流式编码器