1. Go项目的构建
一个Go工程中主要包含以下三个目录:
src:源代码文件,编写程序代码xxx.go,执行命令go build xxx.go会生成*.exe文件;执行go run xxx.go可以直接运行文件
pkg:包文件,执行go install name会在此目录下生成*.a文件,用于import
bin:相关bin文件,执行go install xxx会在此母名生成*.exe文件,可以直接运行
go的基本命令如下:
image.png
2. 变量和常量
Go的程序是保存在多个.go文件中,文件的第一行就是package XXX声明,用来说明该文件属于哪个包(package),package声明下来就是import声明,再下来是类型,变量,常量,函数的声明。Go语言的变量声明格式为
var 变量名 变量类型 [ = 表达式或值]
变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。举个例子:
var name string var age int //批量声明,一个var带多个不同类型的变量声明 var ( a string b int c bool d float32 )
类型推断
我们可以将变量的类型省略,编译器会根据等号右边的值来推导变量的类型完成初始化
在函数内部,可以使用更简略的 := 方式(省略var和type)声明并初始化变量。但是有限制:
不能用在函数外
:= 操作符的左边至少有一个变量是尚未声明的
常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。例如:
const ( n1 = 100 n2 n3 )
3 内置数据类型
类型 | 长度(字节) | 默认值 | 说明 |
---|---|---|---|
bool | 1 | false | |
byte | 1 | 0 | uint8 |
rune | 4 | 0 | 代表一个UTF8字符, int32 |
int, uint | 4或8 | 0 | 32 或 64 位 |
int8, uint8 | 1 | 0 | -128 ~ 127, 0 ~ 255,byte是uint8 的别名 |
int16, uint16 | 2 | 0 | -32768 ~ 32767, 0 ~ 65535 |
int32, uint32 | 4 | 0 | -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名 |
int64, uint64 | 8 | 0 | |
float32 | 4 | 0.0 | |
float64 | 8 | 0.0 | |
complex64 | 8 | 复数,实部和虚部为32位,创建方式:- 使用函数complex创建- a := 6 + 7i | |
complex128 | 16 | 复数,实部和虚部为64位 | |
uintptr | 4或8 | 以存储指针的 uint32 或 uint64 整数 | |
array | 值类型 | ||
struct | 值类型 | ||
string | "" | UTF-8 字符串 | |
slice | nil | 引用类型 | |
map | nil | 引用类型 | |
channel | nil | 引用类型 | |
interface | nil | 接口 | |
function | nil | 函数 | |
nil | 空指针 |
3.1 格式化打印
fmt包支持如下几种打印方式
fmt.Println:打印一行内容,类似std::cout,难以设置格式
fmt.Print:打印内容,并不换行
fmt.Printf:格式化打印,与C语言printf同理
fmt.Sprintf:格式化打印,不同之处使返回string类型,不是打印到屏幕
格式化打印支持的格式符:
image.png
fmt.Printf("type of a is %T, size of a is %d", a, unsafe.Sizeof(a)) // a 的类型和大小
3.2 类型转换
Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。强制类型转换的基本语法如下:
T(表达式)
4 基本语句
4.1 if语句
//可省略条件表达式括号。 //持初始化语句,可定义代码块局部变量。 //代码块左 括号必须在条件表达式尾部。 if 布尔表达式 { //。。。 } else { //else不能单独一行,golang的自动分号插入机制导致的 //。。。 } //另一种格式,在条件判断前执行一条指令 if statement; condition { }
4.2 switch语句
switch var1 { case val1: ... case val2,val3,val4://通过用逗号分隔,可以在一个 case 中包含多个表达式 ... default: ... } //可以看到每个case不需要break来分割 //switch 语句还可以被用于type-switch 来判断某个interface 变量中实际存储的变量类型 switch x.(type){ case type: statement(s) case type: statement(s) /* 你可以定义任意个数的case */ default: /* 可选 */ statement(s) }
注意:
case可以是字符、字符串,表达式,不一定是常量
每个case语句块自动结束退出switch,不需要使用break
如果需要接着执行下一个case的内容,需要使用**fallthrough **
4.3 for循环
三种形式
for init; condition; post { } for condition { } for { } //init: 一般为赋值表达式,给控制变量赋初值; //condition: 关系表达式或逻辑表达式,循环控制条件; //post: 一般为赋值表达式,给控制变量增量或减量。
range循环语句:range类似迭代器操作,返回 (索引, 值) 或 (键, 值)
for key, value := range oldMap { newMap[key] = value }
5 函数
5.1 函数定义
在 Go 语言中,函数声明通用语法如下:
func functionname(parametername type) returntype { // 函数体(具体实现的功能) } //如果有连续若干个函数参数,它们的类型一致,那么无须一一罗列,只需在最后一个参数后添加该类型。
Go 语言支持一个函数可以有多个返回值(也用括号包含),并且可以给返回值命名,这样可以不在return里添加需要返回的变量:
func rectProps(length, width float64)(float64, float64) {//两个括号,一个函数参数,一个返回列表 var area = length * width var perimeter = (length + width) * 2 return area, perimeter//返回多返回值 } //返回值命名 func rectProps(length, width float64)(area, perimeter float64) { area = length * width perimeter = (length + width) * 2 return // 不需要明确指定返回值,默认返回 area, perimeter 的值 }
_ 在 Go 中被用作空白符,可以用作表示任何类型的任何值,通常用在接收函数多返回值,过滤掉不需要的返回值:
area, _ := rectProps(10.8, 5.6) // 返回值周长被丢弃
5.2 可变参数
如果函数最后一个参数被记作 ...T ,这时函数可以接受任意个 T 类型参数作为最后一个参数。可变参数函数的工作原理是把可变参数转换为一个新的切片。
func find(num int, nums ...int) { fmt.Printf("type of nums is %T ", nums)//nums相当于整型slice found := false for i, v := range nums { if v == num { fmt.Println(num, "found at index", i, "in", nums) found = true } } if !found { fmt.Println(num, "not found in ", nums) } fmt.Printf(" ") } func main() { find(89, 89, 90, 95)//传入数多个参数 nums := []int{89, 90, 95} find(89, nums...)//传入一个slice }
5.3 返回error信息
我们可以使用errors包或fmt包来生成error类型的对象,用于返回函数的内部错误:
//实现自定义函数同时返回err和其他返回值 package main import ( "errors" "fmt" ) func f1() (int, error) { //设置多返回值 err := errors.New("I am the error") //使用errors包生成error return 1, err } func f2() (int, error) { //使用fmt包生成error err := fmt.Errorf("I am a error created by fmt") return 2, err } func main() { a, err := f1() if err != nil { fmt.Println(err.Error()) } fmt.Println(a) b, err := f2() if err != nil { fmt.Println(err.Error()) } fmt.Println(b) }5.4 指针传址参数
对于需要在函数内部修改的参数,需要使用传址参数,GO中指针和C语言使一样的,基本符号也是***和&**。
//指针传址参数,和函数返回指针
package main
import "fmt"
func fun1(value *int) *float64 {
*value += 10
myFloat := 98.5
//虽然myFloat是局部变量,但GO并不会释放它,因为所有权被转移到函数外了
return &myFloat
}
func main() {
number := 10
ret := fun1(&number)
fmt.Println(number, " ", *ret)
}6 数组
一个数组的表示形式为[n]T。n表示数组中元素的数量,T代表每个元素的类型。使用示例如下:
var a [3]int //所有元素有默认值0 a := [3]int{12, 78, 50}//简要声明,赋值 a := [3]int{12} //只给第一个元素赋值 var b = [...]int{1, 2, 3} // 定义长度为3的int型数组, 元素为 1, 2, 3 fmt.Println(a) //数组可以直接打印出来 fmt.Println(len(a)) //打印数组长度 //打印内容 for i := range a { fmt.Printf("a[%d]: %d ", i, a[i]) } for i, v := range b { fmt.Printf("b[%d]: %d ", i, v) }
Go中的数组是值类型而不是引用类型。一个数组变量即表示整个数组,它并不是隐式的指向第一个元素的指针(比如C语言的数组)。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。
a := [...]string{"USA", "China", "India", "Germany", "France"} b := a // a copy of a is assigned to b b[0] = "Singapore" //修改b,a不会改变,这不是C++的数组基地址指针
数组的长度是数组类型的一个部分,不同长度或不同类型的数据组成的数组都是不同的类型,因此在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值),因此推荐使用切片。
7 slice切片
切片是由数组建立的一种方便、灵活且功能强大的包装(Wrapper),切片本身不拥有任何数据。它们只是对现有数组的引用。可以理解为简化版的动态数组,slice才是C++的数组指针类似的存在,修改slice就是修改原数组。
7.1 创建slice
带有T类型元素的切片由[]T表示,切片的长度是切片中的元素数,切片的容量是从创建切片的索引开始算起到数组末尾的元素数。创建slice如下:
var ( a []int // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片 b = []int{} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合 c = []int{1, 2, 3} // 有3个元素的切片, len和cap都为3 d = c[:2] // 有2个元素的切片, len为2, cap为3 e = c[0:2:cap(c)] // 有2个元素的切片, len为2, cap为3 f = c[:0] // 有0个元素的切片, len为0, cap为3 g []int = a[1:4] // creates a slice from a[1] to a[3] g = make([]int, 3) // 有3个元素的切片, len和cap都为3 i = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3 j = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3 )
7.2 修改slice
切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。当多个切片共用相同的底层数组时,每个切片所做的更改将反映在数组中。
func main() { numa := [3]int{78, 79 ,80} nums1 := numa[:] // creates a slice which contains all elements of the array nums2 := numa[:] fmt.Println("array before change 1", numa) nums1[0] = 100 fmt.Println("array after modification to slice nums1", numa) nums2[1] = 101 fmt.Println("array after modification to slice nums2", numa) } //输出 //array before change 1 [78 79 80] //array after modification to slice nums1 [100 79 80] //array after modification to slice nums2 [100 101 80] append函数可以追加新元素,原数组长度会变化(不是不能改变长度吗??)。其原理是当新的元素被添加到slice时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用,新切片的容量是旧切片的两倍。
//在切片尾部追加元素 var a []int a = append(a, 1) // 追加1个元素 a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式 a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
删除切片元素:
//删除尾部元素 a = []int{1, 2, 3} a = a[:len(a)-1] // 删除尾部1个元素 a = a[:len(a)-N] // 删除尾部N个元素 //删除开头元素,徐娅移动指针位置 a = a[1:] // 删除开头1个元素 a = a[N:] // 删除开头N个元素
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成:
a = []int{1, 2, 3, ...} a = append(a[:i], a[i+1:]...) // 删除中间1个元素 a = append(a[:i], a[i+N:]...) // 删除中间N个元素 a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素 a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
7.3 slice的内存优化
假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中。只要切片在内存中,数组就不能被垃圾回收。可以使用copy函数获取一个原始slice的的副本,这样原始slice和原数组都可以被自动释放了。
func countries() []string { countries := []string{"USA", "Singapore", "Germany", "India", "Australia"} neededCountries := countries[:len(countries)-2] countriesCpy := make([]string, len(neededCountries)) copy(countriesCpy, neededCountries) //复制slice return countriesCpy } 另外,更严重的是:假设切片里存放的是指针对象,那么下面删除末尾的元素后,被删除的元素依然被切片底层数组引用,从而导致不能及时被自动垃圾回收器回收。保险的方式是先将需要自动内存回收的元素设置为nil,保证自动回收器可以发现需要回收的对象,然后再进行切片的删除操作:
var a []*int{ ... }
a[len(a)-1] = nil // GC回收最后一个元素内存
a = a[:len(a)-1] // 从切片删除最后一个元素8 map
通过向 make 函数传入键和值的类型,可以创建 map,map默认是空指针nil,必须使用make进行初始化。make(map[type of key]type of value) 是创建 map 的语法:
//先make,再添加key-value func main() { personSalary := make(map[string]int) personSalary["steve"] = 12000 personSalary["jamie"] = 15000 personSalary["mike"] = 9000 fmt.Println("personSalary map contents:", personSalary) } //创建时添加key-value func main() { personSalary := map[string]int { "steve": 12000, "jamie": 15000, } personSalary["mike"] = 9000 fmt.Println("personSalary map contents:", personSalary) }
如果获取一个不存在的元素,map 会返回该元素类型的零值。既然无法通过返回值判断key是否存在,我们应该这么做:
value, ok := map[key] //如果 ok 是 true,表示 key 存在,key对应的值就是value ,反之表示 key 不存在。
删除 map 中 key 的语法是 delete(map, key)。这个函数没有返回值。和 slices 类似,map 也是引用类型。当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。
9 字符串和rune
Go 语言中的字符串是一个字节切片或rune切片,可以使用index获取每个字符,并且使用 UTF-8 进行编码。字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。
func mutate(s []rune) string { //接收一个rune切片,修改后返回string s[0] = 'a' return string(s) } func main() { h := "hello" fmt.Println(mutate([]rune(h))) } ****注意:在 UTF-8 编码中,一个代码点可能会占用超过一个字节的空间,如果超过一个字节还使用普通string类型的话,就会出现乱码。对于这种情况,应该使用rune类型的slice。**rune 是 Go 语言的内建类型,它也是 int32 的别称。在 Go 语言中,rune 表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。举例如下:
func printChars(s string) { runes := []rune(s) //先将string转换为rune for i := 0; i < len(runes); i++ { fmt.Printf("%c ", runes[i]) } } func main() { name := "Hello World" printChars(name) fmt.Printf(" ") name = "Señor" printChars(name) }
10 结构体
下面示例为如何创建结构体并初始化:
type Employee struct { //命名结构体 firstName, lastName string age, salary int } func main() { //creating structure using field names emp1 := Employee{ firstName: "Sam", age: 25, salary: 500, lastName: "Anderson", } //creating structure without using field names emp2 := Employee{"Thomas", "Paul", 29, 800} fmt.Println("Employee 1", emp1) fmt.Println("Employee 2", emp2) //创建匿名结构体,并直接生成一个结构体对象 emp3 := struct { firstName, lastName string age, salary int }{ firstName: "Andreah", lastName: "Nikola", age: 31, salary: 5000, } fmt.Println("Employee 3", emp3) }
点号操作符 . 用于访问结构体的字段。
10.1 匿名字段
当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段(Anonymous Field)。虽然匿名字段没有名称,但其实匿名字段的名称就默认为它的类型。以下代码创建一个 Person 结构体,它含有两个匿名字段 string 和 int。
type Person struct { string int }
10.2 导出结构体和字段
如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型(Exported Type)。同样,如果结构体里的字段首字母大写,它也能被其他包访问到。
10.3 结构体比较
结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。
如果结构体包含不可比较的字段,则结构体变量也不可比较
要使用CGO特性,需要安装C/C++构建工具链,在macOS和Linux下是要安装GCC,在windows下是需要安装MinGW工具。同时需要保证环境变量CGO_ENABLED被设置为1,这表示CGO是被启用的状态。
11、cgo启用语句
11.1、import "C"
通过import "C"语句启用CGO特性,紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的C语言代码。当确保CGO启用的情况下,还可以在当前目录中包含C/C++对应的头文件。
示例如下:
//第一个cgo的例子,使用C/C++的函数 package main // // 引用的C头文件需要在注释中声明,紧接着注释需要有import "C",且这一行和注释之间不能有空格 // /* #include//自定义头文件 #include #include void myprint(char* s);//声明头文件中的函数 */ import "C" import ( "fmt" "unsafe" ) func main() { //使用C.CString创建的字符串需要手动释放。 cs := C.CString("Hello World ") C.myprint(cs) C.free(unsafe.Pointer(cs)) fmt.Println("call C.sleep for 3s") C.sleep(3) return }
11.2、cgo
在import "C"语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。#cgo语句主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。
CFLAGS:对应C语言编译参数(以.c后缀名)
CPPFLAGS:对应C/C++ 代码编译参数(.c,.cc,.cpp,.cxx)
CXXFLAGS:对应纯C++编译参数(.cc,.cpp,*.cxx)
LDFLAGS:对应静态库和动态库链接选项,必须使用绝对路径(cgo 中的 ${SRCDIR} 为当前目录的绝对路径)
使用示例如下:
//使用C库,编译时GCC会自动找到libnumber.a或libnumber.so进行链接 package main /*#cgo CFLAGS: -I./c_library #cgo LDFLAGS: -L${SRCDIR}/c_library -l number #include "number.h" */ import "C" import "fmt" func main() { fmt.Println(C.number_add_mod(10, 5, 12)) }
#cgo指令还支持条件选择,当满足某个操作系统或某个CPU架构类型时后面的编译或链接选项生效。比如下面是分别针对windows和非windows下平台的编译和链接选项:
// #cgo windows CFLAGS: -DX86=1 // #cgo !windows LDFLAGS: -lm
12、C与Go之间类型映射
12.1、基本类型转换
Go语言中数值类型和C语言数据类型基本上是相似的,以下是它们的对应关系:
C语言类型 | CGO类型 | Go语言类型 |
---|---|---|
char | C.char | byte |
singed char | C.schar | int8 |
unsigned char | C.uchar | uint8 |
short | C.short | int16 |
unsigned short | C.ushort | 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.ulonglong | uint64 |
float | C.float | float32 |
double | C.double | float64 |
size_t | C.size_t | uint |
12.2、结构体、联合、枚举类型
C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。在Go语言中,我们可以通过C.struct_xxx来访问C语言中定义的struct xxx结构体类型。
/* struct A { int type; // type 是 Go 语言的关键字,此项被屏蔽 float _type; // 将屏蔽CGO对 type 成员的访问 }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a._type) // _type 对应 _type }
对于联合类型,我们可以通过C.union_xxx来访问C语言中定义的union xxx类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组。对于枚举类型,我们可以通过C.enum_xxx来访问C语言中定义的enum xxx结构体类型。
/* enum C { ONE, TWO, }; */ import "C" import "fmt" func main() { var c C.enum_C = C.TWO fmt.Println(c) fmt.Println(C.ONE) fmt.Println(C.TWO) }
12.3、字符串和数组转换
CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:
// Go string to C string, C.free is needed). func C.CString(string) *C.char // Go []byte slice to C array, C.free is needed). func C.CBytes([]byte) unsafe.Pointer // C string to Go string func C.GoString(*C.char) string // C data with explicit length to Go string func C.GoStringN(*C.char, C.int) string // C data with explicit length to Go []byte func C.GoBytes(unsafe.Pointer, C.int) []byte
13、C函数如何返回errno?
CGO也针对
/* #includestatic int div(int a, int b) { if(b == 0) { errno = EINVAL; return 0; } return a/b; } */ import "C" import "fmt" func main() { v0, err0 := C.div(2, 1) fmt.Println(v0, err0) v1, err1 := C.div(1, 0) fmt.Println(v1, err1) }
14、一个完整的封装C函数的例子
该例子的重点是,在封装C函数的模块里要提供外部类型和函数指针类型给其他go模块使用,不能直接在其他模块使用封装模块中的C类型,因为不同模块cgo编译后C类型并不是统一类型,无法进行类型转换。封装C标准库qsort函数:
//封装C标准库函数qsort,给其他go文件或模块使用 package qsort /* #include//qsort的比较函数指针 typedef int (*qsort_cmp_func_t)(const void* a, const void* b); */ import "C" import "unsafe" //将虚拟C包中的类型通过Go语言类型代替,在内部调用C函数时重新转型为C函数需要的类型 //因此外部用户将不再依赖qsort包内的虚拟C包,消除用户对CGO代码的直接依赖 type CompareFunc C.qsort_cmp_func_t //封装qsort的go Sort函数 func Sort(base unsafe.Pointer, num int, size int, cmp CompareFunc) { C.qsort(base, C.size_t(num), C.size_t(size), C.qsort_cmp_func_t(cmp)) }
使用上面qsort库的其他库文件:
package main //extern int go_qsort_compare(void* a, void* b); import "C" import ( "fmt" "qsort" "unsafe" ) //export go_qsort_compare func go_qsort_compare(a, b unsafe.Pointer) C.int { pa, pb := (*C.int)(a), (*C.int)(b) return C.int(*pa - *pb) } func main() { values := []int32{42, 9, 101, 95, 27, 25} qsort.Sort(unsafe.Pointer(&values[0]), len(values), int(unsafe.Sizeof(values[0])), //转换一下函数指针,使用qsort提供的类型,不直接使用C空间函数指针 qsort.CompareFunc(C.go_qsort_compare), ) fmt.Println(values) }
15、中间生成文件
在一个Go源文件中,如果出现了import "C"指令则表示将调用cgo命令生成对应的中间文件。下图是cgo生成的中间文件的简单示意图:
16、Cgo内存访问
如果在CGO处理的跨语言函数调用时涉及到了指针的传递,则可能会出现Go语言和C语言共享某一段内存的场景。我们知道C语言的内存在分配之后就是稳定的,但是Go语言因为函数栈的动态伸缩可能导致栈中内存地址的移动(这是Go和C内存模型的最大差异)。如果C语言持有的是移动之前的Go指针,那么以旧指针访问Go对象时会导致程序崩溃。
16.1 Go访问C内存
C语言空间的内存是稳定的,只要不是被人为提前释放,那么在Go语言空间可以放心大胆地使用。比如下面示例,我们可以在Go中调用C的malloc和free创建、使用和释放内存,不用考虑内存地址移动的问题。
package main /* #includevoid* makeslice(size_t memsize) { return malloc(memsize); } */ import "C" import "unsafe" func makeByteSlize(n int) []byte { p := C.makeslice(C.size_t(n)) return ((*[1 << 31]byte)(p))[0n] } func freeByteSlice(p []byte) { C.free(unsafe.Pointer(&p[0])) } func main() { s := makeByteSlize(1<<32+1) //创建一个超大的内存用于切片 s[len(s)-1] = 255 print(s[len(s)-1]) freeByteSlice(s) }
16.2 Go内存传入C语言函数
C/C++很多库都是需要通过指针直接处理传入的内存数据的,因此cgo中也有很多需要将Go内存传入C语言函数的应用场景。Go的内存是不稳定的,goroutinue栈因为空间不足的原因可能会发生扩展,导致了原来的Go语言内存被移动到了新的位置,如果这时候还按照原来地址访问内存,就会导致内存越界。为了简化并高效处理向C语言传入Go语言内存的问题,cgo针对该场景定义了专门的规则:在CGO调用的C语言函数返回前,cgo保证传入的Go语言内存在此期间不会发生移动,C语言函数可以大胆地使用Go语言的内存!
package main /* #includevoid printString(const char* s, int n) { int i; for(i = 0; i < n; i++) { putchar(s[i]); } putchar(' '); } */ import "C" import "unsafe" import "reflect" func printString(s string) { p := (*reflect.StringHeader)(unsafe.Pointer(&s)) //直接传入go的内存空间给C函数 C.printString((*C.char)(unsafe.Pointer(p.Data)), C.int(len(s))) } func main() { s := "hello" printString(s) }
编辑:黄飞
评论
查看更多