Go,常被称为GoLang,是由 Google 精心打造的一种静态类型、编译型编程语言。它以其简洁的语法、卓越的并发处理能力和高效的性能而著称,因此在后端系统、云原生应用以及微服务架构中得到了广泛应用。Go 语言凭借其丰富的标准库,以及 goroutineschannels 等独特特性,在开发可扩展且高效的程序方面展现了显著优势。许多开发者倾向于将Go与其他编程语言,如 Java,结合使用,以构建功能更为强大的多语言系统。在本文中,我们将深入探讨如何从 Java 环境中调用GoLang函数,以实现两种语言的无缝集成。

依赖项

在从 Java 调用 Go 函数之前,让我们首先看看需要做哪些准备工作:

功能演示

在接下来的步骤中,我将详细介绍如何通过编写 Go 函数、将其编译为共享库,并使用 JNI(Java Native Interface)从 Java 调用该函数。以下是具体步骤:

编写 Go 函数

首先,我们需要编写一个简单的 Go 函数,并将其导出为 C 兼容的符号,以便 Java 可以通过 JNI 调用它。以下是一个示例 Go 函数,它接收两个整数并返回它们的和:

package main

import "C"

//export AddNumbers
func AddNumbers(a, b int) int {
    // 此函数接收两个整数作为输入,
    // 计算它们的和,并返回结果。
    return a + b
}

// main函数是构建共享库所必需的,
// 即使在此情况下它不执行任何操作。
func main() {}

代码解释:

  1. package main:这是 Go 程序的入口点声明。任何需要编译为可执行文件或共享库的 Go 程序都必须使用package main
  2. import "C":该语句启用了 Go 与 C 之间的互操作性。通过导入C包,Go 代码可以生成与 C 兼容的二进制文件,从而支持 JNI 调用。
  3. AddNumbers函数:该函数接收两个整数参数ab,计算它们的和并返回结果。这是一个简单的示例,展示了如何通过 Go 函数处理输入并返回输出。
  4. func main() {}:即使main函数在此不执行任何操作,它也是构建共享库所必需的。Go 编译器需要main函数作为程序的入口点。

将 Go 代码编译为共享库

接下来,我们需要将 Go 代码编译为共享库(.so文件),以便Java程序可以加载并调用它。使用以下命令完成编译:

go build -o libadd.so -buildmode=c-shared main.go

命令解释

编译完成后,会生成两个文件:libadd.so(共享库)和libadd.h(C 头文件)。Java 程序将通过JNI加载libadd.so

编写 Java 代码

现在,我们需要编写一个 Java 程序来加载共享库并调用 Go 函数。以下是示例代码:

/**  
 * Go调用者, 用于调用Go生成的共享库。  
 */  
public class GoInvoker {  

    static {  
    // 加载由Go生成的共享库。  
    // 确保库文件在系统库路径中,或指定其完整路径。  
        System.loadLibrary("add"); // 加载共享库  
    }  

    // 声明一个本地方法,与Go函数对应。  
    // 方法签名必须与Go函数的参数和返回类型匹配。  
    public native int AddNumbers(int a, int b);  

    public static void main(String[] args) {  
        GoInvoker invoker = new GoInvoker();  
        // 调用本地方法并传递两个整数。  
        int result = invoker.AddNumbers(10, 20);  
        // 打印从Go函数接收到的结果。  
        System.out.println("Result from Go Function: " + result);  
    }  

}

代码解释:

  1. 静态块(static { ... }: 在类加载时,静态块会执行System.loadLibrary("add"),加载名为add的共享库(即libadd.so)。确保库文件在系统库路径中,或提供其完整路径。
  2. native关键字native用于声明一个本地方法,表示该方法的实现由外部库(如 Go 编译的共享库)提供。
  3. AddNumbers方法: 该方法与 Go 函数AddNumbers对应,接收两个整数参数并返回一个整数。方法签名必须与 Go 函数完全匹配。
  4. main方法: 在main方法中,创建GoInvoker实例并调用AddNumbers方法,传递参数1020。调用结果存储在result变量中,并打印到控制台。

编译和运行 Java 代码

完成 Java 代码编写后,按照以下步骤编译和运行程序:

  1. 编译 Java 代码
    使用以下命令编译 Java 程序:

    javac GoInvoker.java
    
  2. 运行 Java 程序
    使用以下命令运行程序:

    java GoInvoker
    
  3. 确保共享库可用
    确保libadd.so文件在系统库路径中,或通过以下方式指定路径:
    在 Linux 上,设置LD_LIBRARY_PATH环境变量:

    export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
    

    在 Windows 上,将共享库所在目录添加到PATH环境变量中。

  4. 输出结果
    如果一切配置正确,程序将输出以下结果:

    Result from Go Function: 30
    

通过以上步骤,我们成功实现了从 Java 调用 Go 函数的功能。这种方法结合了 Java 的跨平台能力和 Go 的高性能特性,适用于需要多语言集成的复杂系统开发。

处理复杂数据类型

在实际开发中,我们经常需要处理更复杂的数据类型,例如结构体。为了在 Go 和 Java 之间传递复杂数据,可以使用JSON作为中间格式进行序列化和反序列化。以下是一个示例,展示如何在 Go 中定义一个结构体,将其序列化为JSON,并通过 JNI 在 Java 中解析。

将结构体导出为 JSON

首先,我们在 Go 中定义一个Person结构体,并编写一个函数将其序列化为 JSON 字符串:

package main

import (
    "C"
    "encoding/json"
    "fmt"
    )

// 定义Person结构体
type Person struct {
    Name string `json:"name"`
    Ageint`json:"age"`
}

// 导出一个函数,将结构体序列化为JSON字符串
//export GetPersonJSON
func GetPersonJSON() *C.char {
    person := Person{Name: "John Doe", Age: 30} // 创建Person实例
    jsonData, err := json.Marshal(person)// 序列化为JSON
    if err != nil {
        fmt.Println("Error marshaling data:", err)
        return nil
    }
    return C.CString(string(jsonData)) // 返回C兼容的字符串
}

// main函数是构建共享库所必需的
func main() {}

代码解释

  1. Person结构体: 定义了一个包含Name(字符串类型)和Age(整数类型)的结构体。
  2. GetPersonJSON函数
  3. main函数: 虽然main函数为空,但它是构建共享库所必需的。

处理 JSON 并调用 Go 函数

接下来,我们在 Java 中编写代码,加载共享库并调用 Go 函数以获取 JSON 字符串,然后解析该字符串:

import com.alibaba.fastjson2.JSON;  
import com.alibaba.fastjson2.JSONObject;  

/**  
 * Go调用者, 用于调用Go生成的共享库。  
 */  
public class GoInvoker {  

    static {  
        // 加载Go共享库  
        System.loadLibrary("add"); // 确保库名与Go生成的共享库一致  
    }  

    // 声明本地方法,用于调用Go函数并接收JSON字符串  
    public native String GetPersonJSON();  

    public static void main(String[] args) {  
        GoInvoker invoker = new GoInvoker();  
        // 调用Go函数,获取JSON字符串  
        String jsonResult = invoker.GetPersonJSON();  

        // 解析JSON字符串  
        try {  
            JSONObject personObject = JSON.parseObject(jsonResult);  
            String name = personObject.getString("name");  
            int age = personObject.getIntValue("age");  
            // 打印解析后的结果  
            System.out.println("Name: " + name);  
            System.out.println("Age: " + age);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

}

代码解释:

  1. 加载共享库: 使用System.loadLibrary("add")加载 Go 生成的共享库(libadd.so)。
  2. 本地方法声明public native String GetPersonJSON();声明了一个本地方法,用于调用 Go 函数并返回 JSON 字符串。
  3. 解析 JSON
  4. 异常处理: 如果 JSON 解析失败,捕获并打印异常信息。

编译和运行代码

按照以下步骤编译和运行代码:

  1. 编译 Go 代码
    使用以下命令将 Go 代码编译为共享库:

    go build -o libadd.so -buildmode=c-shared main.go
    
  2. 编译 Java 代码
    使用以下命令编译 Java 程序:

    javac -cp .:org.json.jar GoInvoker.java
    

    (确保org.json.jar在类路径中,或使用 Maven/Gradle 管理依赖。)

  3. 运行 Java 程序
    使用以下命令运行程序:

    java -cp .:org.json.jar GoInvoker
    
  4. 输出结果
    如果一切配置正确,程序将输出以下结果:

    Name: John Doe
    Age: 30
    

总结

本文深入探讨了如何通过 JNI(Java Native Interface)与共享库技术实现 Java 与 Go 的高效集成,从基础数据类型的传递到复杂结构体的处理,全面展示了跨语言调用的技术细节。通过将 Go 的高性能与 Java 的生态优势相结合,开发者能够构建兼具高效性与扩展性的多语言系统。文章不仅提供了从 Go 函数编译到 Java 调用的完整实践指南,还通过 JSON 序列化与反序列化,解决了复杂数据类型的跨语言交互问题,为现代分布式系统与微服务架构提供了强有力的技术支撑。无论是后端开发、云原生应用,还是多语言微服务集成,本文都提供了极具参考价值的解决方案。

FunTester 原创精华

【连载】从 Java 开始性能测试


↙↙↙阅读原文可查看相关链接,并与作者交流