字符串是不可变字节(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]
考虑到字符串只读特性,转换时复制数据到新分配内存是可理解的。当然,性能同样重要,编译器会为某些场合进行专门优化,避免额外分配和复制操作;
除类型转换外,动态构建字符串也容易造成性能问题。
用加法操作符拼接字符串时,每次都需要重新分配内存。如此,在构建“超大”字符串时,性能就显得极差
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
类型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
}
定义数组类型时,数组长度必须是非负整型常量表达式,长度是类型组成部分。也就是,元素类型相同,但长度不同的数组不属于同一类型。
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都返回第一维度长度
切片(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代价大。
将切片视作[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,[]
向切片尾部添(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
注意:
func main() {
var s []int
k := append(s, 1, 2, 3)
fmt.Println(s, k)
}
正因为存在重新分配底层数组的缘故,在某些场合建议预留足够的空间,避免中途内存分配和数据复制开销
在两个切片对象间复制数据,允许指向同一底层数据,允许目标区间重叠。最终所复制长度以较短的切片长度(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]
}
如果切片长时间引用大数组中很小的片段,那么建议新建独立切片,复制出所需数据,以便原数组内存可被及时回收。
字典(哈希表)是一种使用频率极高的数据结构。将其作为语言内置类型,从运行时层面进行优化,可获得更高的性能。
作为无序键值对集合,字典要求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
}
对于海量小对象,应直接用字典存储键值数据拷贝,而非指针。这有助于减少需要扫描的海量对象,大幅度缩短垃圾回收时间。另外字典不会收缩内存,所以,适当替换成新对象是必要的。
结构体(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
不管结构体包含多少字段,其内存总是一次性分配的,各字段在相邻的地址空间按定义的顺序排列。当然,对于引用类型、字符串和指针,结构内存只包含其基本(头部)数据。还有,所有匿名成员字段成员也包含在内