Go语言与数据库开发:01-04
讲完基础数据类型之后,我们接着学习复合数据类型。
复合数据类型,它是以不同的方式组合基本类型可以构造出来的复合数据类型。
几个基本的复合数据类型:
数组,是由同构的元素组成——每个数组元素都是完全相同的类型;
结构体,则是由异构的元素组成的;
数组和结构体都是有固定内存大小的数据结构;
slice和map则是动态的数据结构,它们将根据需要动态增长;
数组:
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是
Slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活,但是要理解slice工作原
理的话需要先理解数组。
数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位
置。内置的len函数将返回数组中元素的个数。
var a[3] int // array of 3 integers
fmt.Println(a[0]) // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]
// Print the indices and elements.
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
// Print the elements only.
for _, v := range a {
fmt.Printf("%d\n", v)
}
默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始
化值的个数来计算。因此,上面q数组的定义可以简化为
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我
们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候
数组才是相等的。不相等比较运算符!=遵循同样的规则。
当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数
参数变量接收的是一个复制的副本,并不是原始调用的变量。
因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改
都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。在这个方面,Go语言对
待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针
对象传入被调用的函数。
当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以
直接反馈到调用者。
虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依
然是僵化的类型,因为数组的类型包含了僵化的长度信息。
数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。
slice
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作
[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序
列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分
构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的
是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不
能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分
别返回slice的长度和容量。
多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。
如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了
slice,因为新slice的长度会变大。
x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。
因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底
层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名。
一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都
是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。
我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。
内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情
况下,容量将等于长度。
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引
用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice
只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增
长用的。
内置的append函数用于向slice追加元素:
var runes []rune
for _, r := range "Hello, 世界" {
runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次
扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间
是一个常数时间。
map
哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key
都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别
对应key和value。
map中所有的key都有相同的类型,所有的value也有着相同的类型,但是
key和value之间可以是不同的数据类型。
内置的make函数可以创建一个map:
ages := make(map[string]int) // mapping from strings to ints
我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
另一种创建空的map的表达式是 map[string]int{}
Map中的元素通过key对应的下标语法访问:
ages["alice"] = 32
fmt.Println(ages["alice"]) // "32"
使用内置的delete函数可以删除元素:
delete(ages, "alice") // remove element ages["alice"]
但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践
中,遍历的顺序是随机的,每一次遍历的顺序都不相同。
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践
中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍
历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我
们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。
map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它
们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常。
在向map存数据前必须先创建map。
通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到
与key对应的value;如果key不存在,那么将得到value对应类型的零值。
结构体
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结
构体的成员。
type Employee struct {
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
var dilbert Employee
dilbert结构体变量的成员可以通过点操作符访问。
或者是对成员取地址,然后通过指针访问:
position := &dilbert.Position
*position = "Senior " + *position // promoted, for outsourcing to Elbonia
点操作符也可以和指向结构体的指针一起工作:
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
相当于下面语句
(*employeeOfTheMonth).Position += " (proactive team player)"
如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决
定的。一个结构体可能同时包含导出和未导出的成员。
结构体类型往往是冗长的,因为它的每个成员可能都会占一行。
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。
(该限制同样适应于数组。)但是S类型的结构体可以包含 *S 指针类型的成员,这可以让我
们创建递归的数据结构,比如链表和树结构等。
结构体类型的零值是每个成员都对是零值如果考虑效率的话,
较大的结构体通常会用指针的方式传入和返回。
如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函
数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回
结构体的地址:
pp := &Point{1, 2}
它是下面的语句是等价的
pp := new(Point)
*pp = Point{1, 2}
匿名成员:
Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就
叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构
体,同时Circle类型被嵌入到了Wheel结构体。
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
最后更新:2017-08-15 15:02:45