数据类型
前言
与其它编程语言差不多,Go原生支持一些常见的数值型、字符串型等数据类型。
基本数据类型
数值类型
- 整数:int、uint、int8(1个字节)、uint8、int16(2个字节)、int32(4个字节)、int64(8个字节)
- 浮点数:float32、float64
- 复数:complex64、complex128
布尔类型
取值有false
和true
两种
字符串类型
- string
- rune
相较于C语言,Go语言原生支持字符串类型。
- 字符串类型的数据不可变,这个特性在多协程并发时能提供一定的安全性。
var s1 string = "hello"
s1[0] = 'w' // 错误。字符串内容不可变
s1 = "world" // ok
- 获取字符串长度的时间复杂度是常数时间。
- 使用反引号实现原生字符串,而不需要转义符。(除了反引号)
var s1 string = `!@#$%^&*()QWERTYUIOasdfagsds`
fmt.Println(s1) // !@#$%^&*()QWERTYUIOasdfagsds
- 原生采用Unicode字符集,避免源码在不同环境下显示乱码的可能。
指针
指针本质上就是内存地址,一般为内存中存储的变量值的起始位置。指针变量即存储内存地址的变量。
指针的优势在于,当我们需要把大量的数据作为函数参数进行传递时,高效的方法不是传递数据本身,而是使用指向数据的指针作为函数的参数,这样就无需复制一个副本来进行操作,速度更快,内存占用更低。
在C/C++语言中,指针直接操作内存的特性使得 C/C++ 具备极高的性能,但指针偏移、指针运算和内存释放可能引发的错误也让指针编程饱受诟病。
Go语言限制了指针类型的偏移和运算能力,保留 了指针高效访问的特性。
在Go语言中,指针包含以下三个概念:
- 指针地址
- 指针类型
- 指针取值
在程序运行的过程中,每一个变量的值都保存在内存中,变量对应的内存有其特定的地址。假设某一个变量的类型为 T,在Go语言中,我们可以通过取址符号 &
获取该变量对应内存的地址,生成该变量对应的指针。此时,变量的内存地址即生成的指针的值,指针类型为 *T
,称为 T 的指针类型,*
代表指针。
指针的默认值是nil
,可以通过判断nil
来确定指针变量是否有值。
指针的基础用法
// 取变量的内存地址
&varname
// 取内存地址的值
*memAddress
// 创建一个指针变量,默认值为nil
var p0 *int
// 声明一个变量的指针: var 指针名 *类型 = 初始化值
var a int = 7
var p1 *int = &a
// 创建指针类型: 指针名 := new(类型)
// 通过new(创建指针时,go编译器会申请内存空间,并将内存空间的值置为0)
p2 := new(float)
// 指针的指针
p1p1 := &p
// *&成对出现相当于互相取消,例如*&*&a就是a
// 指向数组的指针
arr1 := [3]int{1,2,3}
var p3 *[3]int = &arr1
// 指针数组
var p4 [3]*int // 创建能够存放三个指针变量的数组
指针变量声明后如果没有初始化就可能产生“野指针”。对野指针进行操作会导致panic
错误。
地址传参
Go语言中函数的参数都是按值进行传递的,即使参数是指针,也只是指针的一个副本。习惯上把指针的函数参数称为地址参数,而非指针的函数参数成为值参数。
示例代码:
package main
import (
"time"
"fmt"
)
func change(arr [1024]int) {
// 值传参。参数为整数型数组
for i,v := range arr {
arr[i] = v * 2
}
}
func changeByAddress(arr *[1024]int) {
// 地址传参。参数为整型数组指针
for i,v := range *arr {
arr[i] = v * 2
}
}
func main() {
arr := [1024]int{}
for i:=0;i<1024;i++ {
arr[i] = i
}
start := time.Now()
sum := 0
for j := 0; j < 1000000; j++ {
change(arr)
sum++
}
end := time.Since(start)
fmt.Println("change(arr)执行1000000次耗时: ", end)
//fmt.Println(arr)
start = time.Now()
sum = 0
for j := 0; j < 1000000; j++ {
changeByAddress(&arr)
sum++
}
end = time.Since(start)
fmt.Println("changeByAddress(&arr)执行1000000次耗时: ", end)
//fmt.Println(arr)
}
如果打印arr的值,可以发现值传参函数并未修改arr的值,而地址传参后,arr的值有改变
复合数据类型
数组
定义数组的基本语法如下:
// var 数组名 [数组长度]数据类型
var arr [100]int
// 短变量形式
arr2 := [5]int{1,2,3,4,5}
// 使用省略号自动计算数组长度
arr3 := [...]int{1,2,3,4,5}
// 索引为4的值为100,其它为默认值
arr4 := [5]int{4:100}
// 索引0值为10,索引4值为11,索引9值为100,其他为默认值,并自动计算长度,长度为10
arr5 := [...]int{0:10,4:11,9:100}
- 只有常量能定义数组长度,变量不行。
- 数组一旦定义,每个元素就会自动初始化为零值。
- 将一个数组赋值给另一个数组,或者数组作为函数参数时,实际上是复制原数组。因此如果一个数组很大,那么数组的这类操作是比较消耗资源的。而且函数内对传入的数组进行修改,不影响原数组。
- 静态语言的数组大小确定,类型一致。
- go语言的数组可以取值、改值,但无法删值。
- go语言中的数组是值类型,
[3]string{}
和[5]string{}
都是数组,但不是一个类型。
遍历数组
arr3 := [4]int{1,2,3,4}
for i,v := range arr3 {
fmt.Println(i,v)
}
for j:=0;j<len(arr3);j++ {
fmt.Println(j,number[j])
}
数组作为函数的参数
示例代码:
func sum(arr [100]int) int {
length := len(arr)
sum := 0
for i :=0; i < length; i++ {
sum += arr[i]
}
}
func main() {
var arr [100]int
length := len(arr)
for i := 1; i <= length; i++ {
arr[i-1] = i
}
}
二维数组
二维数组的定义方式如下:
var 数组名 [行长度][列长度]数组类型
切片
表示一组具有相同数据类型的集合,但是长度不固定。切片只定义的话是无法使用的,还需要用make
内置函数进行初始化才能进行数据的存取。
切片和数组的操作基本相同,区别在于数组是固定长度的,而切片是可以扩充容量的。
定义切片的方法如下:
var slice []int
切片光定义还不能直接用,需要通过make
进行初始化才能进行数据的存取。定义和初始化切片可以使用更简洁的方法:
// 切片名 := make([]数据类型,长度,容量)
slice := make([]int,4,6)
当然也可以显示声明:
var name []string = []string{"Go","Python","Java","C++","PHP"}
遍历切片
var name []string = []string{"Go","Python","Java","C++","PHP"}
for i,v := range name {
fmt.Println(i,v)
}
切片作为函数的参数
- 切片如果作为函数的参数是引用传递,函数内部的切片参数和外部的切片参数实际上的底层数组是同一个对象,因此函数内部修改切片的值也会影响到外部的切片参数值。
func sum(slice []int) int {
var sum int = 0
for _,i := range slice {
sum += i
}
return sum
}
var slice4 []int = []int{1,3,5,7,9}
fmt.Println(sum(slice4))
切片扩容
- 扩容阶段会申请内存空间,影响速度
- 扩容一旦触发,就会指向新的内存空间
var name []string = []string{"Go","Python","Java","C++","PHP"}
name = append(name, "lua")
fmt.Println(name)
- 切片扩容策略:
- 如果新申请容量是旧容量的两倍,最终容量就是新申请的容量
- 如果旧切片长度小于1024,则新容量是旧容量的2倍
- 如果就切片长度大于1024,则新容量是就容量的1.25倍
slice1 := make([]int, 0) // len=0, cap=0
slice1 = append(slice1,1) // len=1, cap=1
slice1 = append(slice1,2) // len=2, cap=2
slice1 = append(slice1,3) // len=3, cap=4
slice1 = append(slice1,4) // len=4, cap=4
slice1 = append(slice1,5) // len=5, cap=8
切片切割
slice[i:j]
,从slice
下标为i
的元素开始切,切片长度为j-i
slice[i:j:k]
,从slice
下标为i
的 元素开始切,切片长度为j-i
,切片容量为k-i
slice[i:]
,从slice
下标为i
的元素开始切到最后slice[:j]
,从头切到下标为j
的元素
将切片切分为两个切片时,它们共享底层数组,因此修改一个,也会影响到另外一个切片。
删除切片元素
arrWf := [5]string{"atlas", "banshee", "chroma", "equinox", "frost"}
sliceWf := arrWf[:]
// 假设删除banshee
aliceWf = append(sliceWf[:1], sliceWf[2:]...)
补充
- 切片底层数据结构是数组,如果切片基于数组产生的,修改切片的值会影响原来的数组
字典
字典是一种无序的键值对集合,基于哈希表实现。
key的类型必须支持“==
”和”!=
“两种比较运算符,因此函数类型、map类型和切片类型不能作为map的key类型。
定义字典的语法如下:
// var 字典名 map[key数据类型]数据类型
// 如果没显示初始化,map类型变量的默认值为nil
var map1 map[string]string
map1 = make(map[string]string)
// 短句方式
map2 := make(map[string]string)
// 字面量创建
map3 := map[string]string{
"k1":"v1",
"k2":"v2",
}
// 定义一个空的字典
map4 := map[string]string{}
字典之增删改查
// 新增
map1["k1"] = "v1"
map1["k2"] = "v2"
// 删除。delete 函数是从map中删除键的唯一方法。
// 即便传给delete的键在map中不存在,delete函数的执行也不会失败,更不会抛出异常
delete(map1,"k2")
// 修改
map1["k1"] = "v11"
// 查
fmt.Println(map1["k1"])
// 是否存在key: Go
if value,ok := map["Go"]; ok {
fmt.Println(value)
} else {
fmt.Println("key: go not exists")
}
遍历字典
注意:遍历map元素的次序不是固定的。
for k,v := range map1 {
fmt.Println(k,v)
}
字典作为函数参数
map
是引用类型,共享内存,因此函数体内对字典的操作也会影响到到外部字典
package main
import "fmt"
func opMap(m map[string]string) {
m["name"] = "zhangsan"
}
func main() {
map1 := map[string]string{
"k1": "v1",
"k2": "v2",
}
fmt.Println(map1) // map[k1:v1 k2:v2]
opMap(map1)
fmt.Println(map1) // map[k1:v1 k2:v2 name:zhangsan]
if value,ok := map1["name"]; ok {
fmt.Println(value)
} else {
fmt.Println("n")
}
}
字典使用注意项
- 不要依赖map的元素遍历顺序
- map不是线程安全的,不支持并发读写
- 不要尝试获取map中元素(value)的地址(因为map自动扩容时,value的内存地址可能会变化)
其他
byte
nil
container包
Go标准库container
支持3种数据结构:
- 堆:
container/heap
- 列表:
container/list
- 环:
container/ring