Go语言之切片的底层实现

1.切片原理

切片(slice)是Go中一种比较特殊的数据结构,切片是围绕动态数组的概念进行构建的。切片和数组类似但是不同的是切片的长度不固定,且能够进行修改切片元素和增加元素。

我们先来看看切片的结构是什么样的
Go语言之切片的底层实现_第1张图片
我们从上图可以很容易的看出切片由三个部分组成 :

  1. 指向数据集合地址的指针
  2. 切片的长度
  3. 切片的容量

我们在初学切片很容易出现切片中的数据莫名其妙的丢失了的问题,我们看下面这段代码

func Demo(slice []int) {
	//追加元素
	slice = append(slice, 6, 6, 6)
	fmt.Println("函数中:", slice)
}
func main() {
	slice := []int{1, 2, 3, 4}
	//将切片作为函数参数传递(值传递)
	Demo(slice)
	fmt.Println("定义中:", slice)
}

结果:

函数中: [1 2 3 4 6 6 6]
定义中: [1 2 3 4]

在说这代码之前我先要明确这样一个问题:

fmt.Printf("%p\n",slice)
fmt.Printf("%p\n",&slice)
上面这两行代码有什么区别?
首先他们都是取地址,第一个表示的是切片中指向数据集合的地址。第二个表示切片本身的内存地址,也就是存储 指针 ,长度 ,容量这三个值的内存地址。

回到这段代码我们会发现,定义中的slice 为什么没有发生改变呢?
首先函数参数传递是值传递的,什么意思呢就相当于复制了一份放在函数中使用(当然不是很恰当)
当切片作为参数被传递时,传递的是指向数据集合的地址,长度和容量。当我们的通过append()为切片添加元素时,会导致指向数据集合的地址发生改变,而我们定义中slice中存储的数据集合地址还是原来的,所以定义中的slice并没有改变。
我们可以通过代码看看切片中指向数据集合的地址是不是真的改变了

func Demo(slice []int) {
	//追加元素
	fmt.Printf("append()增加元素前 slice中数据集合的地址:%p\n",slice)
	slice = append(slice, 6)
	fmt.Println("函数中:", slice)
	fmt.Printf("append()增加元素后 slice中数据集合的地址:%p\n",slice)

	fmt.Println("slice在内存中的大小:", unsafe.Sizeof(slice))
}
func main() {
	slice := []int{1,2,3,4}
	fmt.Printf("函数调用之前slice中数据集合的地址:%p\n",slice)
	//将切片作为函数参数传递(值传递)
	Demo(slice)
	fmt.Println("定义中:", slice)
	fmt.Printf("函数调用之后 主函数中 slice 中数据集合的地址:%p\n",slice)
	fmt.Println("slice在内存中的大小:", unsafe.Sizeof(slice))
}

结果 :

函数调用之前slice中数据集合的地址:0xc00000e3a0
append()增加元素前 slice中数据集合的地址:0xc00000e3a0
函数中: [1 2 3 4 6]
append()增加元素后 slice中数据集合的地址:0xc000010240
slice在内存中的大小: 24
定义中: [1 2 3 4]
函数调用之后 主函数中 slice 中数据集合的地址:0xc00000e3a0
slice在内存中的大小: 24

我们能很清楚的看到,在使用append之前指向数据集合的地址是没有发生改变的,但在append的之后指向的数据集合的地址是发生了变化的。然而主函数中定义的slice指向的数据集合的地址还是原来的那个地址,所以导致他的值并没有改变
我们通过下面这张图来结合理解:
Go语言之切片的底层实现_第2张图片
那么就会有小伙伴问改怎么解决这个问题呢?
方法一:
通过return返回值将改变后的slice重新赋值给原来的slice

func Demo(slice []int) []int{
	//追加元素
	slice = append(slice, 6, 6, 6)
	fmt.Println("函数中:", slice)
	return slice
}
func main() {
	slice := []int{1, 2, 3, 4}
	//将切片作为函数参数传递(值传递)
	slice = Demo(slice)
	fmt.Println("定义中:", slice)
}

方法二:
通过以指针为参数进行调用

func Demo(slice *[]int) {
	//追加元素
	*slice = append(*slice, 6, 6, 6)
	fmt.Println("函数中:", *slice)
}
func main() {
	slice := []int{1, 2, 3, 4}
	//将切片作为函数参数传递(值传递)
	Demo(&slice)
	fmt.Println("定义中:", slice)
}

2.CGO混合编程

1.GCC安装

GO给C提供了强大的支持,我可以在Go语言中直接编写C语言中的代码。
在Go语言中书写还C语言代码,还需要安装C/C构建工具链

  1. 在mac和linux系统下 安装GCC,使用命令 : yum install gcc
  2. 在windows 上需要安装MinGW工具 工具下载链接(一定要开个VPN,不然会跳转到其他网页)
2.如何使用CGO特性呢?

如果代码中出现 import “C” 语句表示启用GCO特性,紧跟在这行语句前面的注释是
一种特殊语法,里面包含的是正常的C语言代码。
下面我们来看这样一段代码

package main 
//以块注释的形式写C语言代码 

/*#include  
//C语言函数格式 
void Print() {
printf("Hello world"); 
}*/
import "C" 
func main() { 
	C.Print() 
}

这就是一个Go语言中使用C语言的代码

3.类型转换

在Go语言中访问C语言的符号时,一般是通过虚拟的“C”包访问,比如C.int对应C语言的int类型。
Go语言中数值类型和C语言数据类型基本上是相似的,以下是对应关系表

C语言类型 CGO类型 Go语言类型
char C.char byte
singed char C.schar int8
unsinged char C.uchar uint8
short C.short int16
unsigned short C.short uint16
int C.int int32
unsigned int C.uint uint32
long C.long int32
unsigned long C.ulong uint32
long long int C.longlong int64
unsigned long long int C.longlong int64
float C.float float32
double C.double float64
size_t C.size_t uint

如果Go语言在调用C语言函数时,需要传递参数或者接受返回值,都需要类型转换。

package main

//Go和C 混合编程 需要传递参数和接受返回值

/*
#include 
int add(int a,int b)
{
	return a+b;
}
*/
import "C"
import "fmt"

func main() {
	a:=10
	b:=20
	//将Go语言的类型转换为C语言类型
	c:=C.add(C.int(a),C.int(b))
	fmt.Println(c)
	fmt.Printf("%T\n",c)			//main._Ctype_int
	fmt.Println(c+C.int(b))
}

结果:

30
main._Ctype_int 
30

3.切片实现思路:

经过前面两步操作,明白了切片 (slice)的底层原理,同时也了解了CGO混合编程。可以利用C语言相关函数实现切片切片 (slice)
首先我们先构建一个切片的结构体 :

type Slice struct {
	Data unsafe.Pointer  // 指向数据的指针
	len int				 // 切片的长度
	cap int	       		 // 切片的容量
}

既然我们需要用到C语言代码当然少不了CGO:

/*
#include
*/
import "C"

1.实现创建切片
定义创建切片的三个参数:长度 ,容量 , 切片中元素

func (s *Slice) Create(l int, c int, Data ...int) {
	//对传递参数判断
	if l < 0 || c < 0 || l > c || len(Data) > l {
		return 
	}
	//创建堆内存空间 赋值给slice的Data数据
	// C语言开辟空间空间 如果成功为堆区内存地址 如果失败返回值为 NULL 相当于nil
	s.Data = C.malloc(C.ulonglong(c) * TAG) //基于容量开辟
	s.len = l
	s.cap = c

	//将s.Data转成Go语言中可以计算的指针 uintptr
	/*
		uintptr 指针类型(可以计算) , 普通指针是不能进行计算的(*int)
		unsafe.Pointer 万能指针
	*/
	p := uintptr(s.Data)
	for i := 0; i < s.len; i++ {
		*(*int)(unsafe.Pointer(p)) = 0
		p += TAG
	}
	p = uintptr(s.Data)
	for _, v := range Data {
		//数据存储
		*(*int)(unsafe.Pointer(p)) = v //不同类型的指针不能相互转化,只能通过unsafe.Pointer
		//指针偏移 存储下一个数据
		p += TAG //一个整型是8个字节
	}
}

上面的 TAG 为常量 TAG = 8

2.实现输出切片

func (s *Slice) Print() { // (s *Slice)以指针作为接受者,引用传递 //在方法内能够修改对象
	if s == nil {
		return 
	}
	//将s.Data转成可计算的指针 再通过指针找出数据的值
	p := uintptr(s.Data)
	for i := 0; i < s.len; i++ {
		//打印数据
		fmt.Print(*(*int)(unsafe.Pointer(p)), " ")
		//指针偏移
		p += TAG
	}
	//设置打印后换行
	fmt.Println()
}

3.实现切片的追加操作 即Append

func (s *Slice) Append(Data ...int) {
	if s == nil {
		return 
	}
	if len(Data) == 0 {
		return
	}
	//如果添加的数据超出容量 需要扩容
	if s.len+len(Data) > s.cap {
		//扩充容量
		// C.realloc(指针, 字节大小)
		s.Data = C.realloc(s.Data, C.ulonglong(s.cap*2*TAG))
		s.cap *= 2
	}
	//计算指针的偏移,指向插入数据的位置
	p := uintptr(s.Data)
	for i := 0; i < s.len; i++ {
		p += TAG
	}
	//添加数据
	for _, v := range Data {
		*(*int)(unsafe.Pointer(p)) = v
		p += TAG
	}

	//更新长度
	s.len += len(Data)
}

4.实现通过下标获取元素 GetData()

func (s *Slice) GetData(index int) int {
	if s == nil {
		return 
	}
	if index < 0 || index > s.len-1 {
		return 
	}
	p := uintptr(s.Data)
	//将指针指向下标的内存位置
	for i := 0; i < index; i++ {
		p += TAG
	}
	res := *(*int)(unsafe.Pointer(p))
	return res
}

5.实现Search 查找元素 Search(元素) 返回值为int下标

func (s *Slice) Search(Data int) int {
	//更具元素查找下标 返回第一次出现的位置
	if s == nil {
		return 
	}
	p := uintptr(s.Data)
	for i := 0; i < s.len; i++ {
		if *(*int)(unsafe.Pointer(p)) == Data {
			//返回下标位置
			return i
		}
		//指针偏移
		p += TAG
	}
	return -1
}

6.实现Delete 删除元素 Delete(下标)

func (s *Slice) Delete(index int) {
	if s == nil {
		return 
	}

	if index < 0 || index > s.len {
		return 
	}
	//将指针指向要删除元素的位置
	p := uintptr(s.Data)
	for i := 0; i < index; i++ {
		p += TAG
	}
	//用下一个指针为当前的指针赋值
	temp := p
	for i := index; i < s.len; i++ {
		temp += TAG
		*(*int)(unsafe.Pointer(p)) = *(*int)(unsafe.Pointer(temp))
		p += TAG
	}
	s.len -= 1
}

7.实现Insert 插入元素 Insert (下标 元素)

func (s *Slice) Insert(index int, Data int) {
	if s == nil {
		return 
	}
	if index < 0 || index > s.len  {
		return 
	}
	//如果下标和 s.len-1相同
	if index == s.len-1{
		//调用追加函数
		s.Append(Data)
	}
	//扩容(写或不写都行)
	if s.len == s.cap{
		s.Data = C.realloc(s.Data,C.ulonglong(s.cap*2*TAG))
		s.cap *=2
	}
	//获取需要插入数据位置
	p := uintptr(s.Data)
	for i := 0; i < index; i++ {
		p += TAG
	}

	//获取末尾的指针
	temp := uintptr(s.Data)
	temp += uintptr(s.len) * TAG

	for k := s.len; k > index; k-- {
		*(*int)(unsafe.Pointer(temp)) = *(*int)(unsafe.Pointer(temp - TAG))
		temp -= TAG
	}
	*(*int)(unsafe.Pointer(p)) = Data
	s.len+=1
}

8.实现切片的销毁,Destroy 销毁切片

func (s *Slice) Destroy(){
	//释放开辟的堆空间
	C.free(s.Data)
	//将slice结构体重置
	s.Data = nil
	s.len = 0
	s.cap = 0
}

以上就是我实现的一些功能函数,有些许不足大家可以自行修改增加

你可能感兴趣的:(Go语言)