[译]从其他语言调用Go函数

Posted Tacey Wong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[译]从其他语言调用Go函数相关的知识,希望对你有一定的参考价值。


从1.5版本开始,Go编译器通过-buildmode选项引入了对多种构建模式的支持。被称为Go执行模式,这些构建模式扩展了go tool的功能,可以将Go包编译到多种格式,包括Go打包文档,Go共享库,C打包文档,C共享库,以及1.8版本引入的Go动态插件。

本文介绍如何将Go包编译为C共享库。这种构建模式下编译器会输出一个标准模式的共享对象二进制文件(.so),这个文件将Go函数以C语言风格的API对外暴露。同时我们将讨论如何创建可以从C、Python、Ruby、Node和Java调用的Go库。

所有的代码都可以在Github上找到。

Go代码

假设我们已经写了一个非常棒的Go库,我们想让它同样可以被其他语言使用。下面有四点要求是将代码编译为共享库所必须的。

  • 包名必须是main。编译器将会把包代码和所有依赖统一构建到一个单一的共享对象二进制文件。
  • 源代码必须倒入伪包“C”
  • 使用export注释来标记你想要让其他语言访问使用的函数
  • 必须声明一个空main函数
    下面的Go代码导出函数有Add、Cos、Sort和Log。不可否认,这个awesome(令人惊叹的)软件包并没有你看到的那么令人印象深刻。然而,它的各种函数签名将帮助我们探索类型映射的含义。
package main
import "C"
import (
 "fmt"
 "math"
 "sort"
 "sync"
)
var count int
var mtx sync.Mutex
//export Add
func Add(a, b int) int { return a + b }
//export Cosine
func Cosine(x float64) float64 { return math.Cos(x) }
//export Sort
func Sort(vals []int) { sort.Ints(vals) }
//export Log
func Log(msg string) int {
  mtx.Lock()
  defer mtx.Unlock()
  fmt.Println(msg)
  count++
  return count
}
func main() {}

使用-buildmode=c-shared编译选项将其编译为共享对象二进制文件:

go build -o awesome.so -buildmode=c-shared awesome.go

上面的操作完成之后,编译器将会输出两个文件:C语言头文件awesome.h,共享对象二进制文件awesome.so

-rw-rw-r-- 1 tacey tacey 1.7K 10月 26 20:28 awesome.h
-rw-rw-r-- 1 tacey tacey 2.3M 10月 26 20:28 awesome.so

注意,对于如此小的库.so文件就大约有2MB的大小。这是因为整个Go运行时机制和依赖包都被塞进了一个共享对象二进制文件中(类似编译一个单一静态执行文件)

头文件

头文件定义了映射Go兼容类型的C类型(使用CGO语义,这里不讨论)

/* Code generated by cmd/cgo; DO NOT EDIT. */
...
typedef long long GoInt64;
typedef GoInt64 GoInt;
...
typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
...
extern GoInt Add(GoInt p0, GoInt p1);
extern GoFloat64 Cosine(GoFloat64 p0);
...

共享对象文件

编译器生成的其他文件是一个64位ELF共享对象二进制文件(在Linux环境下),我们可以通过file命令来验证它的信息

$> file awesome.so
awesome.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=7074746a2466bbe39ed8350cf34aeff4d4d95b36, not stripped

使用nmgrep命令我们可以检视到我们的go函数已经被暴露:

$> nm awesome.so | grep -e "T Add" -e "T Cosine" -e "T Sort" -e "T Log"
00000000000e9560 T Add
00000000000e95f0 T Cosine
00000000000e96f0 T Log
00000000000e9680 T Sort

下面我们将尝试如何从其他语言调研嗯这些被暴露的函数

从C语言

在C语言中有两种调用共享对象二进制文件中Go函数的方法。一是在编译时源代码静态绑定到一起,但是运行时动态链接。二是Go函数符号在运行时动态加载和绑定。

动态链接

这种方法下我们使用头文件静态引用共享对象文件中的函数和类型。简单示例如下(一些输出语句被忽略了)

#include <stdio.h>
#include "awesome.h"
int main() {
    GoInt a = 12;
    GoInt b = 99;
    printf("awesome.Add(12,99) = %d\\n", Add(a, b));
    printf("awesome.Cosine(1) = %f\\n", (float)(Cosine(1.0)));
    GoInt data[6] = {77, 12, 5, 99, 28, 23};
    GoSlice nums = {data, 6, 6};
    Sort(nums);
    for (int i = 0; i < 6; i++){
        printf("%d,", ((GoInt *)nums.data)[i]);
    }
    GoString msg = {"Hello from C!", 13};
    Log(msg);
}

接着指定共享库编译代码

$> gcc -o client client1.c ./awesome.so

当执行是,二进制链接awesome.so库产生下面的输出

$> ./client
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(77,12,5,99,28,23): 5,12,23,28,77,99,
Hello from C!

动态加载

在此方法下,C代码使用动态链接家在库(libdl.so)来动态加载并绑定被暴露的符号。它使用定义在dhfcn.h中的函数,dlopen打开二进制文件,dlsym查询符号,dlerror便利错误,dlclose关闭共享库文件。

下面的代码比第一种方法中稍长,但功能是一样的。

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
typedef long long go_int;
typedef double go_float64;
typedef struct{void *arr; go_int len; go_int cap} go_slice;
typedef struct{const char *p; go_int len;} go_str;
int main(int argc, char **argv) {
  void *handle;
  char *error;
  handle = dlopen ("./awesome.so", RTLD_LAZY);
  if (!handle) {
    fputs (dlerror(), stderr);
    exit(1);
  }
  go_int (*add)(go_int, go_int) = dlsym(handle, "Add");
  if ((error = dlerror()) != NULL)  {
      fputs(error, stderr);
      exit(1);
  }
  go_int sum = (*add)(12, 99);
  printf("awesome.Add(12, 99) = %d\\n", sum);

 go_float64 (*cosine)(go_float64) = dlsym(handle, "Cosine");
 go_float64 cos = (*cosine)(1.0);

 void (*sort)(go_slice) = dlsym(handle, "Sort");
 go_int data[5] = {44,23,7,66,2};
 go_slice nums = {data, 5, 5};
 sort(nums);

 go_int (*log)(go_str) = dlsym(handle, "Log");
 go_str msg = {"Hello from C!", 13};
 log(msg);

 dlclose(handle);
}

在这个版本中,代码使用他自己定义的C类型:go_int, go_float, go_slice, 和 go_str (为了说明问题,可以用awesome.h)。函数dlsym加载函数符号,并将它们赋给各自的函数指针。

接下来,可以编译代码,将其与dl库(不是Awesome.so)链接,如下所示:

$> gcc -o client client2.c -ldl

当代码执行时,C二进制加载和连接到共享库awesome.so产生下面的输出。

$> ./client
awesome.Add(12, 99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(44,23,7,66,2): 2,7,23,44,66,
Hello from C!

从Python

在Python中会更简单一些。使用ctype外部函数库像下面这样调用被暴露的函数

from ctypes import *
lib = cdll.LoadLibrary("./awesome.so")
lib.Add.argtypes = [c_longlong, c_longlong]
print "awesome.Add(12,99) = %d" % lib.Add(12,99)
lib.Cosine.argtypes = [c_double]
lib.Cosine.restype = c_double 
cos = lib.Cosine(1)
print "awesome.Cosine(1) = %f" % cos
class GoSlice(Structure):
    _fields_ = [("data", POINTER(c_void_p)), 
                ("len", c_longlong), ("cap", c_longlong)]
nums = GoSlice((c_void_p * 5)(74, 4, 122, 9, 12), 5, 5)
lib.Sort.argtypes = [GoSlice]
lib.Sort.restype = None
lib.Sort(nums)
class GoString(Structure):
    _fields_ = [("p", c_char_p), ("n", c_longlong)]
lib.Log.argtypes = [GoString]
msg = GoString(b"Hello Python!", 13)
lib.Log(msg)

lib变量表示从共享对象文件中加载的符号。Python类GoStringGoSlice映射对应的C结构体对象。运行Python代码嗲用Go函数产生下面的输出。

$> python client.py
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(74,4,122,9,12) = [ 4 9 12 74 122 ]
Hello Python!

从Java

可以使用Java原生访问项目或者JNA来调用被暴露的Go函数。

import com.sun.jna.*;
public class Client {
  public interface Awesome extends Library {
    public class GoSlice extends Structure {
      ...
      public Pointer data;
      public long len;
      public long cap;
    }
  
    public class GoString extends Structure {
      ...
      public String p;
      public long n;
    }
    public long Add(long a, long b);
    public double Cosine(double val);
    public void Sort(GoSlice.ByValue vals);
    public long Log(GoString.ByValue str);
  }
  static public void main(String argv[]) {
    Awesome awesome = (Awesome) Native.loadLibrary(
      "./awesome.so", Awesome.class);
    System.out.printf(... awesome.Add(12, 99));
    System.out.printf(... awesome.Cosine(1.0));
    long[] nums = new long[]{53,11,5,2,88};
    Memory arr = new Memory(... Native.getNativeSize(Long.TYPE));
    Awesome.GoSlice.ByValue slice = new Awesome.GoSlice.ByValue();
    slice.data = arr;
    slice.len = nums.length;
    slice.cap = nums.length;
    awesome.Sort(slice);
    Awesome.GoString.ByValue str = new Awesome.GoString.ByValue();
    str.p = "Hello Java!";
    str.n = str.p.length();
    System.out.printf(... awesome.Log(str));
  }
}

Java接口Awesome表示从awesome.so加载的符号。类GoSliceGoString映射对应的C结构体。当代码被编译运行,调用被暴露的G哦函数产生下面的输出

$> javac -cp jna.jar Client.java
$> java -cp .:jna.jar Client
awesome.Add(12, 99) = 111
awesome.Cosine(1.0) = 0.5403023058681398
awesome.Sort(53,11,5,2,88) = [2 5 11 53 88 ]
Hello Java!

从Ruby

从Ruby中调用Go函数的方法模式和Python中差不多。只不过Rubu是通过FFI gem来动态加载调用:

require \'ffi\'
module Awesome
  extend FFI::Library
  ffi_lib \'./awesome.so\'
  class GoSlice < FFI::Struct
    layout :data,  :pointer,
           :len,   :long_long,
           :cap,   :long_long
  end
  class GoString < FFI::Struct
    layout :p,     :pointer,
           :len,   :long_long
  end
  attach_function :Add, [:long_long, :long_long], :long_long
  attach_function :Cosine, [:double], :double
  attach_function :Sort, [GoSlice.by_value], :void
  attach_function :Log, [GoString.by_value], :int
end

print "awesome.Add(12, 99) = ",  Awesome.Add(12, 99), "\\n"
print "awesome.Cosine(1) = ", Awesome.Cosine(1), "\\n"
nums = [92,101,3,44,7]
ptr = FFI::MemoryPointer.new :long_long, nums.size
ptr.write_array_of_long_long  nums
slice = Awesome::GoSlice.new
slice[:data] = ptr
slice[:len] = nums.size
slice[:cap] = nums.size
Awesome.Sort(slice)
msg = "Hello Ruby!"
gostr = Awesome::GoString.new
gostr[:p] = FFI::MemoryPointer.from_string(msg)
gostr[:len] = msg.size
Awesome.Log(gostr)

FFI::library被扩展声明加载被暴露的符号。类GoSliceGoString映射对应的C结构体。当执行代码调用Go函数时产生下面的输出

从NodeJS

NodeJS使用被称作node-ffi的外部函数库(以及其他依赖库)来动态加载和调用被暴露的Go函数

var ref = require("ref");
var ffi = require("ffi");
var Struct = require("ref-struct")
var ArrayType = require("ref-array")
var LongArray = ArrayType(ref.types.longlong);
var GoSlice = Struct({
  data: LongArray,
  len:  "longlong",
  cap: "longlong"
});
var GoString = Struct({
  p: "string",
  n: "longlong"
});
var awesome = ffi.Library("./awesome.so", {
  Add: ["longlong", ["longlong", "longlong"]],
  Cosine: ["double", ["double"]],
  Sort: ["void", [GoSlice]],
  Log: ["longlong", [GoString]]
});
console.log("awesome.Add(12, 99) = ", awesome.Add(12, 99));
console.log("awesome.Cosine(1) = ", awesome.Cosine(1));
nums = LongArray([12,54,0,423,9]);
var slice = new GoSlice();
slice["data"] = nums;
slice["len"] = 5;
slice["cap"] = 5;
awesome.Sort(slice);
str = new GoString();
str["p"] = "Hello Node!";
str["n"] = 11;
awesome.Log(str);

ffi对象管理从动态库加载的符号。Struct对象创建GoSliceGoString来映射对应的C结构体。运行代码调用被暴露的Go函数产生下面的输出:

awesome.Add(12, 99) =  111
awesome.Cosine(1) =  0.5403023058681398
awesome.Sort([12,54,9,423,9] =  [ 0, 9, 12, 54, 423 ]
Hello Node!

总结

本文展示了如何创建一个可以从其他语言中使用的Go库。通过将Go包编译成C风格的共享库,Go程序员可以使用共享对象二进制文件的进程内集成,轻松地让他们的项目与C、Python、Ruby、Node、Java等语言一起工作。所以,下次在Go中创建非常棒的功能API时,记得与非Go开发人员共享。

还真有人点开啊

『每周译Go』使用 Go 泛型的函数式编程

函数式编程是很多语言正在支持或已经支持的日渐流行的编程范式。Go 已经支持了其中一部分的特性,比如头等函数和更高阶功能的支持,使函数式编程成为可能。

Go 缺失的一个关键特性是泛型。缺少这个特性,Go 的函数库和应用不得不从下面的两种方法中选择一种:类型安全 + 特定使用场景或类型不安全 + 未知使用场景。在 2022 年初即将发布的 Go 1.18 版本,泛型将被加进来,从而使 Go 支持新型的函数式编程形式。

在本篇文章中,我将介绍一些函数式编程的背景,Go 函数式编程的现状调查,并讨论 Go 1.18 计划的特性以及如何将它们用于函数式编程。

背景

什么是函数式编程?

维基百科中定义的函数式编程是:

通过应用组合函数的编程范式。

更具体的说,函数式编程有以下几个关键特征:

  • 纯函数 - 使用相同的输入总是返回无共享状态、可变数据或副作用的相同输出的函数
  • 不可变数据 - 数据创建后不会再被分配或修改
  • 函数组合 - 组合多个函数对数据进行处理逻辑
  • 声明式而非指令式 - 表示的是函数的处理方式而无需定义 如何完成

对于函数式编程更详细的信息,可以参考这两篇有详细描述例子的文章:函数式编程是什么?和函数式的 Go

函数式编程的优势是什么?

函数式编程是让开发者提升代码质量的一些模式。这些质量提升的模式并非函数式编程独有,而是一些 “免费” 的优势。

  • 可测性 - 测试纯函数更加简单,因为函数永远不会产生超出作用范围的影响(比如,终端输出、数据库的读取),并总会得到可预测的结果
  • 可表达性 - 函数式编程/库使用声明式的基础可以更高效地表达函数的原始意图,尽管需要额外学习这些基础
  • 可理解性 - 阅读和理解没有副作用、全局或可变的纯函数主观来看更简单

正如多数开发者从经验中学到的,如 Robert C. Martin 在代码整洁之道中所说:

确实,相对于写代码,花费在读代码上的时间超过 10 倍。为了写出新代码,我们一直在读旧代码。…[因此,] 让代码更易读,可以让代码更易写。

根据团队的经验或学习函数式编程的意愿,这些优势会产生很大的影响。相反,对于缺乏经验和足够时间投入学习的团队,或维护大型的代码仓库时,函数式编程将会产生相反的作用,上下文切换的引入或显著的重构工作将无法产生相应的价值。

Go 函数式编程的现状

Go 不是一门函数语言,但确实提供了一些允许函数式编程的特性。有大量的 Go 开源库提供函数特性。我们将会讨论泛型的缺失导致这些库只能折衷选择。

语言特性

函数式编程的语言支持包括一系列从仅支持函数范式(比如 Haskell)到多范式和头等函数的支持(比如 Scale、Elixir),还包括多范式和部分支持(如 Javascript、Go)。在后面的语言中,函数式编程的支持一般是通过使用社区创建的库,它们复制了前面两个语言的部分或全部的标准库的特性。

属于后一种类别的 Go 要使用函数式编程需要下面这些特性:

† 将在 Go 1.18 中可用(2022 年初)

现有的库

在 Go 生态中,有大量函数式编程的库,区别在于流行度、特性和工效。由于缺少泛型,它们全部只能从下面两种选择中取一个:

  1. 类型安全和特定使用场景 - 选择这个方法的库实现的设计是类型安全,但只能处理特定的预定义类型。因为无法应用于自适应的类型或结构体,这些库的应用范围将受限制。
    - 比如, func UniqString(data []string) []stringfunc UniqInt(data []int) []int 都是类型安全的,但只能应用在预定义的类型
  2. 类型不安全和未知的应用场景 - 选择这个方法的库实现的是类型不安全但可以应用在任意使用场景的方法。这些库可以处理自定义类型和结构体,但折衷点在于必须使用类型断言,这让应用在不合理的实现时有运行时崩溃的风险。
    - 比如,一个通用的函数可能有这样的命名: func Uniq(data interface{}) interface{}

这两种设计选择显示了两种相似的不吸引人的选项:有限的使用或运行时崩溃的风险。最简单也许最常见的选择是不使用 Go 的函数式编程库,坚持指令式的风格。

使用泛型的函数式 Go

在2021年3月19日,泛型的设计提案通过并定为 Go 1.18 发行版的一部分。有了泛型之后,函数式编程库就不再需要在可用性和类型安全之间进行折衷。

Go 1.18 实验

Go 开发组发布了一个 go 1.18 游乐场,便于大家尝鲜泛型。同时也有一个实验性的编译器,在 go 代码仓库的一个分支上实现了泛型特性的最小集合。这两个都是在 Go 1.18 上尝鲜泛型的不错选择。

一个使用场景的探索

在前面说到的那个 unique 函数使用了两种可能的设计方法。有了泛型,它可以重写为 func Uniq[T](data []T) []T,并可以使用任意类型来调用,比如 Uniq[string any](data []string) []stringUniq[MyStruct any](data []MyStruct) []MyStruct。为了进一步阐述这个概念,下面是一个具体的例子,展示了在 Go 1.18 中如何使用函数式单元来解决实际问题。

背景

一个在网络世界常见的案例是 HTTP 的请求响应,其中 API 接口返回的 JSON 数据一般会被消费应用转换为一些有用的结构。

问题 & 输入数据

考虑下这个从 API 返回用户、得分和朋友信息的响应:

[
  {
    "id""6096abc445dbb831decde62f",
    "index": 0,
    "isActive"true,
    "isVerified"false,
    "user": {
      "points": 7521,
      "name": {
        "first""Ramirez",
        "last""Gillespie"
      },
      "friends": [
        {
          "id""6096abc46573cedd17fb0201",
          "name""Crawford Arnold"
        },
        ...
      ],
      "company""SEALOUD"
    },
    "level""gold",
    "email""ramirez.gillespie@sealoud.com",
    "text""Consequat pariatur aliquip pariatur mollit mollit cillum sint. Elit est nisi velit cillum. Ex mollit dolor qui velit Lorem proident ullamco magna velit nulla qui. Elit duis non ad laborum ullamco irure nulla culpa. Proident culpa esse deserunt minim sint nisi duis culpa nostrud in incididunt ad. Amet qui laborum deserunt proident adipisicing exercitation quis.",
    "created_at""Saturday, August 3, 2019 8:12 AM",
    "greeting""Hello, Ramirez! You have 9 unread messages.",
    "favoriteFruit""banana"
  },
  ...
]

假设目标是获取各个等级的高分用户。我们将看下函数式和指令式风格的样子。

指令式
// imperative
func getTopUsers(posts []Post) []UserLevelPoints {

    postsByLevel := map[string]Post{}
    userLevelPoints := make([]UserLevelPoints, 0)

    for _, post := range posts {

        // Set post for group when group does not already exist
        if _, ok := postsByLevel[post.Level]; !ok {
            postsByLevel[post.Level] = post
            continue
        }

        // Replace post for group if points are higher for current post
        if postsByLevel[post.Level].User.Points < post.User.Points {
            postsByLevel[post.Level] = post
        }
    }

    // Summarize user from post
    for _, post := range postsByLevel {
        userLevelPoints = append(userLevelPoints, UserLevelPoints{
            FirstName:   post.User.Name.First,
            LastName:    post.User.Name.Last,
            Level:       post.Level,
            Points:      post.User.Points,
            FriendCount: len(post.User.Friends),
        })
    }

    return userLevelPoints

}

posts, _ := getPosts("data.json")
topUsers := getTopUsers(posts)

fmt.Printf("%+v\n", topUsers)
// [{FirstName:Ferguson LastName:Bryant Level:gold Points:9294 FriendCount:3} {FirstName:Ava LastName:Becker Level:silver Points:9797 FriendCount:2} {FirstName:Hahn LastName:Olsen Level:bronze Points:9534 FriendCount:2}]

样例的完整代码

函数式
// functional
var getTopUser = Compose3[[]Post, []Post, Post, UserLevelPoints](
    // Sort users by points
    SortBy(func (prevPost Post, nextPost Post) bool {
        return prevPost.User.Points > nextPost.User.Points
    }),
    // Get top user by points
    Head[Post],
    // Summarize user from post
    func(post Post) UserLevelPoints {
        return UserLevelPoints{
            FirstName:   post.User.Name.First,
            LastName:    post.User.Name.Last,
            Level:       post.Level,
            Points:      post.User.Points,
            FriendCount: len(post.User.Friends),
        }
    },
)

var getTopUsers = Compose3[[]Post, map[string][]Post, [][]Post, []UserLevelPoints](
    // Group posts by level
    GroupBy(func (v Post) string { return v.Level }),
    // Covert map to values only
    Values[[]Post, string],
    // Iterate over each nested group of posts
    Map(getTopUser),
)

posts, _ := getPosts("data.json")
topUsers := getTopUsers(posts)

fmt.Printf("%+v\n", topUsers)
// [{FirstName:Ferguson LastName:Bryant Level:gold Points:9294 FriendCount:3} {FirstName:Ava LastName:Becker Level:silver Points:9797 FriendCount:2} {FirstName:Hahn LastName:Olsen Level:bronze Points:9534 FriendCount:2}]

样例的完整代码

从上面的样例中可以看出一些特性:

  1. 指令式的实现在 Go 1.16 下是有效的(本文编写时的最新版本),而函数式的实现只在使用 Go 1.18(go2go)编译才有效
  2. 函数式例子中的类型参数的泛型函数(如,Compose3、Head 等)仅 Go 1.18 支持
  3. 两个实现在各自对应的风格下,使用了不同的逻辑来解决同样的问题
  4. 指令式的实现相比使用及早求值(即本例中的pneumatic)的函数来说,计算更加高效

使用 Go 1.18 函数式库的实验

在上面的例子中,两个使用场景使用了 go2go 编译器和一个叫做 pneumatic 的 Go 1.18 库,它提供了与Ramda (JavaScript), Elixir 标准库以及其他相似的常见函数式单元。鉴于 go2go 编译器有限的特性集,在本文发布时 pneumatic 只能用于实验目的,但从长期看,随着 Go 1.18 编译器的逐渐成熟,它会包含常见的函数式 Go 库。设置 pneumatic 和使用 Go 1.18 进行函数式编程的指导参见 pneumatic readme。

结论

Go 增加泛型将会支持新型的方案、方法和范式,从而成为众多支持函数式编程的语言之一。随着函数式编程的逐渐流行,函数式编程的支持也会越来越好,从而有机会吸引那些现在还没考虑学习 Go 的开发者并让社区持续发展——这是在我看来比较积极的一面。非常期待看到在后续支持泛型之后和它带来新的解决方法后,Go 社区和生态将会发展成什么样。

参考资料

  • Go 函数库调研
  • go-funk [2.5k stars, type-safe or generic, active]

  • go-underscore [1.2k stars, generic, abandoned]

  • gubrak [336 stars, generic, active]

  • fpGo [167 stars, generic, active]

  • functional-go [92 stars, type-safe, active]

  • 文章
  • Go 泛型的过去、现在和将来

  • 本文永久链接:https://github.com/gocn/translator/blob/master/2021/w24_functional_programming_in_go_with_generics.md

  • 译者:cvley


    别忘了还有 Gopher China 2021 大会在文末等着你哦~


    想和各位技术大佬们同台见面嘛?


    那就赶快点击下方「阅读原文」报名参加呀!

    以上是关于[译]从其他语言调用Go函数的主要内容,如果未能解决你的问题,请参考以下文章

    『每周译Go』使用 Go 泛型的函数式编程

    『每周译Go』Go 官方出品泛型教程:如何开始使用泛型

    Go语言学习-函数

    Go语言之函数使用

    (译)使用Go语言从零编写PoS区块链

    译并发模型和事件循环