Go的数据类型
数据类型的种类
go语言时一种静态类型的编程语言,所以在编译器进行编译的时候,就要知道每个值的类型,这样编译器就知道要位这个值分配多少内存,且终端分配的内存表示什么。
提前知道类型的好处有很多,比如编译器可以合理的使用这些值,进一步优化代码,提高执行效率,减少bug等。
值类型
go语言值类型包含数字,字符串,布尔型,结构体(struct)和数组。数字类型支持整型和浮点型,且支持复数类型,其中位运算采用补码运算。
值类型传递时是拷贝的值,并且在对他进行操作时,生成的也是新创建的值,所以这些类型都是线程安全的,不用担心一个线程的修改会影响到另外一个线程的数据。
1 | func modifyValue(v int){ |
引用类型
引用类型和值类型恰恰相反,修改引用类型,可以影响到任何引用到它的变量。在go语言中,引用类型有指针,切片(slice),map,接口,函数及channel等。
引用类型之所以可以引用,是因为创建引用类型的变量其实是一个标头值,标头值里包含一个指针,指向底层的数据,当我们在函数中传递引用类型时,其实传递的是这个标头值的副本,它所指向的底层数据并没有被复制传递,这也是引用类型传递高效的原因。
所以本质上,我们可以理解在函数传参时都是值传递,只不过引用类型传递的是一个指向底层数据的指针的副本,所以在操作时,可以修改共享的底层数据,进而影响所有引用到这个共享底层数据的变量。
1 | func modify(persons map[string]int){ |
复合类型
复合类型是一种聚合型的数据类型,通过组合值类型和引用类型,来表达更加复杂的数据结构。
要定义一个结构体的类型,是通过type关键字和类型struct进行声明的,结构体类型声定义后,就可以声明该类型了。
1 | type person struct { |
除了值类型外,结构体内的属性也可以是引用类型,或者其他自己定义的类型。具体的类型选择,根据实际情况,如允许修改值本身,可以选择引用类型,否则选择值类型。函数传参是值传递,所以对结构体来说也不例外,结构体传递的是其本身以及里面的值的拷贝。
1 | //定义person结构体 |
数字类型
整数类型
| 序号 | 类型 | 描述 |
|---|---|---|
| 1 | uint8 |
无符号 8 位整型 (0 到 255) |
| 2 | uint16 |
无符号 16 位整型 (0 到 65535) |
| 3 | uint32 |
无符号 32 位整型 (0 到 4294967295) |
| 4 | uint64 |
无符号 64 位整型 (0 到 18446744073709551615) |
| 5 | int8 |
有符号 8 位整型 (-128 到 127) |
| 6 | int16 |
有符号 16 位整型 (-32768 到 32767) |
| 7 | int32 |
有符号 32 位整型 (-2147483648 到 2147483647) |
| 8 | Int64 |
有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
| 9 | uint |
32 或 64 位,根据操作系统32位或64位 |
| 10 | int |
32 或 64 位,根据操作系统32位或64位 |
int类型和uint类型
int和uint并没有一个固定的位数,在32位系统中,int和uint都占用4个字节,也就是32位。而在64位系统中,int和uint都占用8个字节,也就是64位。
在某些场景下,比如在二进制传输或读写文件的结构描述时,应当避免使用int和uint,这样可以保证文件的结构不会受到不同平台的不同编译导致的字节长度不同的影响。
浮点类型
| 序号 | 类型 | 描述 |
|---|---|---|
| 1 | float32 |
IEEE-754 32位浮点型数 |
| 2 | Float64 |
IEEE-754 64位浮点型数 |
| 3 | complex64 |
32位实数和虚数 |
| 4 | complex128 |
64位实数和虚数 |
复数
复数是由两个浮点数表示的,其中一个表示实部(real),另一个表示虚部(imag)。在go中由两个复数类型,complex128(64位实数和虚数)和complex64(32位实数和虚数),complex128为复数的默认类型。
1 | x, y := 1.0, 2.0 |
字符类型
go语言中有两种字符类型,一种是byte类型,代表一个ASCII字符。另一种是rune类型,代表一个Unicode字符。
byte类型
byte占用1个字节,就是8位比特位,所以它和uint8类型本质上没有区别,byte通过数字表示ACSII中的一个字符。另外,在go语言中,单引号'和双引号"并不是等价的。单引号'用来表示字符,双引号"用来表示字符串。
1 | func main(){ |
rune类型
rune占用4个字节,共32位比特位,所以它和uint32类型本质上没有区别,rune通过数字表示Unicode中的一个字符。(Unicode是一个可以表示世界范围内的绝大部门字符的编码规范)
1 | func main(){ |
字符串类型
字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常实用来包含可读的文本,字符串是UTF-8字符的一个序列(当字符为ASCII码时则占用1个字节,其他字符根据需要占用2-4个字节)。字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容。或者也可以说字符串是字节的定长数组。
UTF-8是一种被广泛使用的编码格式,是文本文件的标准编码,XML和JSON也使用该编码格式。由于该编码格式对占用字节长度的不定性,在go语言中字符串也可能根据需要占用1至4个字节,这与其他编码语言不同。这样做不仅减少了内存和硬盘空间占用,同时也无需对UTF-8编码格式的文本进行编码和解码。
定义字符串
- 使用双引号
"定义字符串
1 | str := "Hello World!" |
- 使用反引号(重音符)
`定义多行字符串
1 | //定义多行字符串,文本原样输出 |
- 使用
+拼接字符串
1 | //拼接字符串 |
UTF-8和Unicode
unicode和ASCII类似,都是一种字符集,但unicode支持更多的字符。而UTF-8是使用unicode字符集的编码规则。因为unicode的所有字符都需要占用2个字节的大小,不够的就用0补全,这就导致过于浪费空间。而UTF-8编码虽然使用unicode字符集进行编码,但在传输和存储上减少了字节的占用。(减少ASCII中的字符空间占用,只占用1个字节。其实UTF-8中文占用3个字节,反而比unicode占用更大)
指针类型
什么是指针
当我们创建一个变量 var str string = "Hello World!",str只是编程语言中方便程序员编写和理解的一个标签,只是一个变量名。当我们访问这个标签时,计算机会返回一个值Hello World!,这个值被存放在内存中,那么我们想要得到这个值,必须知道他的内存地址,内存地址会指向内存的一块区域,这个时候我们就可以通过内存地址来获取到这个值。而保存内存地址的变量就称为指针变量。在某些时候,我们需要某个变量的内存地址,这个时候我们就可以创建一个指针变量,通常叫做ptr(pointer)。
那么根据变量中存的值的不同,我们可以简单的对变量划分为普通变量和指针变量,普通变量储存的是数据的值,而指针变量储存的是数据值的内存地址。
但go的类型分为值类型和引用类型,我们在创建值类型的变量时,存储的时数据值本身。而我们创建引用类型的变量时,存储的是数据值的内存地址。
指针的创建
指针的创建有两种方式:
- 通过
&获取变量的内存地址
1 | str := "Hello World!" |
- 通过
new()函数创建指针变量
1 | // func new(Type) *Type |
指针的操作都离不开两个符号:
&取址运算符,从一个普通变量中取得内存地址*取值运算符,从一个指针变量中取得变量的值
当一个指针被定义后未分配任何值时,例如new()函数创建出的初始指针变量,它的值为nil,nil指针又被称作空指针。空指针因为没有指向任何地址,所以无法通过*直接赋给该变量数据值。
指针类型的转换
在go中,不同类型的指针是无法进行类型转换的,但他们都是可以转换成同一个类型unsafe.Pointer。而unsafe.Pointer是一种特殊的指针,它可以包含任意类型的地址,类似于C语言的void*指针。
1 | import( |
从上面的代码,我们可以看到ptr1和ptr2指针的地址是一致的,但他们的类型是不同的。当通过ptr2赋值超过int32大小的值时,不管是打印num还是*ptr,都无法显示出来(返回-1),而打印*ptr2却可以打印出来。不过像这种做法都是不安全的,所以非必要不要使用。
