GO的第六天,复合数据类型---Map、结构体

Map

一个map就是一个哈希表的引用。map的语法:

ages := make(map[string]int)

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

// 修改
ages["alice"] = 43
fmt.Println(ages["alice"])   // 43

// 删除
delete(ages, "alice")

所有这些操作是安全的,即使这些元素不在map中也没有关系;如果一个查找失败将返回value类型对应的零值,例如,即使map中不存在“bob”下面的代码也可以正常工作,因为ages["bob"]失败时将返回0。

ages["bob"] = ages["bob"] + 1

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:

_ = &ages["bob"] // compile error: cannot take address of map element

禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

map是可以遍历的:

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

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式:

import "sort"

// 因为我们一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。
names := make([]string, 0, len(ages))
for name := range ages {
    names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
    fmt.Printf("%s\t%d\n", name, ages[name])
}

map类型的零值是nil,也就是没有引用任何哈希表。

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

map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常,在向map存数据前必须先创建map:

ages["carol"] = 21 // panic: assignment to entry in nil map

map如果key不存在,value对应类型的零值,那我们要怎么区分一个已经存在的0,和不存在而返回零值的0呢?

age, ok := ages["bob"]
if !ok {/* ... */}

你会经常看到将这两个结合起来使用,像这样:

if age, ok := ages["bob"]; !ok { /* ... */ }

在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。

要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:

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
}

从例子中可以看到如何用!ok来区分元素不存在,与元素存在但为0的。我们不能简单地用xv != y[k]判断,那样会导致在判断下面两个map时产生错误的结果:

equal(map[string]int{"A": 0}, map[string]int{"B": 42})  // true

map类型的应用还有很多,我们在这就不一一例举了,以上只是对map类型的简单讲解。下面我们来讲结构体。

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。

用结构体的经典案例处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。

type Employee struct {
    ID        int
    Name      string
    Address   string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

var dilbert Employee

dilbert.Salary += 5000

// 对成员取地址,然后通过指针访问
position := &dilbert.Position
*position = "Senior " + *position

通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行,就像下面的Name和Address成员那样:

type Employee struct {
    ID            int
    Name, Address string
    DoB           time.Time
    Position      string
    Salary        int
    ManagerID     int
}

如果结构体成员名字是以大写字母开头的,那么该成员就是 导出 的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。

ps:一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适应于数组。)但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等

结构体字面值

下面的写法,要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。它要求写代码和读代码的人要记住结构体的每个成员的类型和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。

type Point struct{ X, Y int }

p := Point{1, 2}

这种写法更常用,以成员的名字和相应的值来初始化。

anim := gif.GIF{LoopCount: 64}

在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为,提供了成员的名字,所有成员出现的顺序并不重要。

两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员

package p
type T struct{ a, b int } // a and b are not exported

package q
import "p"
var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
var _ = p.T{1, 2}       // compile error: can't reference a, b

结构体可以作为函数的参数和返回值。例如,这个Scale函数将Point类型的值缩放后返回:

func Scale(p Point, factor int) Point {
    return Point{p.X * factor, p.Y * factor}
}

fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"

如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,

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

如果要在函数内部修改结构体成员的话,用指针传入是必须的;这个我们之前在slice中也有提过。所以我们只用记住,想要在函数内部改变外部数据的话,就传指针。这个和PHP大同小异。

因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:

pp := &Point{1, 2}

// 等价于
pp := new(Point)
*pp = Point{1, 2}

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的:

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类型。

type address struct {
    hostname string
    port     int
}

hits := make(map[address]int)
hits[address{"golang.org", 443}]++

结构体嵌入和匿名成员

type Circle struct {
    X, Y, Radius int
}

type Wheel struct {
    X, Y, Radius, Spokes int
}

我们可以看到,这两个结构体的相似和重复之处。那我们可以怎样进行优化呢?

type Point struct {
    X, Y int
}

type Circle struct {
    Center Point
    Radius int
}

type Wheel struct {
    Circle Circle
    Spokes int
}

这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

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            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:

w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20,   // 此处(和半径处)需要尾随逗号
}

因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所有匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员。但是在包外部,因为circle和point没有导出不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。

今天的内容有点多,还是有很多东西有点模糊的,需要多动手应用,才能慢慢深入了解。加油!!!

参考

《GO语言圣经》

你可能感兴趣的:(golang,map,结构体)