Go语言之数据类型总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言之数据类型总结相关的知识,希望对你有一定的参考价值。

字符串:字符串是不可变的字节序列,本身就是一个复合结构。

头部指针指向字节数组,但是没有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的类型做对齐处理,以便其地址不会越界,避免引发垃圾回收错误。

对齐的原因与硬件有关,以及访问效率有关。


以上是关于Go语言之数据类型总结的主要内容,如果未能解决你的问题,请参考以下文章

2Go语言基础之数据类型

2Go语言基础之数据类型

Go语言基础之基本数据类型

Go语言基础之基本数据类型

Go语言基础之基本数据类型

Go语言之数据类型详解