1. 字符串
2. 数组
3. 切片
4. 字典
5. 结构
字符串
Go语言中的字符串是由一组不可变的字节(byte)序列组成,从源码文件中看出其本身是一个复合结构:
string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
字符串中的每个字节都是以UTF-8编码存储的Unicode字符,字符串的头部指针指向字节数组的开始,但是没有NULL或’\0’结尾标志。 表示方式很简单,用双引号(“”)或者反引号(``),它们的区别是:
- 双引号之间的转义符会被转义,而反引号之间的转义符保持不变
- 反引号支持跨行编写,而双引号则不可以
{
println("hello\tgo") //输出hello go
println(`hello\tgo`) //输出hello\tgo
}
{
println( "hello
go" ) //syntax error: unexpected semicolon or newline, expecting comma or )
println(`hello
go`)
}
输出:
hello
go
在前面类型的章节中描述过字符串的默认值是”“,而不是nil,比如:
var s string
println( s == "" ) //true
println( s == nil ) //invalid operation: s == nil (mismatched types string and nil)
Go字符串支持 “+ , += , == , != , < , >” 六种运算符
Go字符串允许用索引号访问字节数组(非字符),但不能获取元素的地址:比如
{
var a = "hello"
println(a[0]) //输出 104
println(&a[1]) //cannot take the address of a[1]
}
Go字符串允许用切片的语法返回子串(起始和结束索引号)
var a = "0123456"
println(a[:3]) //0,1,2
println(a[1:3]) //1,2
println(a[3:]) //3,4,5,6
日常开发中,经常会有遍历字符串的场景,比如:
{
var a = "Go语言"
for i:=0;i < len(a);i++{ //以byte方式按字节遍历
fmt.Printf("%d: [%c]\n", i, a[i])
}
for i, v := range a{ //以rune方式遍历
fmt.Printf("%d: [%c]\n", i, v)
}
}
输出:
0: [G]
1: [o]
2: [è]
3: [¯]
4: []
5: [è]
6: [¨]
7: []
0: [G]
1: [o]
2: [语]
5: [言]
在Go语言中,字符串的底层使用byte数组存的,并且是不可以改变的,所以byte方式每次得到的只有一个byte,而中文字符是占3个byte的。 rune采用计算字符串长度的方式与byte方式不同,比如:
println(utf8.RuneCountInString(a)) // 结果为 4
println(len(a)) // 结果为 8
所以如果想要获得期待的那种结果的话,需要先将字符串a转换为rune切片,再使用内置的len函数,比如:
{
r := []rune(a)
for i:= 0;i < len(r);i++{
fmt.Printf("%d: [%c]\n", i, r[i])
}
}
所以,在遍历或处理的字符串的情况下,如果其中存在中文,尽量使用rune方式处理。
- 转换 前面讲过不能修改原字符串,如果修改的话需要将字符串转换成[]byte或[]rune , 然后在转换回来,比如:
{
var a = "hello go"
a[1] = 'd' //cannot assign to a[1]
}
{
var a = "hello go"
bs := []byte(a)
...
s2 := string(bs)
rs := []rune(a)
...
s3 := string(rs)
}
Go语言支持用”+“运算符进行字符串拼接,但是每次拼接都需要重新分配内存,如果频繁构造一个很长的字符串, 则性能影响就会很大,比如:
func test1()string{
var s string
for i:= 0;i < 1000 ;i++{
s += "a"
}
return s
}
func Benchmark_test1(b *testing.B){
for i:= 0;i < b.N; i++{
test1()
}
}
输出:
# go test str1_b_test.go -bench="test1" -benchmem
Benchmark_test1-2 5000 227539 ns/op 530338 B/op 999 allocs/op
常用的改进方法是预分配足够的内存空间,然后使用strings.Join函数, 该函数会统计出所有参数的长度,并一次性完成内存分配操作,改进一下上面的代码:
func test()string{
s := make([]string,1000)
for i:= 0;i < 1000 ;i++{
s[i] = "a"
}
return strings.Join(s,"")
}
func Benchmark_test(b *testing.B){
for i:= 0;i < b.N; i++{
test()
}
}
输出:
# go test -v b_test.go -bench="test1" -benchmem
Benchmark_test1-2 200000 10765 ns/op 2048 B/op 2 allocs/op
在日常开发中,可以使用fmt.Sprintf函数来格式化和拼接较少的字符串操作,比如:
{
a := 10010
as := fmt.Sprintf("%d",a)
fmt.Printf("%T , %v\n",as,as)
}
数组
数组是内置(build-in)类型,是一组存放相同类型数据的集合,数组的数据类型是由存储的元素类型和数组的长度共同决定的, 初始化之后长度是固定无法修改的,数组也支持逻辑判断运算符 ”==“, ”!=“,定义方式如下:
{
var a [10]int
var b [20]int
println(a == b) //invalid operation: a == b (mismatched types [10]int and [20]int)
}
即使元素类型相同,但是长度不同数组,也不属于同一类型。
数组的初始化相对灵活,下标索引值从0开始,支持按索引位置初始化,对于未初始化的数组,编译器将给以默认值。
{
var a[4] int //元素初始化为0
b := [4] int{0,1} //未初始化的元素将被初始化为0
c := [4] int{0, 2: 3} //可指定索引位置初始化
d := [...]int{0,1,2} //编译器根据初始化值数量来确定数组的长度
e := [...]int{1, 3:3} //支持索引位置初始化,但数组长度与其无关
type user struct{
name string
age int
}
d := [...] user{ //复合数据类型数组可省略元素初始化类型标签
{"a",1},
{"b",2},
}
}
定义多维数组时,只有数组的第一维度允许使用 “…”
x := [2]int{2,2}
a := [2][2]int{{1,2},{2,2}}
b := [...][2]int{{2,3},{2,2},{3,3}}
c := [...][2][2]int{{ {2,3},{2,2} },{{3,3},{4,4}} }
计算数组长度时,无论使用内置的len还是cap,返回的都是第一维度的长度,比如:
fmt.Println(x, len(x), cap(x))
fmt.Println(a, len(a), cap(x))
fmt.Println(b, len(b), cap(x))
fmt.Println(c, len(c), cap(x))
输出:
[2 2] 2 2
[[1 2] [2 2]] 2 2
[[2 3] [2 2] [3 3]] 3 2
[[[2 3] [2 2]] [[3 3] [4 4]]] 2 2
- 数组指针&指针数组
数组除了可以存放具体类型的数据,也可以存放指针,比如:
{
x, y := 10, 20
a := [...]*int{&x, &y} //指针数组
p := &a //数组的指针
}
- 数组复制
Go语言数组是值(非引用)类型,所以在赋值和参数传递过程中都会复制整个数组数据,比如:
func test(x [2]int){
fmt.Printf("x:= %p,%v\n", &x, x)
}
func main(){
a := [2] int{1, 2}
test(a) //传参过程中完全复制
var b [2]int
b = a //赋值过程中完全复制
fmt.Printf("a:= %p,%v\n", &a, a)
fmt.Printf("b:= %p,%v\n", &b, b)
}
输出:
x:= 0xc42000a330,[1 2]
a:= 0xc42000a320,[1 2]
b:= 0xc42000a370,[1 2]
切片(slice)
在日常开发中,更多的场景是是需要一个可以动态更新长度的数据存储结构,切片本身并非是动态数组或数组指针, 他内部通过指针引用底层数组,并设定相关属性将数据读写操作限定在指定区域内。
/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
- 切片初始化
切片有两种基本初始化方式: > 切片可以通过内置的make函数来初始化,初始化时len=cap,一般使用时省略cap参数,默认和len参数相同,在追加元素时,如果容量cap不足时,将按len的2倍动态扩容。 > 通过数组来初始化切片,以开始和结束索引位置来确定最终所引用的数组片段
//make([]T, len, cap) //T是切片的数据的类型,len表示切片的长度,cap表示capacity
{
s := make([]int,5) //len: 5 cap: 5
s := make([]int,5,10) //len: 5 cap: 10
s := []int{1,2,3}
}
{
arr := [...]int{0,1,2,3,4,5,6,7,8,9}
s1 := arr[:]
s2 := arr[2:5]
s3 := arr[2:5:7]
s4 := arr[4:]
s5 := arr[:4]
s6 := arr[:4:6]
fmt.Println("s1: ",s1, len(s1),cap(s1))
fmt.Println("s2: ",s2, len(s2),cap(s2))
fmt.Println("s3: ",s3, len(s3),cap(s3))
fmt.Println("s4: ",s4, len(s4),cap(s4))
fmt.Println("s5: ",s5, len(s5),cap(s5))
fmt.Println("s6: ",s6, len(s6),cap(s6))
}
输出:
s1: [0 1 2 3 4 5 6 7 8 9] 10 10
s2: [2 3 4] 3 8
s3: [2 3 4] 3 5
s4: [4 5 6 7 8 9] 6 6
s5: [0 1 2 3] 4 10
s6: [0 1 2 3] 4 6
cap 表示切片所引用数组片段的真实长度,len表示已经赋过值的最大下标(索引)值加1.
注意下面两种初始化方式的区别:
{
var a []int
b := []int{}
fmt.Println(a==nil,b==nil)
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))
fmt.Printf("b size %d\n", unsafe.Sizeof(b))
}
输出:
true false
a: &reflect.SliceHeader{Data:0x0, Len:0, Cap:0}
b: &reflect.SliceHeader{Data:0x5168b0, Len:0, Cap:0}
a size 24
b size 24
说明:
1. 变量b的内部指针被赋值,即使该指针指向了runtime.zerobase,但它依然完成了初始化操作
2. 变量a表示一个未初始化的切片对象,切片本身依然会分配所需的内存
切片之间不支持逻辑运算符,仅能判断是否为nil,比如:
{
var a []int
b := []int{}
fmt.Println(a==b) //invalid operation: a == b (slice can only be compared to nil)
}
- reslice
在原slice的基础上进行新建slice,新建的slice依旧指向原底层数组,新创建的slice不能超出原slice 的容量,但是不受其长度限制,并且如果修改新建slice的值,对所有关联的切片都有影响,比如:
{
s := []string{"a","b","c","d","e","f","g"}
s1 := s[1:3] //b,c
fmt.Println(s1, len(s1),cap(s1))
s1_1 := s1[2:5] //c,d,e
fmt.Println(s1_1, len(s1_1),cap(s1_1))
}
输出:
[b c] 2 6
[d e f] 3 4
- append
向切片尾部追加数据,返回新的切片对象; 数据被追加到原底层数组,如果超出cap限制,则为新切片对象重新分配 数组,新分配的数组cap是原数组cap的2倍,比如:
{
s := make([]int,0,5)
s = append(s , 1)
s = append(s , 2,3,4,5)
fmt.Printf("%p, %v, %d\n", s,s, cap(s))
s = append(s , 6) //重新分配内存
fmt.Printf("%p, %v, %d\n", s, s, cap(s))
}
输出:
0xc420010210, [1 2 3 4 5], 5
0xc4200140a0, [1 2 3 4 5 6], 10
如果是向nil切片追加数据,则会高频率的重新分配内存和数据复制,比如:
{
var s []int
fmt.Printf("%p, %v, %d\n", s,s, cap(s))
for i:= 0; i < 10;i++{
s = append(s, i)
fmt.Printf("%p, %v, %d\n", s, s, cap(s))
}
}
所以为了避免程序运行中的频繁的资源开销,在某些场景下建议预留出足够多的空间。
- copy
两个slice之间复制数据时,允许指向同一个底层数组,并允许目标区间重叠。最终复制的长度以较短的切片长度(len) 为准,比如:
{
s1 := []int{0, 1, 2, 3, 4, 5 ,6}
s2 := []int{7, 8 ,9}
copy(s1,s2)
fmt.Println(s1,len(s1),cap(s1))
s1 = []int{0, 1, 2, 3, 4, 5 ,6}
s2 = []int{7, 8 ,9}
copy(s2,s1)
fmt.Println(s2,len(s2),cap(s2))
}
可不可以在同一切片之间复制数据呢?
在日常开发过程中,如果slice长时间引用一个大数组中很小的片段,那么建议新建一个独立的切片,并复制出所需的 数据,以便原数组内存可以被gc及时释优化回收。
字典(map)
初始化
基本操作
遍历
排序
- 字典初始化 map的声明格式如下: map[KeyType]ValueType
字典是无序键值对集合,字典要求KeyType必须是支持相等运算符(==,!=)的数据类型,比如数字、字符串、指针、数组、结构体, 以及对应的接口类型,ValueType可以是任意类型,字典也是引用类型,使用make函数或者初始化表达式语句来创建。
{
m := make(map[string]int)
m["a"] = 1
m["b"] = 2
m2 := map[int]struct{ //匿名结构体
x int
}{
1: {x:100},
2: {x:200},
}
fmt.Println(m,m2)
}
字典基本操作
{
m := make(map[string]int)
m["route"] = 66 //添加key
i := m["route"] //读取key
j := m["root"] //the value type is int, so the zero value is 0
n := len(m) //获取长度
//n := cap(m) // ???
delete(m, "route") //删除key
}
访问不存在的键值,不会引发错误,默认返回ValueType的零值,那能否根据零值来判断key的存在呢 ?
在读取map操作时, 可以使用双返回值来正常读取map,比如:
{
j, ok := m["root"]
}
ok是个bool类型变量,如果key真实存在,则ok的值为true,反之为false,如果你只是想测试key是否存在而不 获取值的话,可以使用忽略字符”_“。
在修改map值操作时,因为受内存访问安全和哈希算法等缘故,字典被设计成”not adrressable”,因此不能直接修改 value成员(结构体或数组)。
{
m := map[int] user {
1 : {
name:"tom",
age:19},
}
m[1].age = 1 //cannot assign to struct field m[1].age in map
}
有两种改造方式: > 一是对整个value进行重新复制 > 二是声明map时valueType为指针类型
{
m := map[int] user {
1 : {
name:"tom",
age:19},
}
u := m[1]
u.age = 1
m[1] = u
}
{
m := map[int] *user {
1 : {
name:"tom",
age:19},
}
m[1].age += 1
}
不能对nil字典进行写操作,但是可以读,比如:
{
var m map[string]int
//p := m["a"] //ok
m["a"] = 1 //panic: assignment to entry in nil map
}
map遍历
{
// var m = make(map[string]int)
var m = map[string]int{}
m["route"] = 66
m["root"] = 67
for key,value := range m{
fmt.Println("Key:", key, "Value:", value)
}
}
因为map是无序的,如果想按照有序key输出的话,可以先把所有的key取出,然后对key进行排序,再遍历map,比如:
{
m := make(map[int]int)
var keys []int
for i := 0 ;i <= 5;i++{
m[i] = i
}
for k, v := range m{
fmt.Println("Key:",k,"Value:",v)
}
for k := range m{
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys{
fmt.Println("Key:",k,"Value:",m[k])
}
}
并发 字典不是并发安全的数据结构,如果某个任务正在对字典进行写操作,那么其他任务就不能对该字典执行并发操作(读、写、删除), 否则会导致程序崩溃,比如:
{
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
go语言编译器提供了这种问题(竞争)的检测方式,比如:
# go run -race file.go
安全
可以使用 sync.RWMutex 实现同步,避免并发环境多goroutings同时读写操作,继续完善上面的例子,比如:
{
var lock = new(sync.RWMutex)
m := make(map[string]int)
go func(){
for {
lock.Lock()
m["a"]++
lock.Unlock()
time.Sleep(time.Microsecond)
}
}()
go func (){
for{
lock.RLock()
_ = m["b"]
lock.RUnlock()
time.Sleep(time.Microsecond)
}
}()
select{}
}
性能
在创建字典时预先准备足够的空间有助于提升性能,减少扩张时内存动态分配和重复哈希操作,比如:
package main
import "testing"
import "fmt"
func test() map[int]int {
m := make(map[int]int)
for i:=0; i < 1000; i++{
m[i] = 1
}
return m
}
func testCap() map[int]int{
m := make(map[int]int,1000)
for i:=0; i < 1000; i++{
m[i] = 1
}
return m
}
func BenchmarkTest(t *testing.B){
for i:= 0;i < t.N; i++{
test()
}
}
func BenchmarkTestCap(t *testing.B){
for i:= 0;i < t.N; i++{
testCap()
}
}
func main(){
resTest := testing.Benchmark(BenchmarkTest)
fmt.Printf("BenchmarkTest \t %d, %d ns/op,%d allocs/op, %d B/op\n", resTest.N, resTest.NsPerOp(), resTest.AllocsPerOp(), resTest.AllocedBytesPerOp())
resTest = testing.Benchmark(BenchmarkTestCap)
fmt.Printf("BenchmarkTestCap \t %d, %d ns/op,%d allocs/op, %d B/op\n", resTest.N, resTest.NsPerOp(), resTest.AllocsPerOp(), resTest.AllocedBytesPerOp())
}
输出:
# go run conmap.go
BenchmarkTest 10000, 160203 ns/op,98 allocs/op, 89556 B/op
BenchmarkTestCap 20000, 65478 ns/op,12 allocs/op, 41825 B/op
结构体(struct)
结构体由一系列被称为字段的命名元素组成。每个字段由名称和类型组成,字段名称可以被显示指定,也可以是匿名的。 声明一个结构体类型时,字段名称必须唯一,可使用”_“补位,支持使用自身指针类型成员。 字段名和排列顺序都属于结构体类型组成的部分,编译器会对结构体中的字段做对齐优化,比如:
type Person struct{
name string
age int
_ int
}
初始化
可按顺序初始化全部字段
使用命名方式初始化指定字段
type Person struct{
name string
age int
}
{
p := Person{
"Tom",
20,
}
/* //初始化排列顺序不能错乱
p := Person{
20,
"Tom",
}
*/
/*
p := Person{
"Tom"} //too few values in struct initializer
*/
p1 := Person{
name: "Tom",
age: 20,
}
p2 := Person{
name: "unknown",
}
}
因为命名初始化不受结构体扩展和字段顺序变化的影响,在日常开发中,建议使用命名初始化。
可以在函数体中直接定义匿名结构类型变量或用作字段类型,比如:
{
u := struct{ //直接定义匿名结构体变量
name string
age int
}{
"Tom",
20,
}
type file struct{
name string
attr struct{ //定义匿名结构类型字段
size int
perm int
}
}
f := file{
name: "passwd",
attr:{ //missing type in composite literal
size: 1,
perm: 1,
},
}
f.attr.size = 1 //ok
f.attr.perm = 1
}
结构体比较
只有在所有字段类型全部支持相等运算符时,才可以做相等操作,比如:
type data struct{
x int
//y map[int]int
y []int
}
func main(){
d1 := data{x:1}
d2 := data{x:2}
fmt.Println(d1 == d2) //(struct containing []int cannot be compared)
}
可以使用结构体指针直接操作结构体,但是不能是多级指针,比如:
{
type Person struct{
name string
age int
}
p := &Person{
"Li",
20,
}
fmt.Println(p)
p.name = "Tom"
p.age = 10
fmt.Println(p)
p2 := &p
*p2.name = "Tom2" //p2.name undefined (type **Person has no field or method name)
//(*p2).name = "Tom2"
fmt.Println(p2)
}
- 空结构体
空结构体(struct{}) 是指没有字段的结构体类型,无论是空结构体还是空结构体数组,其长度都为0,比如:
{
var a struct{}
var b [100]struct{}
println(unsafe.Sizeof(a), unsafe.Sizeof(b))
}
输出:
0 0
但这并不影响对元素的操作,切片的内置子切片、长度和容量等属性依旧可以工作比如:
{
var b [100]struct{}
s := b[:] //slice
s[0] = struct{}{}
s[1] = struct{}{}
s[2] = struct{}{}
fmt.Println(s[3], len(s),cap(s))
}
输出:
{} 100 100
空结构体可以像其他结构体一样正常使用,空结构体也具有正常结构体的属性,但就使用来说,一般用在通道元素类型做事件通知, 比如:
func hello(name string, done chan struct{}) {
fmt.Println("hello ",name)
done <- struct{}{}
}
func main() {
done := make(chan struct{})
langs := []string{"Go", "C", "C++", "Java", "Perl", "Python"}
for _, l := range langs {
go hello(l, done)
}
for _ = range langs {
<-done
}
}
- 匿名字段
匿名字段就是只有类型没有名字的字段(anonymous field),也称作嵌入类型或嵌入字段,比如:
type attr struct{
name string
age int
}
type Person struct{
id int
attr //结构体嵌入
}
func main(){
p := Person{
id: 20,
attr:attr{ //显示初始化
name: "Tom",
age: 20,
},
}
p.name = "kite" //可以直接读写匿名字段成员
p.attr.age = 21 //防止嵌入结构体成员与结构体成员重名
}
除接口指针和多级指针以外的任何命名类型都可以作为匿名字段,比如:
type data struct{
*int
//int //duplicate field int
string
}
func initData(){
x := 100
d := data{
int: &x, //使用基本类型作为字段名
string: "Tom",
}
fmt.Printf("%#v\n", d)
}
- 字段标签
Go语言结构体提供了在运行时,通过反射机制获取字段描述的元数据信息,尽管它不属于数据成员, 但却是类型的组成的部分,日常开发中,常被用做格式校验和数据库关系映射等。
type attr struct{
name string `姓名`
age int `年龄`
}
type Person struct{
id int `身份证号码`
attr `属性`
}
func main(){
p := Person{
id: 20,
attr:attr{
name: "Tom",
age: 20,
},
}
v := reflect.ValueOf(p)
t := v.Type()
for i,n := 0,t.NumField(); i < n; i++{
if v.Field(i).Kind().String() == "struct"{
v1 := v.Field(i)
t1 := v1.Type()
for j, m := 0, t1.NumField(); j < m ; j++{
fmt.Printf("%s: %v\n",t1.Field(j).Tag, v1.Field(j))
}
}else{
fmt.Printf("%s: %v\n",t.Field(i).Tag,v.Field(i).Kind())
}
}
}
输出:
身份证号码: int
姓名: Tom
年龄: 20
- 内存布局
不管结构体含有多少个字段,其内存总是一次性分配的,各字段在相邻的地址空间按定义顺序排列,编译器通常 会对其做对齐处理,对齐通常以所有字段中最长基础类型宽度为准。 对于字段是引用类型、字符串和指针的,结构内存中只包含基本数据,比如:
type Point struct{
x int
y int
}
type Value struct{
id int
name string
data []byte
next *Value
Point
}
func main(){
v := Value{
id: 1,
name: "Tom",
data:[]byte{1,2,3,4},
next:nil,
Point:Point{
x: 100,y:200,
},
}
fmt.Printf("size: %d,align: %d\n", unsafe.Sizeof(v), unsafe.Alignof(v))
fmt.Printf("field\taddress\t\toffset\tsize\n")
fmt.Printf("id\t%p\t%d\t%d\n", &v.id, unsafe.Offsetof(v.id), unsafe.Sizeof(v.id))
fmt.Printf("name\t%p\t%d\t%d\n", &v.name, unsafe.Offsetof(v.name), unsafe.Sizeof(v.name))
fmt.Printf("data\t%p\t%d\t%d\n", &v.data, unsafe.Offsetof(v.data), unsafe.Sizeof(v.data))
fmt.Printf("next\t%p\t%d\t%d\n", &v.next, unsafe.Offsetof(v.next), unsafe.Sizeof(v.next))
fmt.Printf("x\t%p\t%d\t%d\n", &v.Point.x, unsafe.Offsetof(v.Point.x), unsafe.Sizeof(v.Point.x))
fmt.Printf("y\t%p\t%d\t%d\n", &v.Point.y, unsafe.Offsetof(v.Point.y), unsafe.Sizeof(v.Point.y))
}
输出:
size: 72,align: 8
field address offset size
id 0xc4200140a0 0 8
name 0xc4200140a8 8 16
data 0xc4200140b8 24 24
next 0xc4200140d0 48 8
x 0xc4200140d8 0 8
y 0xc4200140e0 8 8
抽象布局:
|-------16--------|------------24------------| |------16-------|
+---+--------+--------+--------+--------+--------+----+-------+-------+
|id |name.ptr|name.len|data.ptr|data.len|data.cap|next|point.x|point.y|
+---+--------+--------+--------+--------+--------+----+-------+-------+
0 8 16 24 32 40 48 56 64 72
练习下面的列子
{
v1 := struct{
a byte
s string
b byte
}{}
fmt.Printf("size: %d,align: %d\n", unsafe.Sizeof(v1), unsafe.Alignof(v1))
fmt.Printf("a\t%p\t%d\t%d\n", &v1.a, unsafe.Offsetof(v1.a), unsafe.Sizeof(v1.a))
fmt.Printf("s\t%p\t%d\t%d\n", &v1.s, unsafe.Offsetof(v1.s), unsafe.Sizeof(v1.s))
fmt.Printf("b\t%p\t%d\t%d\n", &v1.b, unsafe.Offsetof(v1.b), unsafe.Sizeof(v1.b))
v2 := struct{
a byte
b []int
s byte
}{}
fmt.Printf("size: %d,align: %d\n", unsafe.Sizeof(v2), unsafe.Alignof(v2))
fmt.Printf("a\t%p\t%d\t%d\n", &v2.a, unsafe.Offsetof(v2.a), unsafe.Sizeof(v2.a))
fmt.Printf("b\t%p\t%d\t%d\n", &v2.b, unsafe.Offsetof(v2.b), unsafe.Sizeof(v2.b))
fmt.Printf("s\t%p\t%d\t%d\n", &v2.s, unsafe.Offsetof(v2.s), unsafe.Sizeof(v2.s))
v3 := struct{
a struct{}
b int
c struct{}
}{}
fmt.Printf("size: %d,align: %d\n", unsafe.Sizeof(v3), unsafe.Alignof(v3))
fmt.Printf("a\t%p\t%d\t%d\n", &v3.a, unsafe.Offsetof(v3.a), unsafe.Sizeof(v3.a))
fmt.Printf("b\t%p\t%d\t%d\n", &v3.b, unsafe.Offsetof(v3.b), unsafe.Sizeof(v3.b))
fmt.Printf("c\t%p\t%d\t%d\n", &v3.c, unsafe.Offsetof(v3.c), unsafe.Sizeof(v3.c))
v4 := struct{
a struct{}
}{}
fmt.Printf("size: %d,align: %d\n", unsafe.Sizeof(v4), unsafe.Alignof(v4))
fmt.Printf("a\t%p\t%d\t%d\n", &v4.a, unsafe.Offsetof(v4.a), unsafe.Sizeof(v4.a))
}