💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] ## 概述 最近在巩固cgo的基础知识,在网上看到一篇Go和C之间 字符串数组、切片类型转换的文章,让我想到我之前写的一篇在[go中遍历C结构体数组](https://blog.csdn.net/qq_39503880/article/details/112478609)的文章,让我有新的方法来解决之前的问题,把C的数组转化为Go的切片,对于文章的方法我直接”拿来主义“。 ## 数组、字符串和切片 我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。 在Go语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。 Go语言和C语言的数组、字符串和切片之间的相互转换可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换。 ## 以克隆的方式进行类型转换 CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换: ```go func C.CString(string) *C.char //go字符串转化为char* func C.CBytes([]byte) unsafe.Pointer // go 切片转化为指针 func C.GoString(*C.char) string //C字符串 转化为 go字符串 func C.GoStringN(*C.char, C.int) string func C.GoBytes(unsafe.Pointer, C.int) []byte ``` **其中`C.CString`针对输入的Go字符串,克隆一个C语言格式的字符串;返回的字符串由C语言的`malloc`函数分配,不使用时需要通过C语言的`free`函数释放。**`C.CBytes`函数的功能和`C.CString`类似,用于从输入的Go语言字节切片克隆一个C语言版本的字节数组,同样返回的数组需要在合适的时候释放。**`C.GoString`用于将从NULL结尾的C语言字符串克隆一个Go语言字符串。**`C.GoStringN`是另一个字符数组克隆函数。`C.GoBytes`用于从C语言数组,克隆一个Go语言字节切片。 >克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。 上面粗体部分表示,利用`C.CString`把go字符串转化为C字符串,内存由C语言的`malloc`分配,不使用时需要`free`释放内存,否则会出现内存泄漏。 ## 通过直接访问C语言的内存来进行数据转换 在`reflect`包中有字符串和切片的定义:在`reflect`包中有字符串和切片的定义: ```go type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int } ``` 如果不希望单独分配内存,可以在Go语言中直接访问C语言的内存空间: ```go /* #include <string.h> char arr[10]; char *s = "Hello"; */ import "C" import ( "reflect" "unsafe" ) func main() { // 通过 reflect.SliceHeader 转换 var arr0 []byte var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0)) arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0])) arr0Hdr.Len = 10 arr0Hdr.Cap = 10 // 通过切片语法转换 arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10] var s0 string var s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0)) s0Hdr.Data = uintptr(unsafe.Pointer(C.s)) s0Hdr.Len = int(C.strlen(C.s)) sLen := int(C.strlen(C.s)) s1 := string((*[31]byte)(unsafe.Pointer(C.s))[:sLen:sLen]) } ``` 因为**Go语言的字符串是只读**的,用户需要自己保证Go字符串在使用期间,底层对应的C字符串内容不会发生变化、内存不会被提前释放掉。 在CGO中,会为字符串和切片生成和上面结构对应的C语言版本的结构体: ```go typedef struct { const char *p; GoInt n; } GoString; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; ``` ## 在Go语言中直接访问C语言的内存空间的例子 ```go package main /* #include <stdio.h> #include <stdlib.h> typedef struct { char *name; int age; }person; //一个长度为10的person结构体数组 person pon[10]; void NewPersonArray() { int n = 10; //初始化name for(int i = 0; i<n;i++){ pon[i].name = (char*)malloc(sizeof(char)*10); pon[i].age = i; sprintf(pon[i].name, "name:%d", i); } } //释放内存 void freePersonArray() { for (int i = 0; i < 10; i ++){ free(pon[i].name); } } */ import "C" import ( "fmt" "unsafe" ) func main() { C.NewPersonArray() //通过切片语法转换 arr1 := (*[20]C.person)(unsafe.Pointer(&C.pon[0]))[:10:10] for _, v := range arr1 { fmt.Printf("p.name: %s, p.age: %d\n", C.GoString(v.name), int(v.age) ) } C.freePersonArray() } ``` 通过切片语法转换把C的结构体数组转换为go的数据 ```go arr1 := (*[20]C.person)(unsafe.Pointer(&C.pon[0]))[:10:10] ``` >注意,如果`C.pon`是一个数组指针该方法就不适用,在go中无法使用索引`C.pon[0]`的 方法来访问C数组指针中的数据。比如有一个长度为10的数组指针 *C.pon,则在go中无法通过C.pon[0] 索引的方式来访问数据。如果有一个长度为10的数组`[10]C.pon`, 则可以使用C.pon[0] 索引的方式来访问数据,不过在go中还需要转化为切片才能访问 输出: ```shell p.name: name:0, p.age: 0 p.name: name:1, p.age: 1 p.name: name:2, p.age: 2 p.name: name:3, p.age: 3 p.name: name:4, p.age: 4 p.name: name:5, p.age: 5 p.name: name:6, p.age: 6 p.name: name:7, p.age: 7 p.name: name:8, p.age: 8 p.name: name:9, p.age: 9 ``` ## 给一个小彩蛋 在go中一个长度为10的char, 通过访问数组首位元素的地址来输出整个数组 ``` package main /* #include <stdio.h> void NewChar(char *s, int n) { sprintf(s, "I'm char"); } */ import "C" import ( "fmt" "unsafe" ) func main() { var c [10]C.char C.NewChar(&c[0], C.int(10)) //通过数组首元素地址输出整个数组 arr := C.GoString((*C.char)(unsafe.Pointer(&c[0]))) fmt.Printf("111111: %s\n",arr) } ``` 参考文章:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-03-cgo-types.html