嵌入式Linux应用开发之基本语言语法常识

嵌入式技术

1344人已加入

描述

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也针对标准库的errno宏做的特殊支持:在CGO调用C函数时如果有两个返回值,那么第二个返回值将对应errno错误状态。对于void类型函数,这个特性依然有效。

/*
#include 
static 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

/*
#include 

void* 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

/*
#include

void 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)
}

编辑:黄飞

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分