字符串
字符串是不可变字节(byte)序列,其本身是一个复合结构。内置函数len可以返回字节数组长度,cap不接受字符串类型参数。字符串的默认值为”“而不是nil。Go语言中使用‘`’防转义。支持”!=、==、<、>、+、+=”操作符。允许以索引访问字节数字(非字符),但不能获取元素地址。
func main(){
str := "abcde"
fmt.Println(str[1])
fmt.Println(&str[1]) //错误
}
用for遍历字符串时,分byte与rune两种方式
package main
import(
"fmt"
)
func main(){
str := "abcde"
for i := 0;i < len(str);i++{ //byte
fmt.Printf("%d: [%c]\n",i,str[i])
}
for i,s := range str{ //rune:返回数组索引以及字符
fmt.Printf("%d: [%c]\n",i,s)
}
}
输出:
0: [a]
1: [b]
2: [c]
3: [d]
4: [e]
0: [a]
1: [b]
2: [c]
3: [d]
4: [e]
转换
要修改字符串,需将其转换为可变类型([]runne或[]byte),待转换完成再转换回来。但不管如何转换都必须重新分配内存,并复制数据。
str := "abcd"
str1 := []byte(str) //转换为二进制[]byte类型
str2 := string(str1) //再次转换为字符串
用append函数,可将string直接追加到[]byte内:
var s []byte
s = append(s,"abc"...)
用加法操作符拼接字符串时,每次都须重新分配内存。如此在构建“超大”字符串时,性能就显得极差,解决的办法就是预先分配足够大的内存。
Unicode
类型rune专门用来存储Unicode编码,它是int32的别名,使用单引号的字面量其默认类型就是rune。
package main
import(
"fmt"
)
func main(){
str := '中'
fmt.Printf("%T",str)
}
输出:int32
数组
定义数组元素时,数组长度必须是非负整型常量表达式,长度是类型组成部分。也就是说类型相同但是长度不同的数组不属于同一类型。
package main
import(
"fmt"
)
func main(){
var a [4]int //定义长度为4的in同型数组,元素自动初始化为0
b := [4]int{2,5} //未提供初始值得元素自动初始化为0
c := [4]int{5,3:10} //可指定索引位置初始化
d := [...]int{1,2,3} //不指定数组长度,编译器按初始化数组元素的个数确定数组长度
e := [...]int{10,3:100} //支持索引初始化,但数组长度与之相关
fmt.Println(a,b,c,d,e)
fmt.Println(len(a),len(b),len(c),len(d),len(e))
}
输出:
[0 0 0 0] [2 5 0 0] [5 0 0 10] [1 2 3] [10 0 0 100]
4 4 4 3 4
对于结构等复合类型,可省略元素初始化类型标签。
package main
import(
"fmt"
)
func main(){
type user struct{
name string
age int
}
d := [...]user{
{"Tom",20}, //省略了类型标签
{"Jarry",18},
}
fmt.Printf("%#v\n",d)
}
输出:
[2]main.user{main.user{name:”Tom”, age:20}, main.user{name:”Jarry”, age:18}}
在定义多维数组时,仅允许第一维度使用”…”
a := [...][2]int{
{1,2}.
{3,4}
}
内置函数len和cap都返回第一维度长度。当然数组也支持”==、!=”操作符。
指针
在Go语言里也有指针数据与数组指针,相信很多C++程序员对此已经很熟悉了。指针数组是指元素为指针类型的数组,数组指针是指针指向数组的地址。
x,y := 1,2
a := [...]int{&x,&y} //指针数组
p := &a //存储数组地址的指针
复制
与C数组变量隐式作为指针使用不同,Go数组是值类型,赋值和传参操作都会复制整个数组数据。如果需要可改用指针或切片以此避免数据复制
package main
import(
"fmt"
)
func test(x *[2]int){
fmt.Printf("x: %p, %v\n",x,*x)
x[1] += 100
}
func main(){
a := [2]int{10,20}
test(&a)
fmt.Printf("a: %p, %v\n",&a,a)
}
输出:
x: 0xc04203e1d0, [10 20]
a: 0xc04203e1d0, [10 120]
从输出我们可以看出地址相同,也就是说使用了同一个数组
切片
切片(slice)本身是一个只读对象,其工作机制类似数组指针的一种包装,可基于数组或者数组指针创建切片,以开始和结束索引位置确定所引用的数组片段。
x := […]int{0,1,2,3,4,5,6,7,8,9}
expression | slice | len | cap |
---|---|---|---|
x[:] | [0 1 2 3 4 5 6 7 8 9] | 10 | 10 |
x[2:5] | [2 3 4] | 3 | 8 |
x[2:5:7] | [2 3 4] | 3 | 5 |
x[4:] | [4 5 6 7 8 9] | 6 | 6 |
x[:4] | [0 1 2 3] | 4 | 10 |
x[:4:6] | [0 1 2 3] | 4 | 6 |
属性cap表示切片所引用数组片段的真实长度,len用于限定可读的写元素数量。
与数组相同切片同样可以使用索引号访问元素内容。可直接创建切片对象,无需预先准备数组。因为是引用类型须使用make函数或显示初始化语句,它会自动完成底层数组内存分配。
package main
import(
"fmt"
)
func main(){
s1 := make([]int,3,5) //制定len、cap,底层数据初始化为0
s2 := make([]int,3) //省略cap,和len相等
s3 := []int{1,2,5:5} //按初始化元素分配底层数组
fmt.Println(s1,len(s1),cap(s1))
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 0 0 0 5] 6 6
切片只是很小的结构体对象,用来代替数组传参可避免复制开销。还有make函数允许在运行期动态制定数组长度,绕开了数组类型必须使用编译期常量的限制。但是并非所有时候都适合用切片代替数组,因为切片底层数组可能会再堆上分配内存。而且小数组在栈上拷贝的消耗也未必就比make代价大。
reslice
将切片视作[cap]silce数据源,据此创建新切片对象。不能超出cap,但不受len限制。新建切片依旧指向原底层数组,也就是说修改对所有关联切片可见。
package main
import(
"fmt"
)
func main(){
d := [...]int{0,1,2,3,4,5,6,7,8,9}
s1 := d[3:7]
s2 := s1[1:3]
for i := range s2{
s2[i] += 10
}
fmt.Println(d)
fmt.Println(s1)
fmt.Println(s2)
}
输出:
[0 1 2 3 14 15 6 7 8 9]
[3 14 15 6]
[14 15]
append
向切片尾部(slice[len])添加数据,返回新的切片对象。如超出cap限制,则为新的切片对象重新分配数组。新分配的数组长度为原cap的两倍。向nil切片追加数据时,会为其分配底层数组数据。
package main
import(
"fmt"
)
func main(){
s := make([]int,0,100)
s1 := s[:2:4]
s2 := append(s1,1,2,3,4,5,6) //超出s1 cap限制重新分配
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: 0xc04204c380: [0 0]
s2: 0xc0420460c0: [0 0 1 2 3 4 5 6] //数组地址不同,证明重新分配
s data: [0 0 0 0 0 0 0 0 0 0] //append并未向原数组写数据
s1 cap: 4,s2 cap: 8 //新数组是原数组cap的2倍
copy
在两个切片对象间复制数据,允许指向同一底层数组,允许目标区间重叠。最终复制长度以较短的切片长度(len)为准。
package main
import(
"fmt"
)
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)
m := copy(s2,s) //不同数据间的复制
fmt.Println(m,s2)
}
输出:
3 [0 1 2 3 5 6 7 7 8 9]
6 [0 1 2 3 5 6]
字典
字典是一种使用频率极高的数据结构,作为无序键值对集合,字典要求key必须支持相等运算符(==,!=)的数据类型,比如数字、字符串、指针、数组、结构体,以及对应的接口类型。
字典是引用类型,使用make函数或初始化表达语句来创建
package main
import(
"fmt"
)
func main(){
dict := make(map[string]int) //使用make创建字典
dict["a"] = 1 //为字典赋值
dict["b"] = 2
dict2 := map[int]struct{ //使用初始表达式。值为匿名结构类型
x int
}{
1 :{x:100},
2 :{x:200},
}
fmt.Println(dict,dict2)
}
输出:
map[a:1 b:2] map[2:{200} 1:{100}]
基本操作演示
package main
import(
"fmt"
)
func main(){
dict := make(map[string]int)
dict["a"] = 1
dict["b"] = 2
dict["a"] = 10 //修改
dict["c"] = 3 //新增
if v,ok := dict["d"];ok{ //使用ok-idiom判断key是否存在,返回值
fmt.Println(v)
}
fmt.Println(dict)
delete(dict,"b") //删除键值对。不存在时不会报错
fmt.Println(dict)
}
输出:
map[a:10 b:2 c:3]
map[a:10 c:3]
访问不存在的键值,默认返回零值,不会引发错误,但推荐使用ok-idiom模式。万一存储的value本就是零呢!
函数len返回当前键值对数量,cap不解释字典类型。另外因内存访问安全和哈希算法缘故,字典被设计为“not addressable”,故不能直接修改value成员。正确的做法是返回整个value待修改完成后再设置字典键值,或者直接使用指针类型。
package main
import(
"fmt"
)
type user struct{
name string
age int
}
func main(){
m := map[int]user{
1:{"Tom",20},
}
u := m[1] //返回整个value值,修改后再赋值
u.age += 1
m[1] = u
fmt.Println(m)
m2 := map[int]*user{ //使用指针
1:&user{"Jim",22},
}
m2[1].age += 1
fmt.Println(m2[1])
}
输出:
map[1:{Tom 21}]
&{Jim 23}
注:
- 不能对nil字典进行写操作,但却能读
- 内容为空的字典与nil是不同的
- 在迭代期间删除或新增键值时安全的
- 运行时会对字典并发操作做出检测,如果某个任务正在对字典进行写操作,那么其它任务就不能对该字典进行并发操作,否则会造成进程崩溃,可用sync.RWMutex实现同步。正如C++中的加锁。
- 字典本身就是指针包装,传参时无需再次取地址
- 在创建时预先准备足够的空间有助于提升性能
结构
结构体(struct)将多个不同类型命名字段序列打包成一个复合类型。字段名必须唯一,可用”_”补位,支持使用自身指针类型成员。不管结构体包含多少字段,其内存总是一次性分配的,各字段在相邻的地址空间按定义顺序排列。
package main
import(
"fmt"
)
type node struct{
_ int
id int
next *node //自身指针成员
}
func main(){
n1 := node{
id:1,
}
n2 := node{
id:2,
next:&n1,
}
fmt.Println(n1,n2)
}
输出:
{0 1 } {0 2 0xc0420443c0}
可按顺序初始化全部字段,或使用命名方式初始化指定字段。
package main
import(
"fmt"
)
func main(){
type user struct{
name string
age int
}
u1 := user{"Tom",12}
fmt.Println(u1)
}
输出:
{Tom 12}
可直接定义匿名结构类型变量,或者作为字段类型,但因其缺少类型标识,在作为字段类型时无法直接初始化。
package main
import(
"fmt"
)
func main(){
u := struct{ //直接定义匿名结构变量
name string
age int
}{
name:"Tom",
age:18,
}
type file struct{
name string
attr struct{ //定义匿名结构类型字段
owner int
perm int
}
}
f := file{
name:"Test",
}
f.attr.owner = 1
f.attr.perm = 2
fmt.Println(u,f)
}
输出:
{Tom 18} {Test {1 2}}
注
- 只有所在字段全部支持时,才能做相等操作
- 可使用指针直接操作结构字段,但不能是多级指针
空结构
空结构(struct{})是指没有字段的结构类型。它比较特殊,因为无论是其自身,还是作为数组类型其长度均为零。空结构可作为通道元素类型,用于事件通知。
func main{
exit := make(chan struct{})
go func(){
fmt.Println("hello word!")
exit <- struct{}{}
}()
<-exit
}
匿名字段
匿名字段是指没有名字,仅有类型的字段,也被称作嵌入字段或嵌入类型。
type attr struct{
perm int
}
type file struct{
name string
attr //匿名字段,仅有类型名
}
匿名字段只是隐式的以类型名作为字段名字,可直接引用匿名字段的成员,但初始化时须当作独立字段。不仅仅是结构体,除接口指针和多级指针以外的任何命名类型都可作为匿名字段。
不能将基础类型和其指针类型同时嵌入,因为两者隐式名字相同。
字段标签
字段标签(tag)并不是注释,而是用来对字段进行描述的元数据。它不属于数据成员但却是类型的组成部分。在运行期可使用反射获取标签信息。它常被用作格式校验,数据库关系映射等。
type user struct{
name string `姓名` //字段标签
age int `年龄`
}