FunTester Java 调用 Go 解决方案

FunTester · 2025年01月10日 · 最后由 FunTester 回复于 2025年01月16日 · 5475 次阅读

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

依赖项

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

  • Java 开发工具包(JDK):推荐使用 JDK 11 或更高版本,以确保兼容性和性能优化。
  • Go 编译器:确保 Go 已安装在您的系统上,并且环境变量已正确配置,以便在命令行中直接使用go命令。
  • Java 本地接口(JNI)JNI是 Java 平台提供的一种机制,用于将本地代码(如 C/C++ 或 Go 编译的二进制文件)与 Java 代码集成。
  • cgocgo是 Go 语言的一个工具,允许 Go 代码与 C 代码互操作。通过 cgo,您可以生成与 C 兼容的二进制文件,从而支持 JNI 调用。
  • javac 和 java:确保 Java 编译器和运行时环境已安装,以便编译和运行 Java 程序。

功能演示

在接下来的步骤中,我将详细介绍如何通过编写 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

命令解释

  • -o libadd.so:指定输出文件名为libadd.so,这是一个共享库文件。
  • -buildmode=c-shared:告诉 Go 编译器生成一个 C 兼容的共享库,供其他语言(如 Java)调用。

编译完成后,会生成两个文件: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函数
    • 该函数创建了一个Person实例,并将其序列化为 JSON 字符串。
    • 使用json.Marshal函数将结构体转换为 JSON 格式。
    • 如果序列化失败,函数会返回nil
    • 使用C.CString将 Go 字符串转换为 C 兼容的字符串,以便通过 JNI 传递。
  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
    • 使用org.json.JSONObject解析从 Go 函数返回的 JSON 字符串。
    • 提取nameage字段,并打印到控制台。
  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 开始性能测试

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 6 条回复 时间 点赞

大佬,有研究过 go 调用 java 的操作吗

尚酷米 回复

这个没研究,这种是什么场景会用到的呢

FunTester 回复

我们有些请求的加密算法用 java 写的,直接用 go 写不一定对,还很耗时,想着有没有 go 调用 java 的方式。

尚酷米 回复

我觉得把文章内容反向输出即可,把 Java 的项目编译.jar 然后使用 Go 调用 jni 的库。 我 ChatGPT 得到的答案是 gojni。从理论上来讲是可行的,就不知道加密算法是否能够兼容。
有兴趣可以私聊一下,我觉得这个东西还挺有乐趣的,可以一起尝试解决。

这个可行,很早以前找 github 上找了一个包【tekao.net/jnigi】可以通过 go 调用 jar 包里面的方法,但是有个不太好的,一个 go 进程 jvm 只能开一个,然后这个协程必须锁线程,也许是没看懂用法吧,然后我又太菜就放弃了

尚酷米 回复

很早之前很菜,现在很牛,应该可以解决。
成功勿忘告知我一波,我学习一下。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册