CGO
Étant donné que Go nécessite un GC (Garbage Collector), pour certains scénarios nécessitant des performances plus élevées, Go peut ne pas être le plus adapté. Le C, en tant que langage de programmation système traditionnel, offre d'excellentes performances, et CGO permet de relier les deux, permettant des appels mutuels. Go peut appeler du code C pour déléguer les tâches sensibles aux performances, tandis que Go gère la logique de haut niveau. CGO prend également en charge les appels de Go depuis le C, bien que ce scénario soit moins courant et généralement déconseillé.
TIP
Le code présenté dans cet article a été testé sous Windows 10, en utilisant gitbash comme terminal. Il est recommandé aux utilisateurs Windows d'installer MinGW au préalable.
Concernant CGO, il existe une introduction simple sur le site officiel : C? Go? Cgo! - The Go Programming Language. Pour des informations plus détaillées, vous pouvez consulter cmd/cgo/doc.go dans la bibliothèque standard, ou directement la documentation cgo command - cmd/cgo - Go Packages. Les deux contenus sont identiques.
Appel de code
Voici un exemple :
package main
//#include <stdio.h>
import "C"
func main() {
C.puts(C.CString("hello, cgo!"))
}Pour utiliser la fonctionnalité CGO, il suffit d'ajouter l'instruction d'importation import "C". Notez que C doit être en majuscule et que ce nom d'importation ne peut pas être redéfini. Il faut également s'assurer que la variable d'environnement CGO_ENABLED est définie à 1. Par défaut, cette variable d'environnement est activée.
$ go env | grep CGO
$ go env -w CGO_ENABLED=1De plus, il est nécessaire de disposer d'une chaîne d'outils de construction C/C++ locale, c'est-à-dire gcc. Sur la plateforme Windows, il s'agit de MinGW, afin de garantir que le programme puisse être compilé correctement. Exécutez la commande suivante pour compiler. Notez que l'activation de CGO augmente le temps de compilation par rapport à du Go pur.
$ go build -o ./ main.go
$ ./main.exe
hello, cgo!Un autre point important : une fois CGO activé, la compilation croisée n'est plus prise en charge.
Intégration de code C dans Go
CGO permet d'écrire directement du code C dans les fichiers source Go et de l'appeler. Voici un exemple avec une fonction nommée printSum, appelée depuis la fonction main en Go.
package main
/*
#include <stdio.h>
void printSum(int a, int b) {
printf("c:%d+%d=%d",a,b,a+b);
}
*/
import "C"
func main() {
C.printSum(C.int(1), C.int(2))
}Sortie :
c:1+2=3Cette approche convient aux scénarios simples. Si le code C est très volumineux, le mélanger avec du code Go réduit considérablement la lisibilité, ce qui n'est pas recommandé.
Gestion des erreurs
En Go, la gestion des erreurs se fait via les valeurs de retour. Cependant, le C ne permet pas les retours multiples. Pour cela, on peut utiliser errno en C, qui indique qu'une erreur s'est produite pendant l'appel de fonction. CGO a adapté cette approche, permettant de gérer les erreurs en C de manière similaire à Go. Pour utiliser errno, il faut d'abord inclure errno.h. Voici un exemple :
package main
/*
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
int32_t sum_positive(int32_t a, int32_t b) {
if (a <= 0 || b <= 0) {
errno = EINVAL;
return 0;
}
return a + b;
}
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
sum, err := C.sum_positive(C.int32_t(0), C.int32_t(1))
if err != nil {
fmt.Println(reflect.TypeOf(err))
fmt.Println(err)
return
}
fmt.Println(sum)
}Sortie :
syscall.Errno
The device does not recognize the command.Le type d'erreur est syscall.Errno. errno.h définit de nombreux autres codes d'erreur à explorer.
Importation de fichiers C dans Go
L'importation de fichiers C permet de résoudre les problèmes mentionnés ci-dessus. Commencez par créer un fichier d'en-tête sum.h avec le contenu suivant :
int sum(int a, int b);Ensuite, créez sum.c pour implémenter la fonction :
#include "sum.h"
int sum(int a, int b) {
return a + b;
}Puis importez le fichier d'en-tête dans main.go :
package main
//#include "sum.h"
import "C"
import "fmt"
func main() {
res := C.sum(C.int(1), C.int(2))
fmt.Printf("cgo sum: %d\n", res)
}Pour compiler, il faut spécifier le dossier actuel, sinon le fichier C ne sera pas trouvé :
$ go build -o sum.exe . && ./sum.exe
cgo sum: 3Dans le code, res est une variable Go, C.sum est une fonction C. Sa valeur de retour est un int C, pas un int Go. L'appel réussit grâce à la conversion de type effectuée par CGO.
Appel de Go depuis C
L'appel de Go depuis C fait référence à l'appel de Go par C dans le contexte de CGO, et non à un programme C natif appelant Go. La chaîne d'appel est la suivante : go-cgo-c->cgo->go. Go appelle C pour exploiter l'écosystème et les performances de C. Il existe peu de besoins pour qu'un programme C natif appelle Go, et si c'est le cas, il est recommandé d'utiliser la communication réseau.
CGO permet d'exporter des fonctions Go pour être appelées par C. Pour exporter une fonction Go, ajoutez le commentaire //export func_name au-dessus de la signature de la fonction. Les paramètres et la valeur de retour doivent être des types pris en charge par CGO. Exemple :
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}Modifiez le fichier sum.c comme suit :
#include <stdint.h>
#include <stdio.h>
#include "sum.h"
#include "_cgo_export.h"
extern int32_t sum(int32_t a, int32_t b);
void do_sum() {
int32_t a = 10;
int32_t b = 10;
int32_t c = sum(a, b);
printf("%d", c);
}Modifiez également le fichier d'en-tête sum.h :
void do_sum();Puis exportez la fonction en Go :
package main
/*
#include <stdio.h>
#include <stdint.h>
#include "sum.h"
*/
import "C"
func main() {
C.do_sum()
}
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}La fonction sum utilisée dans C est en réalité fournie par Go. Résultat :
20Le point clé est l'importation de _cgo_export.h dans le fichier sum.c, qui contient toutes les informations sur les types exportés par Go. Sans cette importation, les fonctions Go exportées ne peuvent pas être utilisées. Un autre point important : _cgo_export.h ne peut pas être importé dans les fichiers Go, car sa génération nécessite que tous les fichiers source Go puissent être compilés. Ainsi, l'écriture suivante est incorrecte :
package main
/*
#include <stdint.h>
#include <stdio.h>
#include "_cgo_export.h"
void do_sum() {
int32_t a = 10;
int32_t b = 10;
int32_t c = sum(a, b);
printf("%d", c);
}
*/
import "C"
func main() {
C.do_sum()
}
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}Le compilateur indiquera que le fichier d'en-tête est introuvable :
fatal error: _cgo_export.h: No such file or directory
#include "_cgo_export.h"
^~~~~~~~~~~~~~~
compilation terminated.Si une fonction Go a plusieurs valeurs de retour, l'appel depuis C retournera une structure.
Par ailleurs, il est possible de passer un pointeur Go à une fonction C via ses paramètres. Pendant l'appel, CGO tente de garantir la sécurité mémoire. Cependant, la valeur de retour d'une fonction Go exportée ne doit pas contenir de pointeurs, car CGO ne peut pas déterminer si elle est référencée ni fixer la mémoire. Si la mémoire retournée est référencée, puis libérée par le GC Go ou déplacée, le pointeur deviendra invalide. Exemple :
//export newCharPtr
func newCharPtr() *C.char {
return new(C.char)
}Cette écriture ne peut pas être compilée par défaut. Pour désactiver cette vérification, configurez :
GODEBUG=cgocheck=0Il existe deux niveaux de vérification, 1 et 2. Plus le niveau est élevé, plus la surcharge d'exécution est importante. Consultez cgo command - passing_pointer pour plus de détails.
Conversion de types
CGO établit une correspondance entre les types C et Go pour faciliter les appels à l'exécution. Pour les types C, après avoir importé import "C" en Go, on peut généralement y accéder directement via :
C.typenamePar exemple :
C.int(1)
C.char('a')Cependant, les types C peuvent être composés de plusieurs mots-clés, comme :
unsigned charDans ce cas, l'accès direct n'est pas possible. On peut utiliser le mot-clé typedef en C pour créer un alias de type, équivalent aux alias de type en Go :
typedef unsigned char byte;Ainsi, on peut accéder au type unsigned char via C.byte. Exemple :
package main
/*
#include <stdio.h>
typedef unsigned char byte;
void printByte(byte b) {
printf("%c\n",b);
}
*/
import "C"
func main() {
C.printByte(C.byte('a'))
C.printByte(C.byte('b'))
C.printByte(C.byte('c'))
}Sortie :
a
b
cDans la plupart des cas, CGO a déjà défini des alias pour les types courants (types de base, etc.). Vous pouvez également définir vos propres alias sans conflit.
char
Le char en C correspond au type int8 en Go, et unsigned char correspond à uint8, alias byte en Go.
package main
/*
#include <stdio.h>
#include<complex.h>
char ch;
char get() {
return ch;
}
void set(char c) {
ch = c;
}
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
C.set(C.char('c'))
res := C.get()
fmt.Printf("type: %s, val: %v", reflect.TypeOf(res), res)
}Sortie :
type: main._Ctype_char, val: 99Si le paramètre de set est remplacé par C.char(math.MaxInt8 + 1), la compilation échouera avec l'erreur suivante :
cannot convert math.MaxInt8 + 1 (untyped int constant 128) to type _Ctype_charChaînes de caractères
CGO fournit des pseudo-fonctions pour transférer des chaînes et des tranches d'octets entre C et Go. Ces fonctions n'existent pas réellement et vous ne trouverez pas leurs définitions. Tout comme import "C", le paquet C n'existe pas non plus. Elles sont simplement là pour faciliter l'utilisation par les développeurs et sont converties en d'autres opérations lors de la compilation.
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char
// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if 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) []byteUne chaîne en Go est essentiellement une structure contenant une référence à un tableau sous-jacent. Pour la transmettre à une fonction C, il faut utiliser C.CString() pour créer une "chaîne" en C avec malloc, allouer de la mémoire, puis retourner un pointeur C. Comme le C n'a pas de type chaîne, on utilise généralement char* pour représenter une chaîne, c'est-à-dire un pointeur vers un tableau de caractères. N'oubliez pas de libérer la mémoire avec free après utilisation.
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
cstring := C.CString("this is a go string")
C.printfGoString(cstring)
C.free(unsafe.Pointer(cstring))
}Cela peut aussi être un tableau de type char, les deux sont essentiellement identiques : un pointeur vers l'élément de tête.
void printfGoString(char s[]) {
puts(s);
}Il est également possible de transmettre des tranches d'octets. Comme C.CBytes() retourne un unsafe.Pointer, il faut le convertir en *C.char avant de le transmettre à la fonction C.
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
cbytes := C.CBytes([]byte("this is a go string"))
C.printfGoString((*C.char)(cbytes))
C.free(unsafe.Pointer(cbytes))
}Les exemples ci-dessus produisent tous la même sortie :
this is a go stringCes méthodes de transmission de chaînes impliquent une copie mémoire. Après transmission, une copie existe à la fois dans la mémoire C et Go, ce qui est plus sûr. Cela dit, il est possible de transmettre directement un pointeur à une fonction C et de modifier une chaîne Go depuis C. Voici un exemple :
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
ptr := unsafe.Pointer(unsafe.SliceData([]byte("this is a go string")))
C.printfGoString((*C.char)(ptr))
}Sortie :
this is a go stringCet exemple utilise unsafe.SliceData pour obtenir directement le pointeur du tableau sous-jacent de la chaîne, le convertit en pointeur C, puis le transmet à la fonction C. La mémoire de cette chaîne est gérée par Go, donc aucun free n'est nécessaire. L'avantage est qu'aucune copie n'est nécessaire pendant la transmission, mais cela comporte certains risques. L'exemple suivant montre comment modifier une chaîne Go depuis C :
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s, int len) {
puts(s);
s[8] = 'c';
puts(s);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
var buf []byte
buf = []byte("this is a go string")
ptr := unsafe.Pointer(unsafe.SliceData(buf))
C.printfGoString((*C.char)(ptr), C.int(len(buf)))
fmt.Println(string(buf))
}Sortie :
this is a go string
this is c go string
this is c go stringEntiers
Le tableau suivant montre la correspondance entre les entiers Go et C. Pour plus d'informations sur la correspondance des types entiers, consultez cmd/cgo/gcc.go dans la bibliothèque standard.
| go | c | cgo |
|---|---|---|
| int8 | signed char | C.schar |
| uint8 | unsigned char | C.uchar |
| int16 | short | C.short |
| uint16 | unsigned short | C.ushort |
| int32 | int | C.int |
| uint32 | unsigned int | C.uint |
| int32 | long | C.long |
| uint32 | unsigned long | C.ulong |
| int64 | long long int | C.longlong |
| uint64 | unsigned long long int | C.ulonglong |
Exemple de code :
package main
/*
#include <stdio.h>
void printGoInt8(signed char n) {
printf("%d\n",n);
}
void printGoUInt8(unsigned char n) {
printf("%d\n",n);
}
void printGoInt16(signed short n) {
printf("%d\n",n);
}
void printGoUInt16(unsigned short n) {
printf("%d\n",n);
}
void printGoInt32(signed int n) {
printf("%d\n",n);
}
void printGoUInt32(unsigned int n) {
printf("%d\n",n);
}
void printGoInt64(signed long long int n) {
printf("%ld\n",n);
}
void printGoUInt64(unsigned long long int n) {
printf("%ld\n",n);
}
*/
import "C"
func main() {
C.printGoInt8(C.schar(1))
C.printGoInt8(C.schar(1))
C.printGoInt16(C.short(1))
C.printGoUInt16(C.ushort(1))
C.printGoInt32(C.int(1))
C.printGoUInt32(C.uint(1))
C.printGoInt64(C.longlong(1))
C.printGoUInt64(C.ulonglong(1))
}CGO prend également en charge les types entiers de <stdint.h>, dont les tailles mémoire sont plus claires et dont le style de nommage est très similaire à celui de Go.
| go | c | cgo |
|---|---|---|
| int8 | int8_t | C.int8_t |
| uint8 | uint8_t | C.uint8_t |
| int16 | int16_t | C.int16_t |
| uint16 | uint16_t | C.uint16_t |
| int32 | int32_t | C.int32_t |
| uint32 | uint32_t | C.uint32_t |
| int64 | int64_t | C.int64_t |
| uint64 | uint64_t | C.uint64_t |
Il est recommandé d'utiliser les types entiers de <stdint.h> lors de l'utilisation de CGO.
Nombres à virgule flottante
La correspondance entre les types flottants Go et C est la suivante :
| go | c | cgo |
|---|---|---|
| float32 | float | C.float |
| float64 | double | C.double |
Exemple de code :
package main
/*
#include <stdio.h>
void printGoFloat32(float n) {
printf("%f\n",n);
}
void printGoFloat64(double n) {
printf("%lf\n",n);
}
*/
import "C"
func main() {
C.printGoFloat32(C.float(1.11))
C.printGoFloat64(C.double(3.14))
}Tranches (Slices)
Les tranches sont similaires aux chaînes décrites ci-dessus, mais CGO ne fournit pas de pseudo-fonctions pour copier les tranches. Pour permettre à C d'accéder à une tranche Go, il faut transmettre le pointeur de la tranche. Voici un exemple :
package main
/*
#include <stdio.h>
#include <stdint.h>
void printInt32Arr(int32_t* s, int32_t len) {
for (int32_t i = 0; i < len; i++) {
printf("%d ", s[i]);
}
}
*/
import "C"
import (
"unsafe"
)
func main() {
var arr []int32
for i := 0; i < 10; i++ {
arr = append(arr, int32(i))
}
ptr := unsafe.Pointer(unsafe.SliceData(arr))
C.printInt32Arr((*C.int32_t)(ptr), C.int(len(arr)))
}Sortie :
0 1 2 3 4 5 6 7 8 9Le pointeur du tableau sous-jacent de la tranche est transmis à la fonction C. Comme la mémoire de ce tableau est gérée par Go, il est déconseillé que C conserve une référence de pointeur à long terme. Inversement, voici un exemple d'utilisation d'un tableau C comme tableau sous-jacent d'une tranche Go :
package main
/*
#include <stdio.h>
#include <stdint.h>
int32_t s[] = {1, 2, 3, 4, 5, 6, 7};
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
l := unsafe.Sizeof(C.s) / unsafe.Sizeof(C.s[0])
fmt.Println(l)
goslice := unsafe.Slice(&C.s[0], l)
for i, e := range goslice {
fmt.Println(i, e)
}
}Sortie :
7
0 1
1 2
2 3
3 4
4 5
5 6
6 7La fonction unsafe.Slice permet de convertir un pointeur de tableau en tranche. Intuitivement, un tableau en C est un pointeur vers l'élément de tête, donc on pourrait penser qu'il faut l'utiliser ainsi :
goslice := unsafe.Slice(&C.s, l)Cependant, la sortie montre que cela provoque un dépassement de mémoire pour tous les éléments sauf le premier :
0 [1 2 3 4 5 6 7]
1 [0 -1 0 0 0 3432824 0]
2 [0 0 -1 -1 0 0 -1]
3 [0 0 0 255 0 0 0]
4 [2 0 0 0 3432544 0 0]
5 [0 3432576 0 3432592 0 3432608 0]
6 [0 0 3432624 0 0 0 1422773729]Même si un tableau en C est un pointeur de tête, après encapsulation par CGO, il devient un tableau Go avec sa propre adresse. Il faut donc prendre l'adresse du premier élément du tableau :
goslice := unsafe.Slice(&C.s[0], l)Structures
On peut accéder aux structures C en ajoutant le préfixe C.struct_ au nom de la structure. Les structures C ne peuvent pas être intégrées anonymement dans des structures Go. Voici un exemple simple de structure C :
package main
/*
#include <stdio.h>
#include <stdint.h>
struct person {
int32_t age;
char* name;
};
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
var p C.struct_person
p.age = C.int32_t(18)
p.name = C.CString("john")
fmt.Println(reflect.TypeOf(p))
fmt.Printf("%+v", p)
}Sortie :
main._Ctype_struct_person
{age:18 name:0x1dd043b6e30}Si certains membres d'une structure C contiennent des bit-field, CGO ignorera ces membres. Par exemple, si person est modifié comme suit :
struct person {
int32_t age: 1;
char* name;
};L'exécution produira une erreur :
p.age undefined (type _Ctype_struct_person has no field or method age)Les règles d'alignement mémoire des champs de structure en C et Go ne sont pas identiques. Si CGO est activé, le C domine généralement.
Unions
On peut accéder aux unions C en utilisant C.union_ suivi du nom. Comme Go ne prend pas en charge les unions, elles existent sous forme de tableaux d'octets en Go. Voici un exemple simple :
package main
/*
#include <stdio.h>
#include <stdint.h>
union data {
int32_t age;
char ch;
};
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
var u C.union_data
fmt.Println(reflect.TypeOf(u), u)
}Sortie :
[4]uint8 [0 0 0 0]On peut accéder et modifier via unsafe.Pointer :
func main() {
var u C.union_data
ptr := (*C.int32_t)(unsafe.Pointer(&u))
fmt.Println(*ptr)
*ptr = C.int32_t(1024)
fmt.Println(*ptr)
fmt.Println(u)
}Sortie :
0
1024
[0 4 0 0]Énumérations
On peut accéder aux énumérations C en utilisant le préfixe C.enum_ suivi du nom du type d'énumération. Voici un exemple simple :
package main
/*
#include <stdio.h>
#include <stdint.h>
enum player_state {
alive,
dead,
};
*/
import "C"
import "fmt"
type State C.enum_player_state
func (s State) String() string {
switch s {
case C.alive:
return "alive"
case C.dead:
return "dead"
default:
return "unknown"
}
}
func main() {
fmt.Println(C.alive, State(C.alive))
fmt.Println(C.dead, State(C.dead))
}Sortie :
0 alive
1 deadPointeurs


Quand on parle de pointeurs, on ne peut pas éviter la mémoire. Le principal problème des appels mutuels entre CGO est que les modèles mémoire des deux langages sont différents. La mémoire en C est entièrement gérée manuellement par le développeur : malloc() alloue la mémoire, free() la libère. Si elle n'est pas libérée manuellement, elle ne le sera jamais. Ainsi, la gestion mémoire en C est très stable. Go est différent : il dispose d'un GC et l'espace de pile des Goroutines est dynamique. Lorsque l'espace de pile est insuffisant, il augmente, ce qui peut modifier les adresses mémoire. Comme illustré ci-dessus (le schéma n'est pas rigoureux), un pointeur peut devenir un pointeur dangling courant en C. Même si CGO peut éviter le déplacement de mémoire dans la plupart des cas (grâce à runtime.Pinner pour fixer la mémoire), Go déconseille de référencer la mémoire Go à long terme en C. Inversement, référencer la mémoire C depuis Go est plus sûr, sauf si C.free() est appelé manuellement, car cette mémoire ne sera pas libérée automatiquement.
Pour transmettre des pointeurs entre C et Go, il faut d'abord les convertir en unsafe.Pointer, puis en type de pointeur correspondant, comme void* en C. Voici deux exemples. Le premier montre un pointeur C référençant une variable Go et la modifiant :
package main
/*
#include <stdio.h>
#include <stdint.h>
void printNum(int32_t* s) {
printf("%d\n", *s);
*s = 3;
printf("%d\n", *s);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
var num int32 = 1
ptr := unsafe.Pointer(&num)
C.printNum((*C.int32_t)(ptr))
fmt.Println(num)
}Sortie :
1
3
3Le deuxième exemple montre un pointeur Go référençant une variable C et la modifiant :
package main
/*
#include <stdio.h>
#include <stdint.h>
int32_t num = 10;
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println(C.num)
ptr := unsafe.Pointer(&C.num)
iptr := (*int32)(ptr)
*iptr++
fmt.Println(C.num)
}Sortie :
10
11Par ailleurs, CGO ne prend pas en charge les pointeurs de fonction en C.
Bibliothèques de liaison
Le C n'a pas de gestion de dépendances comme Go. Pour utiliser des bibliothèques écrites par d'autres, outre l'obtention du code source, on peut utiliser des bibliothèques de liaison statiques et dynamiques. CGO prend en charge ces deux approches, permettant d'importer des bibliothèques dans des programmes Go sans avoir besoin du code source.
Bibliothèque de liaison dynamique
Une bibliothèque de liaison dynamique ne peut pas s'exécuter seule. Elle est chargée en mémoire avec le fichier exécutable lors de l'exécution. Voici comment créer une bibliothèque dynamique simple et l'appeler avec CGO. Commencez par créer un fichier lib/sum.c :
#include <stdint.h>
int32_t sum(int32_t a, int32_t b) {
return a + b;
}Créez le fichier d'en-tête lib/sum.h :
#include <stdint.h>
int sum(int32_t a, int32_t b);Ensuite, utilisez gcc pour créer la bibliothèque dynamique. Compilez d'abord pour générer le fichier objet :
$ cd lib
$ gcc -c sum.c -o sum.oPuis créez la bibliothèque dynamique :
$ gcc -shared -o libsum.dll sum.oUne fois créée, importez le fichier d'en-tête sum.h dans le code Go et indiquez à CGO où trouver la bibliothèque via des macros :
package main
/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"
*/
import "C"
import "fmt"
func main() {
res := C.sum(C.int32_t(1), C.int32_t(2))
fmt.Println(res)
}CFLAGS: -Ispécifie le chemin relatif de recherche des fichiers d'en-tête.-Lspécifie le chemin de recherche des bibliothèques.${SRCDIR}représente le chemin absolu du répertoire actuel, car ce paramètre doit être un chemin absolu.-lspécifie le nom de la bibliothèque.sumcorrespond àsum.dll.
CFLAGS et LDFLAGS sont des options de compilation GCC. Pour des raisons de sécurité, CGO désactive certains paramètres. Consultez cgo command pour plus de détails.
Placez la bibliothèque dynamique dans le même répertoire que l'exécutable :
$ ls
go.mod go.sum lib/ libsum.dll* main.exe* main.goCompilez et exécutez le programme Go :
$ go build main.go && ./main.exe
3La bibliothèque de liaison dynamique est maintenant appelée avec succès.
Bibliothèque de liaison statique
Contrairement aux bibliothèques dynamiques, l'importation d'une bibliothèque statique avec CGO la lie finalement avec le fichier objet Go pour former un exécutable. Reprenons sum.c et compilons-le en fichier objet :
$ gcc -o sum.o -c sum.cPuis archivez le fichier objet en bibliothèque statique (le préfixe doit être lib, sinon il ne sera pas trouvé) :
$ ar rcs libsum.a sum.oContenu du fichier Go :
package main
/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"
*/
import "C"
import "fmt"
func main() {
res := C.sum(C.int32_t(1), C.int32_t(2))
fmt.Println(res)
}Compilez :
$ go build && ./main.exe
3La bibliothèque de liaison statique est maintenant appelée avec succès.
Conclusion
Bien que l'objectif de CGO soit d'améliorer les performances, la commutation entre C et Go entraîne également une perte de performance non négligeable. Pour des tâches très simples, CGO est moins efficace que du Go pur. Voici un exemple :
package main
/*
#include <stdint.h>
int32_t cgo_sum(int32_t a, int32_t b) {
return a + b;
}
*/
import "C"
import (
"fmt"
"time"
)
func go_sum(a, b int32) int32 {
return a + b
}
func testSum(N int, do func()) int64 {
var sum int64
for i := 0; i < N; i++ {
start := time.Now()
do()
sum += time.Now().Sub(start).Nanoseconds()
}
return sum / int64(N)
}
func main() {
N := 1000_000
nsop1 := testSum(N, func() {
C.cgo_sum(C.int32_t(1), C.int32_t(2))
})
fmt.Printf("cgo_sum: %d ns/op\n", nsop1)
nsop2 := testSum(N, func() {
go_sum(1, 2)
})
fmt.Printf("pure_go_sum: %d ns/op\n", nsop2)
}Il s'agit d'un test très simple comparant une fonction d'addition écrite en C et en Go, exécutée 1 million de fois pour calculer le temps moyen. Résultats :
cgo_sum: 49 ns/op
pure_go_sum: 2 ns/opLes résultats montrent que CGO est plus de vingt fois plus lent que du Go pur. Si la tâche n'est pas une simple addition mais une opération plus coûteuse, l'avantage de CGO serait plus important. Par ailleurs, l'utilisation de CGO présente d'autres inconvénients :
- De nombreux outils Go ne peuvent plus être utilisés, comme
gotest,pprof. L'exemple de test ci-dessus ne peut pas utilisergotest, il faut l'écrire manuellement. - La vitesse de compilation ralentit et la compilation croisée intégrée n'est plus disponible.
- Problèmes de sécurité mémoire.
- Problèmes de dépendances : si quelqu'un utilise votre bibliothèque, il doit également activer CGO.
N'intégrez pas CGO dans vos projets sans une réflexion approfondie. Pour des tâches très complexes, CGO peut apporter des avantages, mais pour des tâches simples, il vaut mieux s'en tenir à Go.
