从如下几个方面介绍GO语言的数据
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]
借鉴: 雨痕
讨论学习: 675020908