点击上方“嵌入式应用研究院”,选择“置顶/星标公众号


来源 | golang编程笔记

整理&排版| 嵌入式应用研究院

1. Go项目的构建

一个Go工程中主要包含以下三个目录:

go build xxx.gogo run xxx.gogo install namego install xxx

go的基本命令如下:

image.png

2. 变量和常量

Go的程序是保存在多个.go文件中,文件的第一行就是package XXX声明,用来说明该文件属于哪个包(package),package声明下来就是import声明,再下来是类型,变量,常量,函数的声明。Go语言的变量声明格式为:

var变量名变量类型[=表达式或值]
关键字var
varnamestring
varageint
//批量声明,一个var带多个不同类型的变量声明
var(
astring
bint
cbool
dfloat32
)

类型推断

  • 我们可以将变量的类型省略,编译器会根据等号右边的值来推导变量的类型完成初始化
  • 在函数内部,可以使用更简略的 := 方式(省略var和type)声明并初始化变量。但是有限制:
    • 不能用在函数外
    • :=操作符的左边至少有一个变量是尚未声明的
constconst
const(
n1=100
n2
n3
)

3 内置数据类型

类型长度(字节)默认值说明
bool1false
byte10uint8
rune40代表一个UTF8字符, int32
int, uint4或8032 或 64 位
int8, uint810-128 ~ 127, 0 ~ 255,byte是uint8 的别名
int16, uint1620-32768 ~ 32767, 0 ~ 65535
int32, uint3240-21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名
int64, uint6480
float3240.0
float6480.0
complex648
复数,实部和虚部为32位,创建方式:- 使用函数complex创建- a := 6 + 7i
complex12816
复数,实部和虚部为64位
uintptr4或8
以存储指针的 uint32 或 uint64 整数
array

值类型
struct

值类型
string
""UTF-8 字符串
slice
nil引用类型
map
nil引用类型
channel
nil引用类型
interface
nil接口
function
nil函数
nil

空指针
3.1 格式化打印

fmt包支持如下几种打印方式

std::coutprintf

格式化打印支持的格式符:

image.png
fmt.Printf("typeofais%T,sizeofais%d",a,unsafe.Sizeof(a))//a的类型和大小
3.2 类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。强制类型转换的基本语法如下:

T(表达式)

4 基本语句

4.1 if语句
//可省略条件表达式括号。
//持初始化语句,可定义代码块局部变量。
//代码块左括号必须在条件表达式尾部。
if布尔表达式{
//。。。
}else{//else不能单独一行,golang的自动分号插入机制导致的
//。。。
}
//另一种格式,在条件判断前执行一条指令
ifstatement;condition{
}
4.2 switch语句
switchvar1{
caseval1:
...
caseval2,val3,val4://通过用逗号分隔,可以在一个case中包含多个表达式
...
default:
...
}
//可以看到每个case不需要break来分割
//switch语句还可以被用于type-switch来判断某个interface变量中实际存储的变量类型
switchx.(type){
casetype:
statement(s)
casetype:
statement(s)
/*你可以定义任意个数的case*/
default:/*可选*/
statement(s)
}

注意:

fallthrough
4.3 for循环

三种形式

forinit;condition;post{}
forcondition{}
for{}
//init:一般为赋值表达式,给控制变量赋初值;
//condition:关系表达式或逻辑表达式,循环控制条件;
//post:一般为赋值表达式,给控制变量增量或减量。

range循环语句:range类似迭代器操作,返回 (索引, 值) 或 (键, 值)

forkey,value:=rangeoldMap{
newMap[key]=value
}

5 函数

5.1 函数定义

在 Go 语言中,函数声明通用语法如下:

funcfunctionname(parameternametype)returntype{
//函数体(具体实现的功能)
}
//如果有连续若干个函数参数,它们的类型一致,那么无须一一罗列,只需在最后一个参数后添加该类型。

Go 语言支持一个函数可以有多个返回值(也用括号包含),并且可以给返回值命名,这样可以不在return里添加需要返回的变量:

funcrectProps(length,widthfloat64)(float64,float64){//两个括号,一个函数参数,一个返回列表
vararea=length*width
varperimeter=(length+width)*2
returnarea,perimeter//返回多返回值
}
//返回值命名
funcrectProps(length,widthfloat64)(area,perimeterfloat64){
area=length*width
perimeter=(length+width)*2
return//不需要明确指定返回值,默认返回area,perimeter的值
}

_在 Go 中被用作空白符,可以用作表示任何类型的任何值,通常用在接收函数多返回值,过滤掉不需要的返回值:

area,_:=rectProps(10.8,5.6)//返回值周长被丢弃

5.2 可变参数
...TT
funcfind(numint,nums...int){
fmt.Printf("typeofnumsis%T\n",nums)//nums相当于整型slice
found:=false
fori,v:=rangenums{
ifv==num{
fmt.Println(num,"foundatindex",i,"in",nums)
found=true
}
}
if!found{
fmt.Println(num,"notfoundin",nums)
}
fmt.Printf("\n")
}
funcmain(){
find(89,89,90,95)//传入数多个参数
nums:=[]int{89,90,95}
find(89,nums...)//传入一个slice
}

5.3 返回error信息

我们可以使用errors包或fmt包来生成error类型的对象,用于返回函数的内部错误:

//实现自定义函数同时返回err和其他返回值
packagemain

import(
"errors"
"fmt"
)

funcf1()(int,error){//设置多返回值
err:=errors.New("Iamtheerror")//使用errors包生成error
return1,err
}

funcf2()(int,error){
//使用fmt包生成error
err:=fmt.Errorf("Iamaerrorcreatedbyfmt")
return2,err
}

funcmain(){
a,err:=f1()
iferr!=nil{
fmt.Println(err.Error())
}
fmt.Println(a)
b,err:=f2()
iferr!=nil{
fmt.Println(err.Error())
}
fmt.Println(b)
}

5.4 指针传址参数

对于需要在函数内部修改的参数,需要使用传址参数,GO中指针和C语言使一样的,基本符号也是***和&**。

//指针传址参数,和函数返回指针
packagemain

import"fmt"

funcfun1(value*int)*float64{
*value+=10
myFloat:=98.5
//虽然myFloat是局部变量,但GO并不会释放它,因为所有权被转移到函数外了
return&myFloat
}

funcmain(){
number:=10
ret:=fun1(&number)
fmt.Println(number,"",*ret)
}

6 数组

[n]T
vara[3]int//所有元素有默认值0
a:=[3]int{12,78,50}//简要声明,赋值
a:=[3]int{12}//只给第一个元素赋值
varb=[...]int{1,2,3}//定义长度为3的int型数组,元素为1,2,3

fmt.Println(a)//数组可以直接打印出来
fmt.Println(len(a))//打印数组长度
//打印内容
fori:=rangea{
fmt.Printf("a[%d]:%d\n",i,a[i])
}
fori,v:=rangeb{
fmt.Printf("b[%d]:%d\n",i,v)
}

Go中的数组是值类型而不是引用类型。一个数组变量即表示整个数组,它并不是隐式的指向第一个元素的指针(比如C语言的数组)。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。

a:=[...]string{"USA","China","India","Germany","France"}
b:=a//acopyofaisassignedtob
b[0]="Singapore"//修改b,a不会改变,这不是C++的数组基地址指针

数组的长度是数组类型的一个部分,不同长度或不同类型的数据组成的数组都是不同的类型,因此在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值),因此推荐使用切片。

7 slice切片

切片是由数组建立的一种方便、灵活且功能强大的包装(Wrapper),切片本身不拥有任何数据。它们只是对现有数组的引用。可以理解为简化版的动态数组,slice才是C++的数组指针类似的存在,修改slice就是修改原数组

7.1 创建slice
[]T
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]//createsaslicefroma[1]toa[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

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。当多个切片共用相同的底层数组时,每个切片所做的更改将反映在数组中。

funcmain(){
numa:=[3]int{78,79,80}
nums1:=numa[:]//createsaslicewhichcontainsallelementsofthearray
nums2:=numa[:]
fmt.Println("arraybeforechange1",numa)
nums1[0]=100
fmt.Println("arrayaftermodificationtoslicenums1",numa)
nums2[1]=101
fmt.Println("arrayaftermodificationtoslicenums2",numa)
}
//输出
//arraybeforechange1[787980]
//arrayaftermodificationtoslicenums1[1007980]
//arrayaftermodificationtoslicenums2[10010180]

append
//在切片尾部追加元素
vara[]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个元素

appendcopy
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
funccountries()[]string{
countries:=[]string{"USA","Singapore","Germany","India","Australia"}
neededCountries:=countries[:len(countries)-2]
countriesCpy:=make([]string,len(neededCountries))
copy(countriesCpy,neededCountries)//复制slice
returncountriesCpy
}

nil
vara[]*int{...}
a[len(a)-1]=nil//GC回收最后一个元素内存
a=a[:len(a)-1]//从切片删除最后一个元素

8 map

makemake(map[type of key]type of value)
//先make,再添加key-value
funcmain(){
personSalary:=make(map[string]int)
personSalary["steve"]=12000
personSalary["jamie"]=15000
personSalary["mike"]=9000
fmt.Println("personSalarymapcontents:",personSalary)
}

//创建时添加key-value
funcmain(){
personSalary:=map[string]int{
"steve":12000,
"jamie":15000,
}
personSalary["mike"]=9000
fmt.Println("personSalarymapcontents:",personSalary)
}

如果获取一个不存在的元素,map 会返回该元素类型的零值。既然无法通过返回值判断key是否存在,我们应该这么做:

value,ok:=map[key]
//如果 ok 是 true,表示 key 存在,key对应的值就是value ,反之表示 key 不存在。

delete(map, key)

9 字符串和rune

Go 语言中的字符串是一个字节切片或rune切片,可以使用index获取每个字符,并且使用 UTF-8 进行编码。字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。

funcmutate(s[]rune)string{//接收一个rune切片,修改后返回string
s[0]='a'
returnstring(s)
}
funcmain(){
h:="hello"
fmt.Println(mutate([]rune(h)))
}

rune
funcprintChars(sstring){
runes:=[]rune(s)//先将string转换为rune
fori:=0;i<len(runes);i++{
fmt.Printf("%c",runes[i])
}
}
funcmain(){
name:="HelloWorld"
printChars(name)
fmt.Printf("\n\n")

name="Señor"
printChars(name)
}

10 结构体

下面示例为如何创建结构体并初始化:

typeEmployeestruct{//命名结构体
firstName,lastNamestring
age,salaryint
}

funcmain(){

//creatingstructureusingfieldnames
emp1:=Employee{
firstName:"Sam",
age:25,
salary:500,
lastName:"Anderson",
}

//creatingstructurewithoutusingfieldnames
emp2:=Employee{"Thomas","Paul",29,800}

fmt.Println("Employee1",emp1)
fmt.Println("Employee2",emp2)
//创建匿名结构体,并直接生成一个结构体对象
emp3:=struct{
firstName,lastNamestring
age,salaryint
}{
firstName:"Andreah",
lastName:"Nikola",
age:31,
salary:5000,
}

fmt.Println("Employee3",emp3)
}

.
10.1 匿名字段
Personstringint
typePersonstruct{
string
int
}

10.2 导出结构体和字段

如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型(Exported Type)。同样,如果结构体里的字段首字母大写,它也能被其他包访问到。

10.3 结构体比较
  • 结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。
  • 如果结构体包含不可比较的字段,则结构体变量也不可比较

CGO_ENABLED

11、cgo启用语句

11.1、import "C"
import "C"

示例如下:

//第一个cgo的例子,使用C/C++的函数
packagemain

//
//引用的C头文件需要在注释中声明,紧接着注释需要有import"C",且这一行和注释之间不能有空格
//

/*
#include<myprint.h>//自定义头文件
#include<stdlib.h>
#include<unistd.h>
voidmyprint(char*s);//声明头文件中的函数
*/
import"C"

import(
"fmt"
"unsafe"
)

funcmain(){
//使用C.CString创建的字符串需要手动释放。
cs:=C.CString("HelloWorld\n")
C.myprint(cs)
C.free(unsafe.Pointer(cs))
fmt.Println("callC.sleepfor3s")
C.sleep(3)
return
}
11.2、cgo
import "C"#cgo#cgo
.c

使用示例如下:

//使用C库,编译时GCC会自动找到libnumber.a或libnumber.so进行链接
packagemain

/*#cgoCFLAGS:-I./c_library
#cgoLDFLAGS:-L${SRCDIR}/c_library-lnumber
#include"number.h"
*/
import"C"
import"fmt"

funcmain(){
fmt.Println(C.number_add_mod(10,5,12))
}
#cgo
//#cgowindowsCFLAGS:-DX86=1
//#cgo!windowsLDFLAGS:-lm

12、C与Go之间类型映射

12.1、基本类型转换

Go语言中数值类型和C语言数据类型基本上是相似的,以下是它们的对应关系:

C语言类型CGO类型Go语言类型
charC.charbyte
singed charC.scharint8
unsigned charC.ucharuint8
shortC.shortint16
unsigned shortC.ushortuint16
intC.intint32
unsigned intC.uintuint32
longC.longint32
unsigned longC.ulonguint32
long long intC.longlongint64
unsigned long long intC.ulonglonguint64
floatC.floatfloat32
doubleC.doublefloat64
size_tC.size_tuint
12.2、结构体、联合、枚举类型
C.struct_xxxstruct xxx
/*
structA{
inttype;//type是Go语言的关键字,此项被屏蔽
float_type;//将屏蔽CGO对type成员的访问
};
*/
import"C"
import"fmt"

funcmain(){
varaC.struct_A
fmt.Println(a._type)//_type对应_type
}
C.union_xxxunion xxxC.enum_xxxenum xxx
/*
enumC{
ONE,
TWO,
};
*/
import"C"
import"fmt"

funcmain(){
varcC.enum_C=C.TWO
fmt.Println(c)
fmt.Println(C.ONE)
fmt.Println(C.TWO)
}
12.3、字符串和数组转换

CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

//GostringtoCstring,C.freeisneeded).
funcC.CString(string)*C.char

//Go[]byteslicetoCarray,C.freeisneeded).
funcC.CBytes([]byte)unsafe.Pointer

//CstringtoGostring
funcC.GoString(*C.char)string

//CdatawithexplicitlengthtoGostring
funcC.GoStringN(*C.char,C.int)string

//CdatawithexplicitlengthtoGo[]byte
funcC.GoBytes(unsafe.Pointer,C.int)[]byte

13、C函数如何返回errno?

errnoerrno
/*
#include<errno.h>
staticintdiv(inta,intb){
if(b==0){
errno=EINVAL;
return0;
}
returna/b;
}
*/
import"C"
import"fmt"
funcmain(){
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文件或模块使用
packageqsort

/*
#include<stdlib.h>
//qsort的比较函数指针
typedefint(*qsort_cmp_func_t)(constvoid*a,constvoid*b);
*/
import"C"
import"unsafe"

//将虚拟C包中的类型通过Go语言类型代替,在内部调用C函数时重新转型为C函数需要的类型
//因此外部用户将不再依赖qsort包内的虚拟C包,消除用户对CGO代码的直接依赖
typeCompareFuncC.qsort_cmp_func_t

//封装qsort的goSort函数
funcSort(baseunsafe.Pointer,numint,sizeint,cmpCompareFunc){
C.qsort(base,C.size_t(num),C.size_t(size),C.qsort_cmp_func_t(cmp))
}

使用上面qsort库的其他库文件:

packagemain

//externintgo_qsort_compare(void*a,void*b);
import"C"

import(
"fmt"
"qsort"
"unsafe"
)

//exportgo_qsort_compare
funcgo_qsort_compare(a,bunsafe.Pointer)C.int{
pa,pb:=(*C.int)(a),(*C.int)(b)
returnC.int(*pa-*pb)
}

funcmain(){
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、中间生成文件

import "C"

16、Cgo内存访问

如果在CGO处理的跨语言函数调用时涉及到了指针的传递,则可能会出现Go语言和C语言共享某一段内存的场景。我们知道C语言的内存在分配之后就是稳定的,但是Go语言因为函数栈的动态伸缩可能导致栈中内存地址的移动(这是Go和C内存模型的最大差异)。如果C语言持有的是移动之前的Go指针,那么以旧指针访问Go对象时会导致程序崩溃。

16.1 Go访问C内存

C语言空间的内存是稳定的,只要不是被人为提前释放,那么在Go语言空间可以放心大胆地使用。比如下面示例,我们可以在Go中调用C的malloc和free创建、使用和释放内存,不用考虑内存地址移动的问题。

packagemain

/*
#include<stdlib.h>

void*makeslice(size_tmemsize){
returnmalloc(memsize);
}
*/
import"C"
import"unsafe"

funcmakeByteSlize(nint)[]byte{
p:=C.makeslice(C.size_t(n))
return((*[1<<31]byte)(p))[0:n:n]
}

funcfreeByteSlice(p[]byte){
C.free(unsafe.Pointer(&p[0]))
}

funcmain(){
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语言的内存

packagemain

/*
#include<stdio.h>

voidprintString(constchar*s,intn){
inti;
for(i=0;i<n;i++){
putchar(s[i]);
}
putchar('\n');
}
*/
import"C"
import"unsafe"
import"reflect"

funcprintString(sstring){
p:=(*reflect.StringHeader)(unsafe.Pointer(&s))
//直接传入go的内存空间给C函数
C.printString((*C.char)(unsafe.Pointer(p.Data)),C.int(len(s)))
}

funcmain(){
s:="hello"
printString(s)
}