数据类型
Go语言提供了一组丰富的数据类型,涵盖了基本的数值类型、复合类型和特殊类型。
基本数据类型
-
布尔型:
bool
:表示真或假(true
或false
)。
-
数值型:
-
整型:包括有符号和无符号整数。
- 有符号整型:
int8
,int16
,int32
,int64
- 无符号整型:
uint8
,uint16
,uint32
,uint64
- 特殊整型:
int
,uint
(根据不同平台,大小可能为32位或64位) - 字节相关别名:
byte
(等同于 uint8),用于表示单个字节;rune
(等同于 int32),用于表示Unicode码点。
- 有符号整型:
-
浮点数:
- 单精度浮点数:
float32
- 双精度浮点数:
float64
- 单精度浮点数:
-
复数:
- 复数有两个部分组成,实部和虚部。
- 单精度复数:complex64
- 双精度复数: complex128
- 复数有两个部分组成,实部和虚部。
-
-
字符串:
- 使用双引号包裹的一串字符,可以包含任何有效的UTF-8编码字符。
派生/复杂数据类型
-
指针:
指向某个值的内存地址。使用方式类似C语言,但不支持指针运算。 -
数组:
固定长度且元素具有相同类型的集合,例如[5]int
. -
切片 (Slice):
动态大小、可变长度序列,是对数组的一个抽象层,例如 []int. -
映射 (Map):
键值对集合,用于存储键到值之间映射关系,例如 map[string]int. -
结构体 (Struct):
聚合多个字段的数据结构,每个字段可以是不同的数据类型。 -
接口 (Interface):
定义一组方法签名,实现多态行为。 -
通道 (Channel):
用于goroutine之间通信,通过管道发送和接收消息。
特殊数据类型
-
空接口 (
interface{}
):
可以持有任何其他具体或抽象数据对象,因为它不包含任何方法集。 -
函数字面量 (
func
) :
可以将函数作为变量进行传递或者返回,并且支持闭包特性。
其中的基本数据类型和指针,之前的文章已经了解过了,接下来展开其余的数据类型学习
数组
数组是一种固定长度且元素类型相同的数据结构。
特性
-
固定长度:
- 数组的长度在声明时就确定,并且不能改变。
-
元素类型相同:
- 数组中的所有元素必须是相同的数据类型。
-
零值初始化:
- 未显式初始化的数组会被自动赋予该类型的零值(如
int
为0,bool
为false)。
- 未显式初始化的数组会被自动赋予该类型的零值(如
声明和初始化
-
声明、初始化数组:
package mainimport ("fmt""testing" )func Test1(t *testing.T) {// 创建一个数组,默认初始化元素都为0var arr1 [5]intarr2 := [5]int{1, 2, 3, 4, 5} // 使用字面量进行初始化arr3 := [...]int{1, 2, 3} // 使用省略号让编译器推断长度//arr := []int{1, 2, 3, 4} // 注意这是切片,不是数组!fmt.Println(arr1)fmt.Println(arr2)fmt.Println(arr3) }
-
多维数组:
可以创建多维数组,例如二维、三维等。
func Test2(*testing.T) {var matrix1 [3][4]int // 三行四列的二维整型数组matrix2 := [2][2]int{{1, 2}, {3, 4}} // 初始化二维矩阵//matrix3 := [2][...]int{{1, 2}, {3, 4}} // 使用省略号 ... 来推断长度仅适用于一维数组。fmt.Println(matrix1)fmt.Println(matrix2) }
访问和修改元素
func Test3(t *testing.T) {arr := [...]string{"1", "2", "a", "b"}fmt.Println(arr)arr[0] = "3" // 修改第一个元素为10fmt.Println(arr)/*修改数组长度,arr类型已确定为4个长度,无法修改为5的长度cannot use [5]string{…} (value of type [5]string) as [4]string value in assignment*///arr = [5]string{"x", "x", "x", "x", "x"}arr = [...]string{"x1", "x2", "x3", "x4"}fmt.Println(arr)value := arr[1] // 获取第二个元素值fmt.Println(value)
}
遍历
func Test4(t *testing.T) {arr := [...]int{1, 2, 3, 4}for i := range arr {fmt.Println(arr[i])}fmt.Println("------分割线------")for _, value := range arr {fmt.Println(value)}
}
注意事项
- 数组是值类型:将一个数组赋给另一个时,会复制整个数据。
func Test5(t *testing.T) {arr := [...]int{1, 2, 3, 4}fmt.Printf("原始数组:%v \n", arr)fmt.Println("------分割线------\n ")// 创建arr的新副本,注意不是引用,修改原始数组,不会修改copy的数组arrCopy := arrarr[0] = 99fmt.Printf("原始数组:%v \n", arr)fmt.Printf("copy的数组:%v \n", arrCopy)fmt.Println("------分割线------\n ")// 函数内对参数修改不会影响原始数据。modifyArray(arr)fmt.Printf("原始数组:%v \n", arr)
}func modifyArray(arr [4]int) {arr = [...]int{99, 99, 99, 99}
}
-
长度是类型的一部分:不同长度的两个同类数据不能互相赋值。
-
数组的元素可以被更改(长度和类型都不可以修改)。
-
数组的内存地址和第一个元素的内存地址相同
func Test6(t *testing.T) {nums := [3]int32{11, 22, 33}/*在Go语言中,数组的内存地址和第一个元素的内存地址相同,这是因为数组是一个连续的内存块,且其起始位置就是第一个元素的位置。数组结构:数组是一块连续分配的内存区域,其中每个元素按顺序排列。nums 的地址实际上是整个数组在内存中的起始地址。nums[0] 是数组中的第一个元素,因此它位于这块连续内存区域的开头。*/fmt.Printf("数组的内存地址:%p \n", &nums)fmt.Printf("数组第1个元素的内存地址:%p \n", &nums[0])fmt.Printf("数组第2个元素的内存地址:%p \n", &nums[1])fmt.Printf("数组第3个元素的内存地址:%p \n", &nums[2]) }
输出:
数组的内存地址:0xc000090190 数组第1个元素的内存地址:0xc000090190 数组第2个元素的内存地址:0xc000090194 数组第3个元素的内存地址:0xc000090198
切片
切片(slice)是一个灵活且功能强大的数据结构,用于处理动态数组。
特性
-
动态长度:
- 切片可以根据需要增长或缩减,不像数组那样固定。
-
引用类型:
- 切片是对底层数组的引用,因此修改切片会影响到底层数组。
-
零值为nil:
- 未初始化的切片默认值为
nil
,长度和容量都是0。
- 未初始化的切片默认值为
创建与初始化
- 从数组创建
- 使用字面量创建
- 使用make函数创建
- 直接声明,会初始化为nil
func Test1(t *testing.T) {// 从数组创建arr := [5]int{1, 2, 3, 4, 5}slice1 := arr[1:4] // 包含元素2、3、4fmt.Println(slice1)fmt.Printf("slice1是否为nil:%v\n", slice1 == nil)// 使用字面量创建slice2 := []int{1, 2, 3, 4, 5}fmt.Println(slice2)fmt.Printf("slice2是否为nil:%v\n", slice2 == nil)// 使用make函数创建slice3 := make([]int, 5) // 指定长度为5fmt.Println(slice3)fmt.Printf("slice3是否为nil:%v\n", slice3 == nil)slice4 := make([]int, 5, 10) // 指定长度为5,容量为10fmt.Println(slice4)fmt.Printf("slice4是否为nil:%v\n", slice4 == nil)// 声明,但不初始化var slice5 []intfmt.Println(slice5)fmt.Printf("slice5是否为nil:%v\n", slice5 == nil)
}
打印
[2 3 4]
slice1是否为nil:false
[1 2 3 4 5]
slice2是否为nil:false
[0 0 0 0 0]
slice3是否为nil:false
[0 0 0 0 0]
slice4是否为nil:false
[]
slice5是否为nil:true
为什么打印的不是<nil>
在Go语言中,打印一个变量时,不同的类型有不同的默认格式化输出。
对于复合类型,如切片、映射、通道和接口,即使它们是nil
,也会打印出表示该类型的空值的字面量。例如:
- 切片:
[]
- 映射:
map[]
- 通道:
chan []
- 接口:
<nil>
(接口是唯一一个即使为nil
也会打印出<nil>
的复合类型)
这种打印行为是为了提供更清晰的信息,帮助开发者理解变量的状态。
操作
-
获取长度、容量,追加元素
func Test3(t *testing.T) {slice := make([]string, 0, 10)// 追加元素,不改变原本的切片,生成新的切片sliceNew := append(slice, "a")// 长度len() ,容量cap()fmt.Printf("slice:%v, 长度:%d,容量:%d\n", slice, len(slice), cap(slice))fmt.Printf("sliceNew:%v, 长度:%d,容量:%d\n", sliceNew, len(sliceNew), cap(sliceNew))// 如果追加操作超过了原有容量,会自动分配新的底层数组并复制旧数据。sliceNew2 := append(slice, "a", "b", "c", "d", "e", "f", "g", "h", "i", "k", "l")fmt.Printf("sliceNew2:%v, 长度:%d,容量:%d\n", sliceNew2, len(sliceNew2), cap(sliceNew2)) }
-
遍历
for i,v:=range slice{fmt.Println(i,v)}
底层实现
切片结构
切片由三个部分组成:
-
指针:
- 指向底层数组中切片可访问部分的起始位置。
-
长度(len):
- 当前切片包含的元素个数。
-
容量(cap):
- 从切片起始位置到底层数组末尾之间元素总数。
底层数组
-
切片本质上是一个对底层数组的一种视图。
-
多个切片可以共享同一个底层数组,并且修改其中一个会影响其他共享相同数据段的切片。
func Test4(t *testing.T) {arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}slice1 := arr[1:4]slice2 := arr[2:5]fmt.Println("修改前:")fmt.Println(arr)fmt.Println(slice1)fmt.Println(slice2)arr[3] = 99 // 修改数组fmt.Println("修改后:")fmt.Println(arr)fmt.Println(slice1)fmt.Println(slice2)slice1[1] = 999 // 修改切片fmt.Println("修改后:")fmt.Println(arr)fmt.Println(slice1)fmt.Println(slice2) }
动态增长
- 当使用
append()
向切片添加元素时,如果超过了当前容量,Go会自动分配更大的内存空间来容纳新数据。 - 新内存通常是原来容量两倍,以减少频繁分配带来的开销。
- 数据从旧内存复制到新内存后,旧内存将被垃圾回收处理。
注意事项
-
效率:由于直接操作的是指针和长度信息,所以在大多数情况下,使用和传递切片比传递整个数组更加高效。
-
共享数据风险:多个切片引用同一底层数据时,要小心并发写操作可能导致的数据竞争问题。
数组和切片的区别
- 长度:
- 数组的长度是固定的,一旦声明,就不能改变。
- 切片的长度是动态的,可以在运行时改变。
- 声明方式:
- 数组的声明需要指定长度:
var array [5]int
。 - 切片的声明不需要指定长度,可以直接使用字面量或
make
函数:slice := []int{1, 2, 3}
或slice := make([]int, 5)
。
- 数组的声明需要指定长度:
- 内存分配:
- 数组的内存分配是连续的。
- 切片的内存分配不一定是连续的,它们实际上指向一个底层数组。
- 容量:
- 数组没有容量的概念。
- 切片有容量的概念,表示切片可以扩展到的最大长度而不会引起内存重新分配。
- 传递参数:
- 数组通过值传递,函数内部对数组的修改不会影响原始数组。
- 切片通过引用传递(实际上是复制了切片结构体,仍然引用相同的数据)。
- 操作:
- 数组的操作较少,例如不能直接在数组上进行
append
操作。 - 切片提供了丰富的内置操作,如
append
、copy
等。
- 数组的操作较少,例如不能直接在数组上进行
- 性能:
- 数组在某些情况下可能更高效,因为其固定大小和连续性。
- 切片由于动态特性可能引入一些开销,但通常灵活性更高。
- 用途:
- 数组通常用于长度已知且不变的场景。
- 切片通常用于长度可能变化或未知的场景,它们提供了更高的灵活性。