08 go语言(golang) - 数据类型:数组、切片

数据类型

Go语言提供了一组丰富的数据类型,涵盖了基本的数值类型、复合类型和特殊类型。

基本数据类型

  1. 布尔型

    • bool:表示真或假(truefalse)。
  2. 数值型

    • 整型:包括有符号和无符号整数。

      • 有符号整型:int8, int16, int32, int64
      • 无符号整型:uint8, uint16, uint32, uint64
      • 特殊整型:int, uint(根据不同平台,大小可能为32位或64位)
      • 字节相关别名:byte(等同于 uint8),用于表示单个字节;rune(等同于 int32),用于表示Unicode码点。
    • 浮点数:

      • 单精度浮点数:float32
      • 双精度浮点数:float64
    • 复数:

      • 复数有两个部分组成,实部和虚部。
        • 单精度复数:complex64
        • 双精度复数: complex128
  3. 字符串

    • 使用双引号包裹的一串字符,可以包含任何有效的UTF-8编码字符。

派生/复杂数据类型

  1. 指针
    指向某个值的内存地址。使用方式类似C语言,但不支持指针运算。

  2. 数组
    固定长度且元素具有相同类型的集合,例如 [5]int.

  3. 切片 (Slice):
    动态大小、可变长度序列,是对数组的一个抽象层,例如 []int.

  4. 映射 (Map):
    键值对集合,用于存储键到值之间映射关系,例如 map[string]int.

  5. 结构体 (Struct):
    聚合多个字段的数据结构,每个字段可以是不同的数据类型。

  6. 接口 (Interface):
    定义一组方法签名,实现多态行为。

  7. 通道 (Channel):
    用于goroutine之间通信,通过管道发送和接收消息。

特殊数据类型

  1. 空接口 (interface{}):
    可以持有任何其他具体或抽象数据对象,因为它不包含任何方法集。

  2. 函数字面量 (func) :
    可以将函数作为变量进行传递或者返回,并且支持闭包特性。

其中的基本数据类型和指针,之前的文章已经了解过了,接下来展开其余的数据类型学习

数组

数组是一种固定长度且元素类型相同的数据结构。

特性

  1. 固定长度

    • 数组的长度在声明时就确定,并且不能改变。
  2. 元素类型相同

    • 数组中的所有元素必须是相同的数据类型。
  3. 零值初始化

    • 未显式初始化的数组会被自动赋予该类型的零值(如int为0,bool为false)。

声明和初始化

  • 声明、初始化数组

    package main
    
    import (
    	"fmt"
    	"testing"
    )
    
    func Test1(t *testing.T) {
    	// 创建一个数组,默认初始化元素都为0
    	var arr1 [5]int
    
    	arr2 := [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" // 修改第一个元素为10
	fmt.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 := arr
	arr[0] = 99
	fmt.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)是一个灵活且功能强大的数据结构,用于处理动态数组。

特性

  1. 动态长度

    • 切片可以根据需要增长或缩减,不像数组那样固定。
  2. 引用类型

    • 切片是对底层数组的引用,因此修改切片会影响到底层数组。
  3. 零值为nil

    • 未初始化的切片默认值为 nil,长度和容量都是0。

创建与初始化

  1. 从数组创建
  2. 使用字面量创建
  3. 使用make函数创建
  4. 直接声明,会初始化为nil
func Test1(t *testing.T) {
	// 从数组创建
	arr := [5]int{1, 2, 3, 4, 5}
	slice1 := arr[1:4] // 包含元素2、3、4

	fmt.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) // 指定长度为5
	fmt.Println(slice3)
	fmt.Printf("slice3是否为nil:%v\n", slice3 == nil)
	slice4 := make([]int, 5, 10) // 指定长度为5,容量为10
	fmt.Println(slice4)
	fmt.Printf("slice4是否为nil:%v\n", slice4 == nil)

	// 声明,但不初始化
	var slice5 []int
	fmt.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)
     }
    

底层实现

切片结构

切片由三个部分组成:

  1. 指针

    • 指向底层数组中切片可访问部分的起始位置。
  2. 长度(len)

    • 当前切片包含的元素个数。
  3. 容量(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会自动分配更大的内存空间来容纳新数据。
  • 新内存通常是原来容量两倍,以减少频繁分配带来的开销。
  • 数据从旧内存复制到新内存后,旧内存将被垃圾回收处理。

注意事项

  1. 效率:由于直接操作的是指针和长度信息,所以在大多数情况下,使用和传递切片比传递整个数组更加高效。

  2. 共享数据风险:多个切片引用同一底层数据时,要小心并发写操作可能导致的数据竞争问题。

数组和切片的区别

  1. 长度
    • 数组的长度是固定的,一旦声明,就不能改变。
    • 切片的长度是动态的,可以在运行时改变。
  2. 声明方式
    • 数组的声明需要指定长度:var array [5]int
    • 切片的声明不需要指定长度,可以直接使用字面量或make函数:slice := []int{1, 2, 3}slice := make([]int, 5)
  3. 内存分配
    • 数组的内存分配是连续的。
    • 切片的内存分配不一定是连续的,它们实际上指向一个底层数组。
  4. 容量
    • 数组没有容量的概念。
    • 切片有容量的概念,表示切片可以扩展到的最大长度而不会引起内存重新分配。
  5. 传递参数
    • 数组通过值传递,函数内部对数组的修改不会影响原始数组。
    • 切片通过引用传递(实际上是复制了切片结构体,仍然引用相同的数据)。
  6. 操作
    • 数组的操作较少,例如不能直接在数组上进行append操作。
    • 切片提供了丰富的内置操作,如appendcopy等。
  7. 性能
    • 数组在某些情况下可能更高效,因为其固定大小和连续性。
    • 切片由于动态特性可能引入一些开销,但通常灵活性更高。
  8. 用途
    • 数组通常用于长度已知且不变的场景。
    • 切片通常用于长度可能变化或未知的场景,它们提供了更高的灵活性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿-瑞瑞

打赏不准超过你的一半工资哦~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值