复合数据类型(构造类型)
map
golang map 是Hash表的引用,差不多就是PHP 关联数组或者 Python 字典,当然 C++ STL 也有map,但 golang map 应该是 unordered_map。map 所有键必须同类型,值也必须同类型,这一点并不像 PHP 关联数组那样随心所欲,另外,键必须是支持 == 进行比较的,所以通常它是一个原始基本类型。由于浮点比较的不精确性,所以用浮点数为键不是好主意(golang NaN可以是浮点)。
初始化一个map,类似如下:
ages := map[string]int {
"Alice": 31,
"Charlie": 34,
}
创建一个空的map,有两种方式
ages := map[string]int {}
// or
ages := make(map[string]int)
移除map的某个元素(键值对),可以使用内置函数 delete,如
delete(ages, "Alice")
可以安全地删除不存在的元素(键不存在),甚至可以安全地访问不存在的键(此时返回该类型的零值)或者修改它(此时先自动创建元素再修改),例如
ages["Bob"] = ages["Bob"] + 1
// or
ages["Bob"] += 1
// or
ages["Bob"]++
上面的代码,都会先主动创建键为Bob,值为0的元素,然后将值加1,这一点似乎比STL map要更夸张一点。
golang map的键是无序的,这和 STL map 用红黑树内部有序不同,如果确实需要按键排序,只能遍历map把键取出作为一个数组,对该数组排序再来索引map。
map 的元素访问,遍历都是自然的,只是不存在的键也可以访问(返回零值)。那么如何区分返回的恰好是0,还是因为键不存在返回了0?答案是访问键不仅返回值,还可以同时返回检索是否成功:
age, ok := ages["bob"]
if !ok { /* not key! */ }
// or
if age, ok := ages["bob"]; !ok { /* not key! */ }
和 slice 一样,除了用 == 或 != 和 nil 作比较,map 是不可比较的。确实要比较2个map,只能手动自己写函数,只是 map 允许访问不存在的键,给比较函数带来一个小坑
package main
import (
"fmt"
)
func main() {
map1 := map[string]int{"A": 0}
map2 := map[string]int{"B": 42}
fmt.Println(equal(map1, map2)) // false
}
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 { // y 中键 k 不存在也是不等的
return false
}
}
return true
}
golang 没有集合类型,但可以直接用 map[T]bool 来代替集合。当我们希望键是某种不可比较的类型(例如 slice)时怎么办?和前面的排序问题一样,变通实现,即引入辅助函数k(),确保当且仅当 x 和 y 相等时, k(x) == k(y),而且 k() 返回类型是可以比较的(通常是字符串类型),然后创建 map[k(T)]T‘形式的map
map 的值类型可以是复合类型,如 slice 或者又一个map,make(map[string]map[string]bool) 将创建键为string,值为map类型,值的map类型中的键为string,值为bool
结构体
golang结构体和C结构体比较相似,不过golang结构体成员可以是空的,另外,声明的时候是用定义新类型(类型别名)的方式进行的:
type Employee struct {
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
用该类型声明变量 var john Empoyee
访问成员 john.Salary -= 5000
指针访问 position := &john.Position; *position = "Senior " + *position
指向结构体的指针也用点号.访问!
var e *Empoyee = &john ; e.Position += " (proactive team player)"
结构体成员的名字和顺序很重要,在不改变顺序情况下,相同类型的成员可以简化写成一行。
golang 结构体有一个被王垠大神吐槽的访问控制机制,即成员变量名称首字母大写意味着该变量是可导出的,而小写的是私有的。这个和包全局变量的情况一致。
结构体初始化可以使用两种方式(短结构多用方式一,复杂结构多用方式二),但不能混用
package main
import (
"fmt"
)
func main() {
p1 := Point{1, 2} // 方式一
p2 := Point{Y: 2, X: 1} // 方式二,可不按顺序
p3 := Point{X: 1} // 方式二,部分成员给定值,其余默认0值
fmt.Println(p1) // {1 2}
fmt.Println(p2) // {1 2}
fmt.Println(p3) // {1 0}
}
type Point struct {
X, Y int
}
结构体类型作为函数参数,也是拷贝的(值传递),所以,对于大的结构体作为函数参数,一般使用指针方式。创建、初始化结构体变量并获取它的地址可以一步完成:
pp := &Point{1, 2}
// 等价于
pp := new(Point)
*pp = Point{1, 2}
&Point{1, 2} 一步完成(返回了指针),所以可以作为函数实参。
结构体是否可以用 == 或 != 比较,是由其所有成员决定的,即所有成员可以比较,结构体才可以比较。当结构体可以用 == 比较时,它可以作为map的键。
golang支持嵌套匿名结构体,并为访问匿名结构体的成员提供快捷语法:
package main
import (
"fmt"
)
func main() {
var w Wheel
w.X = 8 // 等价于 w.Circle.Point.X = 8
w.Y = 8 // 等价于 w.Circle.Point.Y = 8
w.Radius = 5 // 等价于 w.Circle.Radius = 5
w.Spokes = 20
fmt.Println(w) // {{{8 8} 5} 20}
w1 := Wheel{Circle{Point{8, 8}, 5}, 20}
w2 := Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5, // 这个逗号是必需的!
},
Spokes: 20, // 这个逗号是必需的!
}
fmt.Printf("%#v\n", w1)
fmt.Printf("%#v\n", w2)
}
type Point struct {
X, Y int
}
type Circle struct {
Point // 匿名结构体,自动嵌入成员 X, Y
Radius int
}
type Wheel struct {
Circle // 匿名结构体,自动嵌入 X, Y, Radius
Spokes int
}
代码中,结构体用字面量初始化时,仍然只能用标准方式(注意代码中含嵌套的结构体初始化时最后一个成员尾部的逗号,可以理解为”有儿子的结构体初始化最后一个成员后加逗号“)。上述代码输出
{{{8 8} 5} 20}
main.Wheel{Circle:main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}, Spokes:20}
main.Wheel{Circle:main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}, Spokes:20}
对于嵌套结构体的成员的访问控制,例如 point 不可导出,但有可导出的成员 X 和 Y,当将它通过 circle(不可导出)嵌入 Wheel 时, w.X 在引入Wheel的包仍然可用,但 w.circle.point.X 就不行。简单理解,嵌入的成员它的访问控制并不由原来的结构体说了算,好像它已经成了包含嵌体的结构体的成员。
嵌入的匿名成员,并不限于结构体类型,任何命名的类型或指向命名类型的指针都可以。用快捷方式访问匿名成员的方式,不仅适用于匿名成员内部变量,也适用于它的方法。这种机制,帮助 golang 实现了 聚合方式的 OOP。
----