Go的数据类型
数据类型的种类
go
语言时一种静态类型的编程语言,所以在编译器进行编译的时候,就要知道每个值的类型,这样编译器就知道要位这个值分配多少内存,且终端分配的内存表示什么。
提前知道类型的好处有很多,比如编译器可以合理的使用这些值,进一步优化代码,提高执行效率,减少bug
等。
值类型
go
语言值类型包含数字
,字符串
,布尔型
,结构体(struct)
和数组
。数字类型支持整型和浮点型,且支持复数类型,其中位运算采用补码运算。
值类型传递时是拷贝的值,并且在对他进行操作时,生成的也是新创建的值,所以这些类型都是线程安全的,不用担心一个线程的修改会影响到另外一个线程的数据。
func modifyValue(v int){
v += 1
}
func main(){
v := 18 //声明一个int类型的变量v
fmt.Println(v) //打印变量v
modifyValue(v) //修改变量v
fmt.Println(v) //打印变量v
}
/*结果
18
18
*/
引用类型
引用类型和值类型恰恰相反,修改引用类型,可以影响到任何引用到它的变量。在go
语言中,引用类型有指针
,切片(slice)
,map
,接口
,函数
及channel
等。
引用类型之所以可以引用,是因为创建引用类型的变量其实是一个标头值,标头值里包含一个指针,指向底层的数据,当我们在函数中传递引用类型时,其实传递的是这个标头值的副本,它所指向的底层数据并没有被复制传递,这也是引用类型传递高效的原因。
所以本质上,我们可以理解在函数传参时都是值传递,只不过引用类型传递的是一个指向底层数据的指针的副本,所以在操作时,可以修改共享的底层数据,进而影响所有引用到这个共享底层数据的变量。
func modify(persons map[string]int){
persons["will"] = 20
}
func main(){
persons := map[string]int{
"leo" : 10,
"will" : 18,
}
fmt.Println(persons) //打印map类型的变量persons
modify(persons) //修改map类型的变量persons
fmt.Println(persons) //打印map类型的变量persons
}
/*结果
map[leo:10 will:18]
map[leo:10 will:20]
*/
复合类型
复合类型是一种聚合型的数据类型,通过组合值类型和引用类型,来表达更加复杂的数据结构。
要定义一个结构体的类型,是通过type
关键字和类型struct
进行声明的,结构体类型声定义后,就可以声明该类型了。
type person struct {
name string
age int
tel string
}
func main(){
var p1 person //申明person类型,默认初始化值
p2 := person { "test", 18, "10086" } //申明person类型
}
除了值类型外,结构体内的属性也可以是引用类型,或者其他自己定义的类型。具体的类型选择,根据实际情况,如允许修改值本身,可以选择引用类型,否则选择值类型。函数传参是值传递,所以对结构体来说也不例外,结构体传递的是其本身以及里面的值的拷贝。
//定义person结构体
type person struct {
name string
age int
tel string
}
//修改值,传入变量,将不会影响函数外的变量
func modifyPerson1(p person){
p.age++
}
//修改值,传入指针,将影响函数外的变量
func modifyPerson2(p *person){
p.tel = "10010"
}
func main(){
p := { name = "test", age = 18, tel = "10086" } //申明一个person类型的变量p
fmt.Println(p) //打印变量p
modifyPerson1(p) //修改变量p,传入p的值
fmt.Println(p) //打印变量p
modifyPerson2(&p) //修改变量p,传入p的内存地址
fmt.Println(p) //打印变量p
}
/*结果
{test 18 10086}
{test 18 10086}
{test 18 10010}
*/
数字类型
整数类型
序号 | 类型 | 描述 |
---|---|---|
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
为复数的默认类型。
x, y := 1.0, 2.0
var v complex128 = complex(x,y)
fmt.Println(v)
/*结果
(1+2i)
*/
字符类型
go
语言中有两种字符类型,一种是byte
类型,代表一个ASCII
字符。另一种是rune
类型,代表一个Unicode
字符。
byte类型
byte
占用1个字节,就是8位比特位,所以它和uint8
类型本质上没有区别,byte
通过数字表示ACSII
中的一个字符。另外,在go
语言中,单引号'
和双引号"
并不是等价的。单引号'
用来表示字符,双引号"
用来表示字符串。
func main(){
var a byte = 'A'
var b uint8 = 'A'
//打印变量
fmt.Println(a)
fmt.Println(b)
//打印单引号括起来的字符字面值
fmt.Printf("%q\n",a)
fmt.Printf("%q\n",b)
//打印所占字节数
fmt.Println(unsafe.Sizeof(a))
fmt.Println(unsafe.Sizeof(b))
}
/*结果
65
65
'A'
'A'
1
1
*/
rune类型
rune
占用4个字节,共32位比特位,所以它和uint32
类型本质上没有区别,rune
通过数字表示Unicode
中的一个字符。(Unicode
是一个可以表示世界范围内的绝大部门字符的编码规范)
func main(){
var a rune = 'A'
var b uint32 = 'A'
//打印变量
fmt.Println(a)
fmt.Println(b)
//打印unicode码的值
fmt.Printf("%c\n",a)
fmt.Printf("%c\n",b)
//打印所占字节数
fmt.Println(unsafe.Sizeof(a))
fmt.Println(unsafe.Sizeof(b))
}
/*结果
65
65
A
A
4
4
*/
字符串类型
字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常实用来包含可读的文本,字符串是UTF-8
字符的一个序列(当字符为ASCII
码时则占用1个字节,其他字符根据需要占用2-4个字节)。字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容。或者也可以说字符串是字节的定长数组。
UTF-8
是一种被广泛使用的编码格式,是文本文件的标准编码,XML
和JSON
也使用该编码格式。由于该编码格式对占用字节长度的不定性,在go
语言中字符串也可能根据需要占用1至4个字节,这与其他编码语言不同。这样做不仅减少了内存和硬盘空间占用,同时也无需对UTF-8
编码格式的文本进行编码和解码。
定义字符串
- 使用双引号
"
定义字符串
str := "Hello World!"
- 使用反引号(重音符)
`
定义多行字符串
//定义多行字符串,文本原样输出
str := `Hello
World`
fmt.Print(str)
/*结果
Hello
World
*/
- 使用
+
拼接字符串
//拼接字符串
str := "hello" + " " + "World" + "!"
fmt.Println(str)
/*结果
hello World!
*/
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
的类型分为值类型
和引用类型
,我们在创建值类型
的变量时,存储的时数据值本身。而我们创建引用类型
的变量时,存储的是数据值的内存地址。
指针的创建
指针的创建有两种方式:
- 通过
&
获取变量的内存地址
str := "Hello World!"
ptr := &str //获取str的指针
- 通过
new()
函数创建指针变量
// func new(Type) *Type
ptr := new(string) //获取string类型的指针变量
*ptr = "Hello World!" //为指针变量指向的内存地址赋值
指针的操作都离不开两个符号:
&
取址运算符,从一个普通变量中取得内存地址*
取值运算符,从一个指针变量中取得变量的值
当一个指针被定义后未分配任何值时,例如new()
函数创建出的初始指针变量,它的值为nil
,nil指针
又被称作空指针
。空指针
因为没有指向任何地址,所以无法通过*
直接赋给该变量数据值。
指针类型的转换
在go
中,不同类型的指针是无法进行类型转换的,但他们都是可以转换成同一个类型unsafe.Pointer
。而unsafe.Pointer
是一种特殊的指针,它可以包含任意类型的地址,类似于C语言
的void*
指针。
import(
"unsafe"
"math"
)
func main(){
var num int32 = 10
ptr1 := &num
fmt.Println(num) //打印num
fmt.Println(ptr1) //打印num的指针
ptr2 := (*float64)(unsafe.Pointer(ptr1)) //转换指针的类型为float64
fmt.Println(ptr2)
*ptr2 = math.MaxFloat64 //设置num的值
fmt.Println(num) //打印num
fmt.Println(*ptr1)
fmt.Println(*ptr2)
}
/*结果
10
0xc0000b2004
0xc0000b2004
-1
-1
1.7976931348623157e+308
*/
从上面的代码,我们可以看到ptr1
和ptr2
指针的地址是一致的,但他们的类型是不同的。当通过ptr2
赋值超过int32
大小的值时,不管是打印num
还是*ptr
,都无法显示出来(返回-1),而打印*ptr2
却可以打印出来。不过像这种做法都是不安全的,所以非必要不要使用。