golang学习随便记4

复合数据类型(构造类型)

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。

----

上一篇:Unity中射线的几种


下一篇:类封装案例2