字符串:字符串是不可变的字节序列,本身就是一个复合结构。
头部指针指向字节数组,但是没有NULL结尾。默认以UTF-8编码存储Unicode字符,字面量里允许使用十六进制、八进制和UTF-8编码格式。
内置函数len返回字节数组长度,cap不接受字符串类型参数。字符串默认是nil而不是""。
使用反引号定义的字符串不做转义处理,并支持跨行。
支持!=、==、<、>、+、+=操作符。
允许以索引号访问字节数组(非字符),但是不能获取元素地址。
以切片语法返回子串的时候,其内部依旧指向原字节数组。
package main
import "fmt"
func main() {
str := "Hello World!"
fmt.Println(str)
fmt.Println(str[1:3])
fmt.Println(str[:])
fmt.Println(str[:2])
/*
Hello World!
el
Hello World!
He
*/
fmt.Println(str[3])//108
}
使用for循环遍历字符串的时候分为rune和byte两种方式。
转换:如果要修改字符串,需要将其修改为可变类型[]rune或[]byte,完成后再次转换为字符串。无论如何转换都需要重新分配内存并复制数据。
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := "HelloWorld"
pp("s:x\n",&s)
bs := []byte(s)
s2 := string(bs)
pp("stirng 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)
}
func pp(format string, ptr interface{}){
p := reflect.ValueOf(ptr).Pointer()
h := (*uintptr)(unsafe.Pointer(p))
fmt.Printf(format,*h)
}
/*
s:x
%!(EXTRA uintptr=5015415)stirng to []byte,bs: c04204c0b0
[]byte to string,s2: c04204c0c0
string to []rune,rs: c042068030
[]rune to string,s3:c04204c0e0
*/
使用加法操作符拼接字符串的时候,每次都会重新分配内存。如此,在构建超大的字符串的时候性能会变差。可以使用strings.Jion函数,它会统计所有参数的长度,并一次性完成内存分配操作。
使用bytes.Buffer也能完成类似的操作,并且性能相当。
Unicode类型rune专门用来存储Unicode码点,它是int32的别名,相当于UCS-4/UTF-32的编码格式,使用单引号的字面量,默认值是rune。
package main
import "fmt"
func main() {
r := '我'
fmt.Println("%r", r)
}
//%r 25105
数组:
定于数组类型的时候,数组的长度必须是非负×××常量表达式,长度是类型数组的组成部分,也就是说元素类型相同,但是长度不同的数组类型不同。
对于复合类型,可省略元素初始化类型标签。
在定义多维数组的时候,仅仅第一维度允许使用“...”。
内置函数len和cap都返回第一维度长度。
package src
import (
"fmt"
)
func main() {
arr := [2]int
arr_2 := [...][2]int{
{10,20},
{10,20},
{20,30},
}
fmt.Println(len(arr),cap(arr))
fmt.Println(len(arr_2),cap(arr_2))
fmt.Println(len(arr_2[0]),cap(arr_2[0]))
}
/*
2 2
3 3
2 2
*/
指针:指针数组是指元素为指针类型的数组,数组指针是获取数组变量的地址。指针可以获取任意元素的地址。
数值指针可以直接用来操作元素。
package main
import (
_ "fmt"
)
func main(){
a := [10]int{1,2,3,4,5,6,7,8,9,0}
println(&a,&a[0],&a[1])
}
复制:与C数组变量隐式作为指针使用不同,Go数组是值类型,赋值和传参都会复制整个数组数据。如果需要,可以改为切片或者是指针。
切片:切片本身并不是动态数组或者是数组指针,他通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内。
切片本身就是一个只读对象,以开始和结束索引位置确定所引用的数组片段,不支持反向索引,实际范围是一个右半开区间。
属性cap表示切片所引用数组片段的真实程度。
len用于限定可读写元素数量。另外数组必须addressable,否则会引发错。
和数组一样,切片同样使用索引号访问元素内容,其实索引为0,而非对应底层数组真实的索引位置。
可以直接创建切片对象,不需要预先准备数组。因为是引用类型,必须使用make函数或者是显示初始化语句,它会自动完成底层数组内存的分配。
使用make函数可以指定len和cap的值,省略cap,和len相等。
1 package main
2 import (
3 "fmt"
4 )
5 func main(){
6 s_1 := make([]int,3,5)
7 s_2 := make([]int,3)
8 s_3 := []int{10,20,5:30}
9
10 fmt.Println(s_2,len(s_2),cap(s_2))
11 fmt.Println(s_1,len(s_1),cap(s_1))
12 fmt.Println(s_3,len(s_3),cap(s_3))
13
14 }
运行结果:
[0 0 0] 3 3
[0 0 0] 3 5
[10 20 0 0 0 30] 6 6
切片是很小的结构体,用来代替数组传参可以避免复制开销,make函数允许在运行期动态指定数组长度,绕开了数组类型必须使用编译器常量的限制。
并不是所有的时候都适合使用切片代替数组,因为切片底层数组可能会在堆上分配内存,而且小数组在栈上拷贝数据的消耗也未必就比make代价大。
将切片看作[cap]slice数据源,据此创建新切片对象。不能超出cap,但是不受len限制。
append函数向切片尾部添加数据,返回行的切片对象。
1 package main
2 import (
3 "fmt"
4 )
5 func main(){
6 s1 := make([]int,0,10)
7 s2 := append(s1,10)
8 s3 := append(s2,20,30,40)
9 fmt.Println(s3)
10 fmt.Println(s2)
11 fmt.Println(s1)
12 }
运行结果:
[10 20 30 40]
[10]
[]
数据被追加到原底层数组,如果超出cap限制,就是新切片对象重新分配数组。
向nil切片追加数据的时候,会为其分配底层数组内存。
正是因为存在重新分配底层数组的缘故,在某些场合建议预留更多足够的空间,避免中途内存分配和数据复制开销。
copy在两个切片对象间复制数据,允许指向同一个底层数组,允许目标区间重叠。最终所复制长度都以比较短的切片长度(len)为准。
1 package main
2 import "fmt"
3 func main(){
4 s := []int{1,2,3,4,5,6}
5 s1 := []int{7,8,9}
6 s2 := copy(s,s1)
7 fmt.Println(s)
8 fmt.Println(s1)
9 fmt.Println(s2)
10 }
运行结果:
[7 8 9 4 5 6]
[7 8 9]
3
如果切片长时间引用大数组中很小的片段,那么建议新建独立切片。复制出所需要的数据,以便原始的数组可以及时回收。
字典:
字典就是一种哈希表,它是一种使用频率极高的数据结构。将其作为语言内置类型从运行时层面进行优化,可以获得更高的性能。
字典是无序的键值对集合,字典要求Key必须是支持相等运算符的数据类型,使用make函数或者初始化表达式语句来创建。访问不存在的兼职,默认返回0,不会引发错误。但是推荐使用ok-idiom模式,毕竟通过零值无法判断键值是否存在,或者是存储的value本就是0.
对字典进行迭代,每次返回键值的次序是不一样的。函数len返回键值对的数量,函数cap不接受字典类型的数据。不能直接修改vlaue成员,因为字典被设计成not addressable的。
正确的做法应该是返回整个value,待修改后在设置字典的键值。或者是直接使用指针来修改。不能对字典进行读写,但是能读。内容为空的字典和nil是不一样的。
在迭代期删除或者是新增键值是安全的。
运行时对字典并发操作做出检测。如果某个任务正在对字典进行写操作,那么其他任务就不能对字典执行并发操作(读写)。否则会导致进程崩溃。
可以启用数据竞争检查此类问题,可以使用sync.RWMutex实现同步,避免读写操作同时进行。字典对象本身就是指针包装,传参时不需要再次取地址。
在创建预先准备足够空间有助于性能提升,减少扩张时内存分配和重新哈希操作。
对于海量小数据对象,应该直接使用字典存储键值数据拷贝,而不是指针。这有助于减少需要扫描的对象数据,大幅度减少垃圾回收的时间。另外,字典不会收缩内存,所以适当替换成新的对象时有必要的。
1 package main
2 import "fmt"
3 func main(){
4 m := make(map[int]string)
5 fmt.Println(m)
6 for i:=0;i<10;i++{
7 m[i] = "Hello"
8 }
9 fmt.Println(m)
10 }
运行结果:
map[]
map[0:Hello 5:Hello 6:Hello 9:Hello 7:Hello 8:Hello 1:Hello 2:Hello 3:Hello 4:Hello]
1 package main
2
3 import "fmt"
4
5 func main() {
6 m := make(map[int]string)
7 for i := 0; i < 26; i++ {
8 m[i] = string(97 + i)
9 }
10 fmt.Println(m, len(m))
11 if v, ok := m[25]; ok {
12 fmt.Println(v)
13 fmt.Println(ok)
14 }
15
16 if v, ok := m[28]; ok {
17 fmt.Println(v)
18 fmt.Println(ok)
19 }
20 }
运行结果:
map[1:b 4:e 7:h 11:l 15:p 19:t 5:f 10:k 18:s 24:y 3:d 6:g 8:i 9:j 21:v 22:w 23:x 0:a 2:c 12:m 13:n 14:o 16:q 17:r 20:u 25:z] 26
z
true
结构:字段名必须唯一,可用“_”补位,支持使用自身类型成员。除对齐处理外,编译器不会优化、调整内存布局。推荐使用命名初始化。这样在扩充结构字段或者是调整字段顺序时,不会导致初始化语句出错。
可以直接定义匿名结构类型变量或用作字段类型,但是因为其缺少类型标识,在作为字段类型时无法直接初始化。
1 package main
2 import "fmt"
3 func main(){
4 u := struct {
5 name string
6 age int
7
8 }{
9 name:"tom",
10 age:20,
11 }
12
13 type file struct {
14 name string
15 attr struct {//声明匿名字段
16 owner int
17 perm int
18 }
19 }
20
21 f := file {
22 name:"test.dat",
23 }
24
25 f.attr.owner = 1
26 f.attr.perm = 0775
27 fmt.Println(u,f)
28 }
运行结果:
{tom 20} {test.dat {1 509}}
可以使用指针直接操作结构字段,但是不能是多级指针。
空结构是指没有字段的结构类型。它比较特殊,因为无论是其自身,还是作为数组元素类型,其长度都是0。
尽管没有分配数组内存,但是依然可以操作元素,对应切片len,cap属性也正常。
实际上,这类长度为零的对象通常指向runtime.zerobase变量。
空结构可以作为通道元素类型,用于事件通知。匿名字段,是没有名字,仅有类型的字段,也被称作嵌入字段或者是嵌入字段。
从编译器角度看,这只是隐式的以类型名作为字段名称而已。可以直接引用匿名字段的成员,但是初始化的时候必须当做独立字段使用。
不能将基础类型和其他指针类型同时嵌入,因为两者隐式名字相同。
错误实例:
type data struct {
*int
int
}
字段标签:字段标签并不是注释,而是用来对字段进行描述的元数据。尽管它不属于数据类型成员,但是却是类型的组成部分。
在运行期。可用反射获取标签信息。它常用来格式校验或者是数据库关系映射。
标准库中reflect.StructTag提供了分析和解析功能。
不管结构体包含多少字段,其内容总是一次性分配,各个字段在相邻的地址空间按照定义顺序排序。
借助unsafe包中的相关函数,可以输出所有字段的偏移量和长度。在分配内存的时候,字段必须做对齐处理,通常以所有字段中最长的基础类型宽度为标准。
空结构类型字段,如果它是最后一个字段,那么编译器将会当做长度为1的类型做对齐处理,以便其地址不会越界,避免引发垃圾回收错误。
对齐的原因与硬件有关,以及访问效率有关。
共同学习,写下你的评论
评论加载中...
作者其他优质文章