go后台架构优缺点(FFI实战之对接GOCGO)
Go语言发行到现在已经超过了10个年头了,虽然已经过了那么久了,也已经很稳定了,生态也很强大了但是从编程世界来说,也依然是个儿童,前辈们也依然活力满满,所以为了使用前辈们留下来的武器和库,咱们必须要和前辈们进行必要的交互,在咱们编程世界中,称之为FFI,也就是外部函数交互接口,Go中用来做这一块的,保不齐的会需要用到CGO,本文不会涉及太多的深度的CGO方面的内容,不过是记录一些实用的技巧,以及日常实用需要掌握的常规的转换,关于CGo比较详细的教程请参考2.1. 快速入门-Go语言高级编程,今天小编就来说说关于go后台架构优缺点?下面更多详细答案一起来看看吧!
go后台架构优缺点
简要说明Go语言发行到现在已经超过了10个年头了,虽然已经过了那么久了,也已经很稳定了,生态也很强大了。但是从编程世界来说,也依然是个儿童,前辈们也依然活力满满,所以为了使用前辈们留下来的武器和库,咱们必须要和前辈们进行必要的交互,在咱们编程世界中,称之为FFI,也就是外部函数交互接口,Go中用来做这一块的,保不齐的会需要用到CGO,本文不会涉及太多的深度的CGO方面的内容,不过是记录一些实用的技巧,以及日常实用需要掌握的常规的转换,关于CGo比较详细的教程请参考2.1. 快速入门-Go语言高级编程
明确目标咱们用CGO的目的是什么,前面也说了,就是FFI,用来和其他的语言进行交互,那么交互的主要规则就是在双方都能识别出对方,主要包括:
1、调用方式,CGO使用GCC编译,默认的调用方式就是cdecl,stdcall是win下独有的调用方式,主要区别就是cdecl是由调用者去清理堆栈,而windows是由于系统本身很多时候需要在堆栈上操作,所以其设计的stdcall是被调用的函数执行完毕之后自己清理堆栈,他们两个的传参方式都是由堆栈传参,从右向左传递,其他并无不同,至于比较详细解释,可以网络上查看相关的资料
2、参数类型的话主要就是表现在入栈的参数的内容长度一致则可以。
把这两点搞明确了,那么我们就能明确的用其他的语言写动态库或者静态库去给Go调用,也可以用Go写动态库去给其他语言调用,这个就是咱们的最终极目标,就我个人来说,多数时候是用Go写动态库去给Delphi调用,因为Delphi这已经日薄西山的老头语言,当前的最新的流行的各种库都不给提供Delphi的适配,而如果自己去适配,需要花费的时间就比较多,所以,很多时候,就会用Go写一个给Delphi调用。
初摸门槛无论如何,咱们还是先从一个最简单的例子入手,第一步先写一个hello from cgo,先在GO自身调用成功。先来一个官方例子:
package main
/*
#include <stdio.h>
#include <stdlib.h>
void print(char* msg){
printf("recv from go :%s",msg);
}
*/
import "C"
import "unsafe"
func main() {
goString := C.CString("Hello from cgo \n")
C.print(goString)
C.free(unsafe.Pointer(goString))
}
这个官方例子比较简单,咱们只用了一个数据类型C.CString来将Go的数据传递到C语言中去使用;乍一看来,这个C.CString不知道是啥,黑盒子中呢,只是被告知要这样使用,作为一个有理想的猿,当然不能满足在这种黑盒状态,一定要搞清楚情况。下面,咱们不使用这个C.CString来传递,然后试试能不能成。
分析构造自定义类型我们来分析一下GO语言的string类型的结构类型,这一块,在Go的相关文档都有介绍,而且在Go自带的源码中,也有给出这个结构,那就是反射包中的StringHeader结构,构造原型如下:
type StringHeader struct {
Data uintptr
Len int
}
其中Data实际上就是字符串的真实数据地址,而Go使用UTF8存放字符串,所以,这个Data就是一个指向Utf8数据串的指针,Len就是这个数据的长度。这样来看,是不是就比较明确了呢, 现在咱们将这个结构在C语言中声明一个相似的结构体。
typedef struct _goString{
char* utf8Data;
size_t datalen;
}goString,*pgoString;
然后,咱们使用这个结构体来构造我们要显示的函数
void printData(pgoString data,int intValue){
char nData[data->datalen 1];
nData[data->datalen] = 0;
memcpy(nData,data->utf8Data,data->datalen);
printf("recv from go :%s, intValue=%d",nData,intValue);
}
最后咱们在GO中调用
func main() {
goString := C.CString("Hello from cgo \n")
C.print(goString)
C.free(unsafe.Pointer(goString))
temp := "this is from go"
C.printData(C.pgoString(unsafe.Pointer(&temp)), C.int(231))
}
以上通过构造了一个类似和Go结构体相同的类型进去了,实际上C.CString也和这个差不多,只是C.CString做的更安全一些,因为CGO是单独运行在一个goroutine中的,而go在某些GC的时候,可能会将对象转移,所以像上面搞的那种模式,如果在对象被转移了之后,那么那个temp对象的地址肯定就无效了,从而会导致达到一个非预期的效果。所以,具体情况可以具体去使用不同的方式,自己需要确保的就是地址不变的话,用上面的方法是有效的,一般只要能确保这个对象是在某个触发函数的局部对象,并且调用的CGO函数中不会启动线程去访问这个对象地址的话,一般不会有问题。其他的结构体类型按照相似的方式去处理就好,简单的类型int,有C.int等,具体可以查看相关的文档,这里不细描述了。
反向输出,交相辉映前面讲了在go语言中调用C语言的方法,实际上两者是相辅相成的,互相调用才是FFI的基石,所以下面咱们再来试试在C中调用Go的函数,高级编程中的写法,直接使用原始标记类型
//export SayHello
func SayHello(s *C.char) {
fmt.Print(C.GoString(s))
}
上面使用了C.char类型,然后用的时候,直接使用了C.GoString类型来实现,而在上面,我们已经声明了一个我们对应的go字符串类型_goString,所以,这里我们依然可以使用我们自己实现的方式来写代码,如下:
//export goPrint
func goPrint(cmsg C.pgoString) {
//这个cmsg就是一个pgoString,这里直接使用
data := reflect.StringHeader{
Data: uintptr(unsafe.Pointer(cmsg.utf8Data)),
Len: int(cmsg.datalen),
}
msg := *(*string)(unsafe.Pointer(&data))
fmt.Println("cmsg from c=", msg)
}
上面咱们构造了一个reflect.StringHeader结构,然后进行赋值,而实际上,pgoString本身就是Go的数据类型,所以咱们就没必要再转一遍,可以直接一步到位
//export goPrint
func goPrint(cmsg C.pgoString) {
//这个cmsg就是一个pgoString,这里直接使用
msg := *(*string)(unsafe.Pointer(cmsg))
fmt.Println("cmsg from c=", msg)
}
大家可以试试,然后咱们就可以在C语言中使用goPrint函数了,如下
static void printGoPrint(){
char nData[] = "这是来自于C的";
goString cstr;
cstr.utf8Data = &nData[0];
cstr.datalen = strlen(nData);
goPrint(&cstr);
SayHello("SayHello from C");
}
通过前面的讲解,基本上已经在Go中调用C和在C中调用Go都能支持了,但是细心的人可能就发现了,我在上面的printGoPrint函数前面加上了static,这是为啥呢。
这个主要原因就是,咱们在这个代码中有了GO的导出函数//export goPrint和//export SayHello,只要有这个,那么和这些导出函数写在一个GO文件中的,最终在编译连接的时候,由于obj文件会进行几次连接,然后就会发现多个相同的C函数,就会连接错误,而咱们加上static就表示这函数只在这个文件内有效,其他的文件中无法访问,所以就能祛除这个问题,如果需要在多个文件中都能访问,应该将这些函数提出去,放到单独的C语言文件中,然后在需要使用的地方,声明一下就行了,这样就不用添加static了。
下面咱们将整体代码拆分成三个文件,test.h,test.c,main.go
//test.h
#ifndef testh
#define testh
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _goString{
char* utf8Data;
size_t datalen;
}goString,*pgoString;
extern void print(char* msg);
extern void printGoPrint();
extern void printData(pgoString data,int intValue);
#endif
//test.c
#include "test.h"
void print(char* msg){
printf("recv from go :%s",msg);
}
void printData(pgoString data,int intValue){
char nData[data->datalen 1];
nData[data->datalen] = 0;
memcpy(nData,data->utf8Data,data->datalen);
printf("recv from go :%s, intValue=%d",nData,intValue);
}
void printGoPrint(){
char nData[] = "这是来自于C的";
goString cstr;
cstr.utf8Data = &nData[0];
cstr.datalen = strlen(nData);
goPrint(&cstr);
SayHello("SayHello from C");
}
//main.go
package main
/*
#include <stdlib.h>
#include "test.h"
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
goString := C.CString("Hello from cgo \n")
C.print(goString)
C.free(unsafe.Pointer(goString))
temp := "this is from go"
C.printData(C.pgoString(unsafe.Pointer(&temp)), C.int(231))
C.printGoPrint()
}
//export goPrint
func goPrint(cmsg C.pgoString) {
//这个cmsg就是一个pgoString,这里直接使用
msg := *(*string)(unsafe.Pointer(cmsg))
fmt.Println("cmsg from c=", msg)
}
//export SayHello
func SayHello(s *C.char) {
fmt.Print(C.GoString(s))
}
然后此时再去编译,注意这个时候编译,需要按照目录编译,不能只制定main.go这个文件编译,否则会连接不到c语言文件中实现的函数了。
小结和注意要点以上,就可以发现,拆分成多个文件,就可以不需要使用static修饰了,而也将代码分开更容易管理。
到此为止,基本上对于一些比较基本的CGO使用方式,也都讲解了一遍,实际上为了和其他语言进行交互,其实最主要的还是需要理解各个语言之间的数据类型是如何展现的,参数方式是如何传递的,只要理解了这些,其他的就是不变应万变了,以及在使用过程中一些魔法处置,可能需要一些经验,以及细致的去查看官方文档。最后,总结一下,可能最能碰到的一些需要注意的要点:
1、go语言文件中,有//export导出函数的,无论本文件是否需要使用CGO写C的代码,必须要import "C",否则会编译出错,猜想主要应该是为了生成CABI接口,拆分出C代码,否则这个导出是无效的
2、import "C"和C代码之间不能有空格,必须连接在一起写
3、交叉编译的时候生成动态库的时候,如果编译32位,GCC要选择32位的GCC,编译64位,要指定64位的GCC,否则不能编译成功
4、go的export函数,导出函数调用方式都是cdecl的
下一期,将讲解Go,FFI之间如何使用回调函数互相调用,敬请期待。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com