go语言学习(第五章,数据)(go 语言学习笔记)

5.1 字符串

字符串是不可变字节(byte)序列,其本身是一个复合结构

type stringStruct struct{
	str unsafe.Pointer
	len int
}

头部指针指向字节数组,但没有NULL结尾。默认以UTF-8编码存储Unicode字符,字面量里允许使用十六进制、八进制和UTF编码格式。

func main() {
	s := "雨痕\x61\142\u0041"
	fmt.Printf("%s\n", s)
	fmt.Printf("%x ,len:%d\n", s, len(s))
}
结果:
雨痕abA
e99ba8e79795616241 ,len:9

内置函数len返回字节数组长度,cap不接受字符串类型参数
字符串默认值不是nil,而是“”

func main() {
	var s string
	println(s == "")		// true
	//println(s == nil)	//invalid operation: s == nil (mismatched types string and nil)
}

使用“·”定义不做转义处理的原始字符串,支持跨行。

func main() {
	s := `line\r\n
	lin2`
	println(s)
}
结果:
line\r\n
        lin2

编译器不会解析原始字符串内的注释语句,且前置缩进空格也属于字符串内容。
支持 “!= 、== 、<、>、+、+=”操作符。

func main() {
	s := "ab" +
	"cd"
	println(s == "abcd")
	println(s > "abc")
	println(s <"ad")
}
结果
true
true
true

允许以索引号访问字节数组(非字符),但不能获取元素地址

func main() {
	s := "abcd"
	println(s[1])
	println(&s[1]) // cannot take the address of s[1]
}

以切片语法(起始和结束索引号)返回子串时,其内部依旧指向原字节数组。

func main() {
	s := "abcdefg"
	s1 := s[:3]
	s2 := s[1:4]
	s3 := s[2:]
	println(s1, s2, s3)
	fmt.Printf("%#v\n", (*reflect.StringHeader)(unsafe.Pointer(&s)))
	fmt.Printf("%#v\n", (*reflect.StringHeader)(unsafe.Pointer(&s1)))
}
结果
abc bcd cdefg
&reflect.StringHeader{Data:0x4c065c, Len:7}
&reflect.StringHeader{Data:0x4c065c, Len:3}

使用for遍历字符串时,分byte和rune两种方式。

func main() {
	s := "雨痕"
	for i := 0; i < len(s); i++ {
		fmt.Printf("%d:[%c]\n", i, s[i])
	}
	for i, c := range s {
		fmt.Printf("%d:[%c]\n", i, c)
	}
}
result:
0:[é]
1:[
2:[¨]
3:[ç]
4:[]
5:[]
0:[]
3:[]

转换

要修改字符串,须将其转换为可变类型([]rune或[]byte),待完成后再转换回来。但不管如何转换,都须重新分配内存,并复制数据。

func pp(format string, ptr interface{}) {
	p := reflect.ValueOf(ptr).Pointer()
	h := (*uintptr)(unsafe.Pointer(p))
	fmt.Printf(format, *h)
}

func main() {
	s := "hello word!"
	pp("s:%x\n", &s)
	bs := []byte(s)
	s2 := string(bs)
	pp("string to []byte,bs:%x\n", &bs)
	pp("[]byte to string,s2:%x\n", &s2)
	rs := []rune(s)
	s3 := string(rs)
	pp("string to []rune,rs:%x\n", &rs)
	pp("[]rune to string,s3:%x\n", &s3)

}
结果
s:4c1efc
string to []byte,bs:c00004c0a0
[]byte to string,s2:c00004c0b0
string to []rune,rs:c000072030
[]rune to string,s3:c00004c0d0

用append函数,可将string直接追加到[]byte内。

func main() {
	var bs []byte
	bs = append(bs,"abc"...)
	fmt.Println(bs)
}
结果
[97 98 99]

考虑到字符串只读特性,转换时复制数据到新分配内存是可理解的。当然,性能同样重要,编译器会为某些场合进行专门优化,避免额外分配和复制操作;

  • 将[]byte转换为string key,去map[string] 查询的时候。
  • 将string转换成[]byte,进行for range 迭代时,直接取字节赋值给局部变量。

性能

除类型转换外,动态构建字符串也容易造成性能问题。
用加法操作符拼接字符串时,每次都需要重新分配内存。如此,在构建“超大”字符串时,性能就显得极差

func test() string {
	var s string
	for i := 0; i < 100000; i++ {
		s += "a"
	}
	return s
}
func BenmarkTest(b *testing.B) {
	for i := 0; i < b.N; i++ {
		test()
	}
}

改进思路是预分配足够的内存空间。常用 的方法是用strings.Join函数,它会统计所有参数长度,并一次性完成内存分配操作

func test() string {
	s := make([]string, 10000)
	for i := 0; i < 10000; i++ {
		s[i] = "我"
	}
	return strings.Join(s, "")
}
func BenmarkTest(b *testing.B) {
	for i := 0; i < b.N; i++ {
		test()
	}
}

另外,bytes.Buffer也能完成类似操作,且性能相当

func test() string {
	var b bytes.Buffer
	b.Grow(1000)
	for i := 0; i <1000 ; i++{
		b.WriteString("我")
	}
	
	return b.String()
}

对于数量较少的字符串格式化拼接,可使用fmt.Sprintf、test/template等方法
字符串操作通常在堆上分配内存,这会对web等高并发应用造成较大影响,会有大量字符串对象要做垃圾回收。建议使用[]byte缓存池,或在栈上自行拼装等方式来实现zero-garbage

Unicode

类型rune专门用来存储Unicode码点(code point),它是int32的别名,相当于UCS-4/UTF-32编码格式。使用单引号的字面量,其默认类型就是rune。

func main() {
	r := '我'
	fmt.Printf("%T", r)	//int32
}

除[]rune外,还可直接在rune、byte、string间进行转换

func main() {
	r := '我'
	s := string(r)	// 我
	b := byte(r)	// 17
	s2 := string(b)	// 
	r2 := rune(b)	 // 17
	fmt.Println(s, b, s2, r2)
}

要知道字符串存储的字节数组,不一定就是合法的UTF-8文本。

func main() {
	s := "十五"
	s = string(s[0:1] + s[3:4])
	fmt.Println(s, utf8.ValidString(s))		//�� false
}

校准库unicode里提供了丰富的操作函数。除验证函数外,还可用RuneCountInString代替len返回Unicode字符数量

func main() {
	s := "十.五"
	println(len(s), utf8.RuneCountInString(s))	// 7 3
}

5.2 数组

定义数组类型时,数组长度必须是非负整型常量表达式,长度是类型组成部分。也就是,元素类型相同,但长度不同的数组不属于同一类型。

func main() {
	var d1 [3]int
	var d2 [2]int
	d1 = d2	//cannot use d2 (type [2]int) as type [3]int in assignment
}

灵活的初始化方式

func main() {
	var a [4]int                  // 元素自动初始化为零
	b := [4]int{2, 5}             // 未提供初始值的元素自动初始化为0
	c := [10]int{5, 3: 10, 5: 20} // 可指定索引位置
	d := [...]int{1, 2, 3}        // 编译器按初始化值数量确定数组长度
	e := [...]int{10, 5: 32}      // 支持索引初始化,但注意数组长度与此有关
	fmt.Println(a, b, c, d, e)
}
结果
[0 0 0 0] [2 5 0 0] [5 0 0 10 0 20 0 0 0 0] [1 2 3] [10 0 0 0 0 32]

对于复合类型,可省略元素初始化类型标签。

func main() {
	type user struct {
		name string
		age  byte
	}
	d := [...]user{
		{"Tom", 20},	//user{"Tom", 20},
		{"Mary", 18},
	}
	fmt.Printf("%#v\n", d)
}

在定义多维数组时,仅第一维度允许使用“…”.

func main() {
	a := [2][2]int{
		{1, 2},
		{2, 3},
	}
	b := [...][2]int{
		{1, 2},
		{2, 3},
	}
	c := [...][2][2]int{
		{
			{1, 2},
			{1, 2},
		},
		{
			{2, 3},
			{2, 3},
		},
	}
	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)
}
结果
[[1 2] [2 3]]
[[1 2] [2 3]]
[[[1 2] [1 2]] [[2 3] [2 3]]]

内置函数len和cap都返回第一维度长度

5.3 切片

切片(slice)本身并非动态数组或数组指针。它内部通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内。

type slice struct{
	array unsafe.Pointer
	len int
	cap int
}

切片本身是个只读对象,其工作机制类似数组指针的一种包装
可基于数组或数组指针创建切片,以开始和结束索引位置确定所引用的数组片段。不支持反向索引,实际范围是一个右半开区间

func main() {
	x := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	println(len(x[:]), cap(x[:]))	//10 10
	println(len(x[2:5]), cap(x[2:5]))	//3 8
	println(len(x[2:5:7]), cap(x[2:5:7]))	// 3 5
	println(len(x[4:]), cap(x[4:]))	//6 6
	println(len(x[:4]), cap(x[:4]))	//4 10
	println(len(x[:4:6]), cap(x[:4:6]))	//4 6
}

属性cap表示切片所引用数组片段的真是长度,len用于限定可读写的元素数量。另外,数组必须addressable,否则会引发错误

func main() {
	m := map[string][2]int{
		"a": {1, 2},
	}
	s := m["a"][:]	// invalid operation m["a"][:] (slice of unaddressable value)
}

和数组一样,切片同样使用索引号访问元素内容。起始索引为0,而非对应的底层数组真实索引位置。

func main() {
	x := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	s := x[2:5]
	for i := 0; i < len(s); i++ {
		println(s[i])
	}
	for i, v := range s {
		println(i, ":", v)
	}
}
结果
3
4
5
0 : 3
1 : 4
2 : 5

可直接创建切片对象,无须预先准备数组。因为是引用类型,须使用make函数或显示初始化语句,它会自动完成底层数组内存分配

func main() {
	s := make([]int, 3, 5)
	s2 := make([]int, 3)
	s3 := []int{1, 2, 3}
	fmt.Println(s, len(s), cap(s))
	fmt.Println(s2, len(s2), cap(s2))
	fmt.Println(s3, len(s3), cap(s3))
}
结果
[0 0 0] 3 5
[0 0 0] 3 3
[1 2 3] 3 3

注意下面两种定义方式的区别。前者仅定义了一个[]int 类型变量,并未执行初始化操作,而后者则用初始化表达式完成了全部创建过程。

func main() {
	var a []int
	b := []int{}
	println(a == nil, b == nil)	//true false
}

输出更详细的信息,我们可以看到两者的差异

func main() {
	var a []int
	b := []int{}
	fmt.Printf("a:%#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&a)))
	fmt.Printf("b:%#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)))
	fmt.Printf("a size :%d\n", unsafe.Sizeof(a))
}
result
a:&reflect.SliceHeader{Data:0x0, Len:0, Cap:0}
b:&reflect.SliceHeader{Data:0x5771c8, Len:0, Cap:0}
a size :24

变量b的内部指针被赋值,尽管它指向runtime.zerobase,但它依旧完成了初始化操作。
另外,a == nil,仅表示它是个未初始化的切片对象,切片本身依然会分配所需内存。可以直接对nil切片执行slice[:]操作,同样返回nil
不支持比较操作,就算元素类型支持也不行,仅能判断是否为nil。

func main() {
	a := make([]int, 1)
	b := make([]int, 1)
	println(a == b)	//invalid operation: a == b (slice can only be compared to nil)
}

可获取元素地址,但不能向数组那样直接用指针访问元素内容。

func main() {
	s := []int{0, 1, 2, 3, 4}
	p := &s
	p0 := &s[0]
	p1 := &s[1]

	println(p, p0, p1)
	(*p)[0] += 100
	//p[2] += 20	//p[2] (type *[]int does not support indexing)
	*p1 += 100
	fmt.Println(s)
}
结果
0xc000071f58 0xc000072030 0xc000072038
[100 101 2 3 4]

如果元素类型也是切片,那么可以实现类似交错数组的功能

func main() {
	s := [][]int{
		{1, 2},
		{10, 11, 12, 13},
		{100, 101, 102},
	}
	fmt.Println(s[1])
	s[2] = append(s[2], 200, 300)
	fmt.Println(s[2])
}
结果
[10 11 12 13]
[100 101 102 200 300]

很显然,切片只是很小的结构体对象,用来代替数组传参可避免复制开销。还有,make函数允许在运行期间动态指定数组长度,绕开了数组类型必须使用编译期常量的限制。
并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存。而且小数组在栈上拷贝的消耗也未必就比make代价大。

reslice

将切片视作[cap]slice 数据源,据此创建新切片对象。不能超出cap,但不受len限制。

func main() {
	s := [10]int{0, 1, 2, 3, 4, 5, 6}
	i1 := s[3:7]
	i2 := i1[1:3]
	fmt.Println(i1,i2)
	for i := range i2{
		i2[i] += 100
	}
	fmt.Println(s)
	fmt.Println(i1)
	fmt.Println(i2)
}
result
[3 4 5 6] [4 5]
[0 1 2 3 104 105 6 0 0 0]
[3 104 105 6]
[104 105]

利用reslice操作,很容易实现一个栈式数据结构。

func main() {
	// 栈最大容量为5
	stack := make([]int, 0, 5)
	// 入栈
	push := func(x int) error {
		n := len(stack)
		if n == cap(stack) {
			return errors.New("stack is full")
		}
		stack = stack[:n+1]
		stack[n] = x
		return nil
	}
	// 出栈
	pop := func() (int, error) {
		n := len(stack)
		if n == 0 {
			return 0, errors.New("stack is empty")
		}
		x := stack[n-1]
		stack = stack[:n-1]
		return x, nil
	}
	// 入栈测试
	for i := 0; i < 7; i++ {
		fmt.Printf("push %d :%v ,%v\n", i, push(i), stack)
	}
	// 出栈测试
	for i := 0 ; i<10 ; i++{
		x,err := pop()
		fmt.Printf("pop :%d ,%v,%v\n",x,err,stack)
	}
}
结果
push 0 :<nil> ,[0]
push 1 :<nil> ,[0 1]
push 2 :<nil> ,[0 1 2]
push 3 :<nil> ,[0 1 2 3]
push 4 :<nil> ,[0 1 2 3 4]
push 5 :stack is full ,[0 1 2 3 4]
push 6 :stack is full ,[0 1 2 3 4]
pop :4 ,<nil>,[0 1 2 3]
pop :3 ,<nil>,[0 1 2]
pop :2 ,<nil>,[0 1]
pop :1 ,<nil>,[0]
pop :0 ,<nil>,[]
pop :0 ,stack is empty,[]
pop :0 ,stack is empty,[]
pop :0 ,stack is empty,[]
pop :0 ,stack is empty,[]
pop :0 ,stack is empty,[]

append

向切片尾部添(slice[len])添加数据,返回新的切片对象。

func main() {
	s := make([]int, 0, 5)
	s1 := append(s, 10)
	s2 := append(s1, 20, 30)
	fmt.Println(s, len(s), cap(s))
	fmt.Println(s1, len(s1), cap(s1))
	fmt.Println(s2, len(s2), cap(s2))
}
结果
[] 0 5
[10] 1 5
[10 20 30] 3 5

数据未被追加到原底层数组。如超出cap限制,则为新切片对象重新分配数组

func main() {
	s := make([]int, 0, 100)
	s1 := s[:2:4]
	s2 := append(s1, 1, 2, 3, 4, 5, 6, 7, 8)
	fmt.Printf("s1:%p:%v\n", &s1[0], s1)
	fmt.Printf("s2:%p:%v\n", &s2[0], s2)

	fmt.Printf("s data: %v\n", s[:10])
	fmt.Printf("s1 cap:%d,s2 cap:%d\n", cap(s1), cap(s2))
}
结果
s1:0xc000086000:[0 0]
s2:0xc00007a0a0:[0 0 1 2 3 4 5 6 7 8]	// 数组地址不同,确认重新分配
s data: [0 0 0 0 0 0 0 0 0 0]		// append并未向底层数组添加数据
s1 cap:4,s2 cap:10

注意:

  • 是超出且切片cap限制,而非底层数组长度限制,因为cap可小于数组长度
  • 新分配数组长度是原cap的2被,而非原数组的2倍(并非总是2倍)
    向nil切片追加数据时,会为其分配底层数组内存
func main() {
	var s []int
	k := append(s, 1, 2, 3)
	fmt.Println(s, k)
}

正因为存在重新分配底层数组的缘故,在某些场合建议预留足够的空间,避免中途内存分配和数据复制开销

copy

在两个切片对象间复制数据,允许指向同一底层数据,允许目标区间重叠。最终所复制长度以较短的切片长度(len)为准

func main() {
	s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := s[5:8]
	n := copy(s[4:], s1)	// 在同一底层数组的不同区间复制
	fmt.Println(n, s)
	s2 := make([]int, 6)	// 在不同数组间复制
	n = copy(s2, s)
	fmt.Println(n, s2)
}
结果
3 [0 1 2 3 5 6 7 7 8 9]
6 [0 1 2 3 5 6]

还可直接从字符串中复制数据到[]byte。

func main() {
	b := make([]byte, 3)
	n := copy(b, "abcde")
	fmt.Println(n, b)	//3 [97 98 99]
}

如果切片长时间引用大数组中很小的片段,那么建议新建独立切片,复制出所需数据,以便原数组内存可被及时回收。

5.4 字典

字典(哈希表)是一种使用频率极高的数据结构。将其作为语言内置类型,从运行时层面进行优化,可获得更高的性能。
作为无序键值对集合,字典要求key必须是支持相等运算符(==,!=)的数据类型,比如,数字、字符串、指针、数组、结构体,以及对应接口类型。
字典是引用类型,使用make函数或初始化表达语句来创建。

func main() {
	m := make(map[string]int)
	m["a"] = 1
	m["b"] = 2

	m2 := map[int]struct{ x int }{
		1: {19},
		2: {x:18},	// 可省略key、value类型标签
		3: {17},
	}
	m2[4] = struct{x int}{x:16}
	fmt.Println(m, m2)
}
结果
map[a:1 b:2] map[1:{19} 2:{18} 3:{17} 4:{16}]

基本操作演示:

func main() {
	m := map[string]int{
		"a": 1,
		"b": 2,
	}
	m["a"] = 10
	m["c"] = 30
	if v, ok := m["d"]; ok {
		println(v)
	}
	delete(m, "d")
	fmt.Println(m)
}
结果
map[a:10 b:2 c:30]

访问不存在的键值,默认返回零值,不会引发错误。但推荐使用ok-idiom模式,毕竟通过零值无法判断键值是否存在,或许存储的value本身就是零值。
对字典进行迭代,每次返回的键值次序都不相同

func main() {
	m := make(map[string]int)
	for i := 0; i < 8; i++ {
		m[string('a'+i)] = i
	}
	for i := 0; i < 4; i++ {
		for k, v := range m {
			print(k, ":", v, "    ")
		}
		println()
	}
}
结果
f:5    g:6    h:7    a:0    b:1    c:2    d:3    e:4
a:0    b:1    c:2    d:3    e:4    f:5    g:6    h:7
f:5    g:6    h:7    a:0    b:1    c:2    d:3    e:4
e:4    f:5    g:6    h:7    a:0    b:1    c:2    d:3

函数len返回当前键值对数量,cap不接受字典类型。另外,因访问安全和哈希算法等缘故,字典被设计成“not addressable”,故不能直接修改value成员(结构或数组)。

func main() {
	type user struct {
		name string
		age  byte
	}
	m := map[int]user{
		1: {"tom", 22},
		2: {"lin", 25},
	}
	m[1].age += 18	//cannot assign to struct field m[1].age in map

}

正确做法是返回整个value,待修改后再设置字典键值,或直接使用指针类型

func main() {
	type user struct {
		name string
		age  byte
	}
	m := map[int]*user{
		1: &user{"tom", 22},
		2: &user{"lin", 25},
	}
	m[1].age += 18
	fmt.Println(m[1])
}

不能对nil字典进行写操作,但却能读

func main() {
	var m map[string]int
	println(m["a"])	// 0
	m["a"] = 1		//panic: assignment to entry in nil map
}

注意:内容为空的字典,与nil是不同的

func main() {
	var m1 map[string]int
	m2 := map[string]int{}
	println(m1 == nil, m2 == nil)	//true  false
}

安全

在迭代期间删除或新增键值是安全的

func main() {
	m := make(map[int]int)
	for i := 0; i < 10; i++ {
		m[i] = i + 10
	}
	for k := range m {
		if k == 5 {
			m[100] = 1000
		}
		delete(m, k)
		fmt.Println(k, m)
	}
}
result:
8 map[2:12 3:13 6:16 7:17 9:19 0:10 1:11 4:14 5:15]
9 map[0:10 1:11 4:14 5:15 2:12 3:13 6:16 7:17]
2 map[0:10 1:11 4:14 5:15 3:13 6:16 7:17]
3 map[6:16 7:17 0:10 1:11 4:14 5:15]
6 map[5:15 0:10 1:11 4:14 7:17]
7 map[0:10 1:11 4:14 5:15]
0 map[5:15 1:11 4:14]
1 map[4:14 5:15]
4 map[5:15]
5 map[100:1000]

对于此例而言,不能保证迭代操作会删除新增的键值
运行时会对字典并发操作做出检测。如果某个任务正在对字典进行写操作,那么其他任务就不能对该字典执行并发操作(读、写、删除),否则会导致进程崩溃

func main() {
	m := make(map[string]int)
	go func() {
		for {
			m["a"] += 1
			time.Sleep(time.Microsecond)
		}
	}()
	go func() {
		for {
			_ = m["b"]
			time.Sleep(time.Microsecond)
		}
	}()
	select {}	// 阻止退出
}
结果
fatal error: concurrent map read and map write

可用sync.RWMutex实现同步,避免读写操作同时进行

func main() {
	var lock sync.RWMutex // 使用读写锁,以获得最佳性能
	m := make(map[string]int)
	go func() {
		for {
			lock.Lock()
			m["a"] += 1
			lock.Unlock()
			time.Sleep(time.Microsecond)
		}

	}()

	go func() {
		for {
			lock.RLock()
			_ = m["b"]
			lock.RUnlock()
			time.Sleep(time.Microsecond)
		}
	}()
	select {}
}

性能

字典对象本身就是指针包装,传参时无须再次取地址。

func test(x map[string]int) {
	fmt.Printf("x:%p\n", x)
}
func main() {
	m := make(map[string]int)
	test(m)
	fmt.Printf("m:%p,%d\n", m, unsafe.Sizeof(m))
	m2 := map[string]int{}
	test(m2)
	fmt.Printf("m2:%p,%d\n", m2, unsafe.Sizeof(m2))

}
结果
x:0xc000060240
m:0xc000060240,8
x:0xc000060270
m2:0xc000060270,8

在创建时预先准备足够空间有助于提升性能,减少扩张时的内存分配和重新哈希操作。

func test() map[int]int {
	m := make(map[int]int)
	for i := 0; i < 1000; i++ {
		m[i] = i
	}
	return m
}
func testCap() map[int]int {
	m := make(map[int]int, 1000)
	fmt.Println(m)
	for i := 0; i < 1000; i++ {
		m[i] = i
	}
	return m
}

对于海量小对象,应直接用字典存储键值数据拷贝,而非指针。这有助于减少需要扫描的海量对象,大幅度缩短垃圾回收时间。另外字典不会收缩内存,所以,适当替换成新对象是必要的。

5.5 结构

结构体(struct)将多个不同类型命名字段(field)序列打包成一个复合类型
字段名必须唯一,可用“_”补位,支持使用自身指针类型成员。字段名、排序顺序属于类型组成部分。除对齐处理外,编译器不会优化、调整内存布局。

func main() {
	type node struct {
		_      int
		id     int
		before *node
	}
	n1 := node{id: 1}
	n2 := node{id: 2, before: &n1}
	fmt.Println(n1, n2)
}
结果
{0 1 <nil>} {0 2 0xc000046400}

可按顺序初始化全部字段,或使用命名方式初始化指定字段

func main() {
	type user struct {
		name string
		age  byte
	}
	u1 := user{"tom", 12}
	//u2 := user{"tom"}	//too few values in user literal
}

推荐用命名初始化。这样在扩充结构字段或调整字段顺序时,不会导致初始化语句出错。
可直接定义匿名结构类型变量,或用作字段类型。但因缺少类型标识,在作为字段类型时无法直接初始化,稍显麻烦

func main() {
	u := struct {
		name string
		age  int
	}{
		name: "tom",
		age:  12,
	}
	type file struct {
		name string
		attr struct {
			owner int
			perm  int
		}
	}
	f := file{
		name: "test.dat",
		// attr:{		// missing type in composite literal
		// 	owner:1,
		// 	perm:1,
		// },
	}
	f.attr.owner = 1	//正确方式
	f.attr.perm = 1
	fmt.Println(u, f)
}
结果
{tom 12} {test.dat {1 1}}

也可以在初始化语句中再次定义结构体,但那样看上去会非常丑陋
只有在所有字段类型全部都支持时,才能做相等操作。

func main() {
	type data struct {
		x int
	}
	d1 := data{
		x: 100,
	}
	d2 := data{
		x: 100,
	}
	println(d1 == d2)	// true
	type file struct {
		x int
		y map[string]int
	}
	f1 := file{
		x: 10,
	}
	f2 := file{
		x: 10,
	}
	println(f1 == f2)	//f1 == f2 (struct containing map[string]int cannot be compared)
}

可以使用指针直接操作结构字段,但不能是多级指针

func main() {
	type user struct {
		name string
		age  int
	}
	p := &user{
		name: "tom",
		age:  18,
	}
	p.name = "mary"
	p.age++
	p2 := &p
	//*p2.name = "jack" //p2.name undefined (type **user has no field or method name)

}

空结构

空结构(struct{})是指没有字段的结构类型。它比较特殊,因为无论是其自身,还是作为数组元素类型,其长度都为零。

func main() {
	var a struct{}
	var b [100]struct{}

	println(unsafe.Sizeof(a), unsafe.Sizeof(b))	// 0 0
}

尽管没有分配数组内存,但依然可以操作元素,对应切片len、cap属性正常。

func main() {
	var d [100]struct{}
	s := d[:]
	d[1] = struct{}{}
	d[2] = struct{}{}
	fmt.Println(s[3], len(s), cap(s))
}
结果
{} 100 100

实际上,这类“长度”为零的对象通常都指向runtime.zerobase 变量。

func main() {
	a := [10]struct{}{}
	b := a[:]
	c := [0]int{}
	fmt.Printf("%p,%p,%p\n", &a[0], &b[0], &c)//0x5771c8,0x5771c8,0x5771c8
}

空结构可作为通道元素类型,用于事件通知。

func main() {
	exit := make(chan struct{})
	go func() {
		println("hello")
		exit <- struct{}{}
	}()
	<-exit
	println("end.")
}

匿名字段

所谓匿名字段,是指没有名字,仅有类型的字段,也被称作嵌入字段或嵌入类型。

func main() {
	type attr struct {
		perm int
	}
	type file struct {
		name string
		attr
	}
}

从编译器角度,这只是隐式地以类型名作为字段名字。可直接引用匿名字段的成员,但初始化时须当作独立字段。

func main() {
	type attr struct {
		perm int
	}
	type file struct {
		name string
		attr
	}
	f := file{
		name: "test.dat",
		attr: attr{		// 显示初始化匿名字段
			perm: 0775,
		},
	}
	f.perm = 0644	// 直接设置匿名字段成员
	println(f.perm)	// 直接读取匿名字段成员
}
结果
420

如嵌入其他包中的类型,则隐式字段不包括报名。

func main() {
	type data struct {
		os.File
	}
	d := data{
		File: os.File{},
	}
	fmt.Printf("%#v\n", d)
}
结果
main.data{File:os.File{file:(*os.file)(nil)}}

不仅仅是结构体,除接口指针和多级指针以外的任何命名类型都可作为匿名字段。

func main() {
	type data struct {
		*int
		string
	}
	x := 100
	d := data{
		int:    &x,
		string: "abc",
	}
	fmt.Printf("%#v\n", d)
}
结果
main.data{int:(*int)(0xc00004c058), string:"abc"}

不能将基础类型和其指针类型同时嵌入,因为两者隐式名字相同

func main() {
	type d struct {
		int
		*int	//duplicate field int
	}
}

虽然可以像普通字段那样访问匿名字段成员,但会存在重名问题。默认情况下,编译器从当前显示命名字段开始,逐步向内查找匿名字段成员。如匿名字段成员被外层同名字段遮蔽,那么必须用显示字段名。
严格来说,Go并不是传统意义上的面对对象编程语言,或者说仅实现了最小面对对象机制。匿名嵌入不是继承,无法实现多态处理。虽然配合方法集,可用接口来实现一些类似操作,但其本质是完全不同的。

字段标签

字段标签(tag)并不是注释,而是用来对字段进行描述的元数据。尽管它不属于数据成员,但确是类型的组成部分。
在运行期,可用反射获取标签信息。它常被用作格式校验,数据库关系映射等。

func main() {
	type user struct {
		name string `昵称`
		sex  byte   "性别"
	}
	u := user{"Tom", 1}
	v := reflect.ValueOf(u)
	t := v.Type()
	
	for i,n :=0,t.NumField();i<n;i++{
		fmt.Printf("%s:%v\n", t.Field(i).Tag,v.Field(i))
	}
}
结果
昵称:Tom
性别:1

内存布局

不管结构体包含多少字段,其内存总是一次性分配的,各字段在相邻的地址空间按定义的顺序排列。当然,对于引用类型、字符串和指针,结构内存只包含其基本(头部)数据。还有,所有匿名成员字段成员也包含在内

你可能感兴趣的:(go)