This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 7ec4a175b feat(go): implement grpc stub generation (#3698)
7ec4a175b is described below

commit 7ec4a175bce944de4e9ef2e85f2065f4250dee42
Author: Ayush Kumar <[email protected]>
AuthorDate: Tue Jun 9 10:51:05 2026 +0530

    feat(go): implement grpc stub generation (#3698)
    
    ## Why?
    Go implementation only provides type generation and misses the gRPC
    service stub generations for now. this PR adds it.
    
    ## Related issues
    Closes #3274
    
    
    ## What it does ?
    ### Implemented till now -
    #### `ImportTracker` class in `base.py`
    1. which returns the foreign imports defined in a RpcMethod inside a
    schema file's service interface.
    
    #### `generate_services()`
    1. which returns a service generated file
    2. License header, package header using `get_package_name()`
    3. the imports block of go using `_build_import_block(tracker)`
    `_build_import_block(tracker)` function returns the import block with
    fixed grpc, context imports which are required by every single generated
    file for each service. and a foreign type imports statement returned by
    the `tracker.go_imports()`. at end after all the service code generation
    is done, this function inserts the import block to the saved placeholder
    index.
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 compiler/fory_compiler/generators/go.py            |   7 +-
 compiler/fory_compiler/generators/services/base.py |  54 ++
 compiler/fory_compiler/generators/services/go.py   | 720 +++++++++++++++++++++
 .../fory_compiler/tests/test_service_codegen.py    |  26 +-
 integration_tests/grpc_tests/generate_grpc.py      |   8 +
 integration_tests/grpc_tests/go/go.mod             |  35 +
 integration_tests/grpc_tests/go/go.sum             |  24 +
 integration_tests/grpc_tests/go/main.go            | 299 +++++++++
 .../apache/fory/grpc_tests/GrpcInteropTest.java    |  94 +++
 integration_tests/grpc_tests/run_tests.sh          |   4 +-
 10 files changed, 1268 insertions(+), 3 deletions(-)

diff --git a/compiler/fory_compiler/generators/go.py 
b/compiler/fory_compiler/generators/go.py
index b25e63c79..c4cc57e73 100644
--- a/compiler/fory_compiler/generators/go.py
+++ b/compiler/fory_compiler/generators/go.py
@@ -21,6 +21,7 @@ from pathlib import Path
 from typing import Dict, List, Optional, Set, Tuple, Union as TypingUnion
 
 from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.generators.services.go import GoServiceGeneratorMixin
 from fory_compiler.frontend.utils import parse_idl_file
 from fory_compiler.ir.ast import (
     Message,
@@ -38,7 +39,7 @@ from fory_compiler.ir.ast import (
 from fory_compiler.ir.types import PrimitiveKind
 
 
-class GoGenerator(BaseGenerator):
+class GoGenerator(GoServiceGeneratorMixin, BaseGenerator):
     """Generates Go structs with fory tags."""
 
     language_name = "go"
@@ -206,6 +207,10 @@ class GoGenerator(BaseGenerator):
         # Generate a single Go file with all types
         files.append(self.generate_file())
 
+        # Generate gRPC service stubs if requested
+        if self.options.grpc:
+            files.extend(self.generate_services())
+
         return files
 
     def get_package_name(self) -> str:
diff --git a/compiler/fory_compiler/generators/services/base.py 
b/compiler/fory_compiler/generators/services/base.py
new file mode 100644
index 000000000..2bc2bcfb2
--- /dev/null
+++ b/compiler/fory_compiler/generators/services/base.py
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"Shared utilities for gRPC service stub generators."
+
+from enum import Enum
+from typing import List, Dict
+from fory_compiler.ir.ast import RpcMethod
+
+
+class StreamingMode(Enum):
+    UNARY = 1
+    CLIENT_STREAMING = 2
+    SERVER_STREAMING = 3
+    BIDIRECTIONAL = 4
+
+
+def streaming_mode(method: RpcMethod) -> StreamingMode:
+    """Defines the type of rpc streaming patterns."""
+    if not method.client_streaming and not method.server_streaming:
+        return StreamingMode.UNARY
+    elif method.client_streaming and not method.server_streaming:
+        return StreamingMode.CLIENT_STREAMING
+    elif not method.client_streaming and method.server_streaming:
+        return StreamingMode.SERVER_STREAMING
+    else:
+        return StreamingMode.BIDIRECTIONAL
+
+
+class ImportTracker:
+    """Accumulates cross-package Go imports for generated service stubs."""
+
+    def __init__(self):
+        self._imports: Dict[str, str] = {}
+
+    def add(self, alias: str, import_path: str) -> None:
+        self._imports[alias] = import_path
+
+    def go_imports(self) -> List[str]:
+        return sorted(self._imports.values())
diff --git a/compiler/fory_compiler/generators/services/go.py 
b/compiler/fory_compiler/generators/services/go.py
new file mode 100644
index 000000000..399dbe685
--- /dev/null
+++ b/compiler/fory_compiler/generators/services/go.py
@@ -0,0 +1,720 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Go gRPC service code generator."""
+
+from typing import List
+from fory_compiler.generators.services.base import (
+    ImportTracker,
+    StreamingMode,
+    streaming_mode,
+)
+from fory_compiler.generators.base import GeneratedFile
+from fory_compiler.ir.ast import Service, NamedType
+
+
+class GoServiceGeneratorMixin:
+    """Generates Go gRPC service stubs."""
+
+    def generate_services(self) -> List[GeneratedFile]:
+        local_services = [
+            s for s in self.schema.services if not self.is_imported_type(s)
+        ]
+        if not local_services:
+            return []
+        return [self._generate_grpc_file(local_services)]
+
+    def _generate_grpc_file(self, services: List[Service]) -> GeneratedFile:
+        """Generate one _grpc.go file containing all services in the schema."""
+        lines: List[str] = []
+        tracker = ImportTracker()
+
+        # License header
+        lines.append(self.get_license_header("//"))
+        lines.append("")
+
+        # Package declaration
+        lines.append(f"package {self.get_package_name()}")
+        lines.append("")
+
+        # Save placeholder index so imports can be inserted after all code is
+        # generated (imports are collected lazily via tracker as types are 
resolved).
+        import_placeholder_index = len(lines)
+
+        # CodecV2 is emitted once per file, shared by all services in the 
schema.
+        lines.extend(self._generate_codec())
+
+        for service in services:
+            lines.extend(self._generate_client_interface(service, tracker))
+            lines.extend(self._generate_client_struct(service))
+            lines.extend(self._generate_new_client(service))
+            lines.extend(self._generate_client_methods(service, tracker))
+            lines.extend(self._generate_stream_types(service, tracker))
+            lines.extend(self._generate_server_interface(service, tracker))
+            lines.extend(self._generate_unimplemented_server(service, tracker))
+            lines.extend(self._generate_server_stream_types(service, tracker))
+            lines.extend(self._generate_service_desc(service, tracker))
+            lines.extend(self._generate_register_server(service))
+
+        import_lines = self._build_import_block(tracker)
+        for i, line in enumerate(import_lines):
+            lines.insert(import_placeholder_index + i, line)
+
+        return GeneratedFile(
+            path=f"{self.get_file_name()}_grpc.go", content="\n".join(lines)
+        )
+
+    def _build_import_block(self, tracker: ImportTracker) -> List[str]:
+        imports = [
+            '"context"',
+            '"google.golang.org/grpc"',
+            '"google.golang.org/grpc/codes"',
+            '"google.golang.org/grpc/mem"',
+            '"google.golang.org/grpc/status"',
+            '"github.com/apache/fory/go/fory"',
+        ]
+
+        for alias, path in tracker._imports.items():
+            imports.append(f'{alias} "{path}"')
+
+        sorted_imports = sorted(set(imports))
+
+        lines = ["import ("]
+        for imp in sorted_imports:
+            lines.append(f"\t{imp}")
+        lines.append(")")
+        lines.append("")
+
+        return lines
+
+    def _resolve_go_type(self, named_type: NamedType, tracker: ImportTracker) 
-> str:
+        type_ref = self.schema.resolve_type_name(named_type.name)
+        type_def = self.schema.get_type(type_ref)
+        if type_def is not None and self.is_imported_type(type_def):
+            info = self._import_info_for_type(type_def)
+            if info:
+                alias, import_path, _ = info
+                tracker.add(alias, import_path)
+                return f"*{alias}.{type_ref}"
+        return f"*{type_ref}"
+
+    def _generate_client_interface(
+        self, service: Service, tracker: ImportTracker
+    ) -> List[str]:
+        lines: List[str] = []
+        lines.append(
+            f"// {service.name}Client is the client API for {service.name} 
service."
+        )
+        lines.append(f"type {service.name}Client interface {{")
+        for method in service.methods:
+            req_type = self._resolve_go_type(method.request_type, tracker)
+            res_type = self._resolve_go_type(method.response_type, tracker)
+            mode = streaming_mode(method)
+            if mode is StreamingMode.UNARY:
+                signature = f"(ctx context.Context, in {req_type}, opts 
...grpc.CallOption) ({res_type}, error)"
+                
lines.append(f"\t{self.to_pascal_case(method.name)}{signature}")
+            elif mode is StreamingMode.SERVER_STREAMING:
+                signature = f"(ctx context.Context, in {req_type}, opts 
...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client, 
error)"
+                
lines.append(f"\t{self.to_pascal_case(method.name)}{signature}")
+            else:
+                signature = f"(ctx context.Context, opts ...grpc.CallOption) 
({service.name}_{self.to_pascal_case(method.name)}Client, error)"
+                
lines.append(f"\t{self.to_pascal_case(method.name)}{signature}")
+
+        lines.append("}")
+        lines.append("")
+        return lines
+
+    def _generate_client_struct(self, service: Service) -> List[str]:
+        lines: List[str] = []
+        lines.append(f"type {self.to_camel_case(service.name)}Client struct 
{{")
+        lines.append("\tcc grpc.ClientConnInterface")
+        lines.append("\tfory *fory.Fory")
+        lines.append("}")
+        lines.append("")
+        return lines
+
+    def _generate_new_client(self, service: Service) -> List[str]:
+        lines: List[str] = []
+        lines.append(
+            f"func New{service.name}Client(cc grpc.ClientConnInterface, f 
*fory.Fory) {service.name}Client {{"
+        )
+        lines.append(
+            f"\treturn &{self.to_camel_case(service.name)}Client{{cc: cc, 
fory: f}}"
+        )
+        lines.append("}")
+        lines.append("")
+        return lines
+
+    def _generate_codec(self) -> List[str]:
+        lines: List[str] = []
+        lines.append(
+            "// CodecV2 implements grpc/encoding.CodecV2 using Fory 
serialization."
+        )
+        lines.append(
+            "// Pass a configured *fory.Fory instance with all message types 
registered."
+        )
+        lines.append("type CodecV2 struct {")
+        lines.append("\tFory *fory.Fory")
+        lines.append("}")
+        lines.append("")
+        lines.append(
+            "// Marshal serializes v with Fory. The result is copied before 
being handed"
+        )
+        lines.append(
+            "// to gRPC because Fory reuses its internal write buffer across 
calls —"
+        )
+        lines.append(
+            "// streaming handlers may buffer multiple frames before sending, 
and without"
+        )
+        lines.append("// a copy all frames would alias the last serialized 
value.")
+        lines.append("func (c CodecV2) Marshal(v any) (mem.BufferSlice, error) 
{")
+        lines.append("\tb, err := c.Fory.Marshal(v)")
+        lines.append("\tif err != nil {")
+        lines.append("\t\treturn nil, err")
+        lines.append("\t}")
+        lines.append("\tout := make([]byte, len(b))")
+        lines.append("\tcopy(out, b)")
+        lines.append("\treturn mem.BufferSlice{mem.NewBuffer(&out, nil)}, nil")
+        lines.append("}")
+        lines.append("")
+        lines.append(
+            "// Unmarshal deserializes the gRPC frame into v. Each buffer 
segment is"
+        )
+        lines.append(
+            "// copied into a fresh slice because the transport may reclaim 
the"
+        )
+        lines.append("// underlying memory before Fory finishes reading it.")
+        lines.append("func (c CodecV2) Unmarshal(data mem.BufferSlice, v any) 
error {")
+        lines.append("\tb := make([]byte, data.Len())")
+        lines.append("\tn := 0")
+        lines.append("\tfor _, buf := range data {")
+        lines.append("\t\tn += copy(b[n:], buf.ReadOnlyData())")
+        lines.append("\t}")
+        lines.append("\treturn c.Fory.Unmarshal(b, v)")
+        lines.append("}")
+        lines.append("")
+        lines.append(
+            '// Name returns "fory", the codec identifier used with 
grpc.ForceCodecV2.'
+        )
+        lines.append('func (CodecV2) Name() string { return "fory" }')
+        lines.append("")
+        return lines
+
+    def _generate_client_methods(
+        self, service: Service, tracker: ImportTracker
+    ) -> List[str]:
+        lines: List[str] = []
+        stream_index = 0
+        for method in service.methods:
+            req_type = self._resolve_go_type(method.request_type, tracker)
+            res_type = self._resolve_go_type(method.response_type, tracker)
+            mode = streaming_mode(method)
+            if mode is StreamingMode.UNARY:
+                lines.append(
+                    f"func (c *{self.to_camel_case(service.name)}Client) 
{self.to_pascal_case(method.name)}(ctx context.Context, in {req_type}, opts 
...grpc.CallOption) ({res_type}, error) {{"
+                )
+                lines.append(f"\tout := new({res_type[1:]})")
+                lines.append(
+                    "\tcallOpts := 
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)"
+                )
+                lines.append(
+                    f'\terr := c.cc.Invoke(ctx, 
"{self.get_grpc_method_path(service, method)}", in, out, callOpts...)'
+                )
+                lines.append("\tif err != nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\treturn out, nil")
+                lines.append("}")
+                lines.append("")
+            elif mode is StreamingMode.SERVER_STREAMING:
+                lines.append(
+                    f"func (c *{self.to_camel_case(service.name)}Client) 
{self.to_pascal_case(method.name)}(ctx context.Context, in {req_type}, opts 
...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client, 
error) {{"
+                )
+                lines.append(
+                    "\tcallOpts := 
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)"
+                )
+                lines.append(
+                    f'\tstream, err := c.cc.NewStream(ctx, 
&_{service.name}_serviceDesc.Streams[{stream_index}], 
"{self.get_grpc_method_path(service, method)}", callOpts...)'
+                )
+                lines.append("\tif err != nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append(
+                    f"\tx := 
&{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client{{stream}}"
+                )
+                lines.append("\tif err := x.SendMsg(in); err != nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\tif err := x.CloseSend(); err != nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\treturn x, nil")
+                lines.append("}")
+                lines.append("")
+                stream_index += 1
+            else:
+                lines.append(
+                    f"func (c *{self.to_camel_case(service.name)}Client) 
{self.to_pascal_case(method.name)}(ctx context.Context, opts 
...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client, 
error) {{"
+                )
+                lines.append(
+                    "\tcallOpts := 
append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)"
+                )
+                lines.append(
+                    f'\tstream, err := c.cc.NewStream(ctx, 
&_{service.name}_serviceDesc.Streams[{stream_index}], 
"{self.get_grpc_method_path(service, method)}", callOpts...)'
+                )
+                lines.append("\tif err != nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append(
+                    f"\treturn 
&{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client{{stream}},
 nil"
+                )
+                lines.append("}")
+                lines.append("")
+                stream_index += 1
+        return lines
+
+    def _generate_stream_types(
+        self, service: Service, tracker: ImportTracker
+    ) -> List[str]:
+        lines: List[str] = []
+        for method in service.methods:
+            req_type = self._resolve_go_type(method.request_type, tracker)
+            res_type = self._resolve_go_type(method.response_type, tracker)
+            mode = streaming_mode(method)
+            if mode is StreamingMode.UNARY:
+                continue
+            if mode is StreamingMode.CLIENT_STREAMING:
+                # type interface
+                lines.append(
+                    f"type 
{service.name}_{self.to_pascal_case(method.name)}Client interface {{"
+                )
+                lines.append(f"\tSend({req_type}) error")
+                lines.append(f"\tCloseAndRecv() ({res_type}, error)")
+                lines.append("\tgrpc.ClientStream")
+                lines.append("}")
+                lines.append("")
+
+                # struct
+                lines.append(
+                    f"type 
{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client 
struct {{"
+                )
+                lines.append("\tgrpc.ClientStream")
+                lines.append("}")
+                lines.append("")
+
+                # methods
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client) 
Send(m {req_type}) error {{"
+                )
+                lines.append("\treturn x.ClientStream.SendMsg(m)")
+                lines.append("}")
+                lines.append("")
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client) 
CloseAndRecv() ({res_type}, error) {{"
+                )
+                lines.append("\tif err := x.ClientStream.CloseSend(); err != 
nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append(f"\tm := new({res_type[1:]})")
+                lines.append("\tif err := x.ClientStream.RecvMsg(m); err != 
nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\treturn m, nil")
+                lines.append("}")
+                lines.append("")
+            elif mode is StreamingMode.SERVER_STREAMING:
+                # type interface
+                lines.append(
+                    f"type 
{service.name}_{self.to_pascal_case(method.name)}Client interface {{"
+                )
+                lines.append(f"\tRecv() ({res_type}, error)")
+                lines.append("\tgrpc.ClientStream")
+                lines.append("}")
+                lines.append("")
+
+                # struct
+                lines.append(
+                    f"type 
{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client 
struct {{"
+                )
+                lines.append("\tgrpc.ClientStream")
+                lines.append("}")
+                lines.append("")
+
+                # methods
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client) 
Recv() ({res_type}, error) {{"
+                )
+                lines.append(f"\tm := new({res_type[1:]})")
+                lines.append("\tif err := x.ClientStream.RecvMsg(m); err != 
nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\treturn m, nil")
+                lines.append("}")
+                lines.append("")
+            else:
+                # interface
+                lines.append(
+                    f"type 
{service.name}_{self.to_pascal_case(method.name)}Client interface {{"
+                )
+                lines.append(f"\tSend({req_type}) error")
+                lines.append(f"\tRecv() ({res_type}, error)")
+                lines.append("\tgrpc.ClientStream")
+                lines.append("}")
+                lines.append("")
+
+                # struct
+                lines.append(
+                    f"type 
{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client 
struct {{"
+                )
+                lines.append("\tgrpc.ClientStream")
+                lines.append("}")
+                lines.append("")
+
+                # methods
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client) 
Send(m {req_type}) error {{"
+                )
+                lines.append("\treturn x.ClientStream.SendMsg(m)")
+                lines.append("}")
+                lines.append("")
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Client) 
Recv() ({res_type}, error) {{"
+                )
+                lines.append(f"\tm := new({res_type[1:]})")
+                lines.append("\tif err := x.ClientStream.RecvMsg(m); err != 
nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\treturn m, nil")
+                lines.append("}")
+                lines.append("")
+        return lines
+
+    def _generate_server_interface(
+        self, service: Service, tracker: ImportTracker
+    ) -> List[str]:
+        lines: List[str] = []
+        lines.append(
+            f"// {service.name}Server is the server API for {service.name} 
service."
+        )
+        lines.append(
+            f"// All implementations must embed 
Unimplemented{service.name}Server"
+        )
+        lines.append("// for forward compatibility.")
+        lines.append(f"type {service.name}Server interface {{")
+        for method in service.methods:
+            req_type = self._resolve_go_type(method.request_type, tracker)
+            res_type = self._resolve_go_type(method.response_type, tracker)
+            mode = streaming_mode(method)
+            if mode is StreamingMode.UNARY:
+                lines.append(
+                    f"\t{self.to_pascal_case(method.name)}(context.Context, 
{req_type}) ({res_type}, error)"
+                )
+            elif mode is StreamingMode.SERVER_STREAMING:
+                lines.append(
+                    f"\t{self.to_pascal_case(method.name)}({req_type}, 
{service.name}_{self.to_pascal_case(method.name)}Server) error"
+                )
+            else:
+                lines.append(
+                    
f"\t{self.to_pascal_case(method.name)}({service.name}_{self.to_pascal_case(method.name)}Server)
 error"
+                )
+        lines.append(f"\tmustEmbedUnimplemented{service.name}Server()")
+        lines.append("}")
+        lines.append("")
+        return lines
+
+    def _generate_unimplemented_server(
+        self, service: Service, tracker: ImportTracker
+    ) -> List[str]:
+        lines: List[str] = []
+        lines.append(
+            f"// Unimplemented{service.name}Server must be embedded to have 
forward compatible implementation."
+        )
+        lines.append(f"type Unimplemented{service.name}Server struct {{}}")
+        lines.append("")
+        lines.append(
+            f"func (Unimplemented{service.name}Server) 
mustEmbedUnimplemented{service.name}Server() {{}}"
+        )
+        lines.append("")
+        for method in service.methods:
+            req_type = self._resolve_go_type(method.request_type, tracker)
+            res_type = self._resolve_go_type(method.response_type, tracker)
+            mode = streaming_mode(method)
+            if mode is StreamingMode.UNARY:
+                lines.append(
+                    f"func (Unimplemented{service.name}Server) 
{self.to_pascal_case(method.name)}(context.Context, {req_type}) ({res_type}, 
error) {{"
+                )
+                lines.append(
+                    f'\treturn nil, status.Errorf(codes.Unimplemented, "method 
{self.to_pascal_case(method.name)} not implemented")'
+                )
+                lines.append("}")
+                lines.append("")
+            elif mode is StreamingMode.SERVER_STREAMING:
+                lines.append(
+                    f"func (Unimplemented{service.name}Server) 
{self.to_pascal_case(method.name)}({req_type}, 
{service.name}_{self.to_pascal_case(method.name)}Server) error {{"
+                )
+                lines.append(
+                    f'\treturn status.Errorf(codes.Unimplemented, "method 
{self.to_pascal_case(method.name)} not implemented")'
+                )
+                lines.append("}")
+                lines.append("")
+            else:
+                lines.append(
+                    f"func (Unimplemented{service.name}Server) 
{self.to_pascal_case(method.name)}({service.name}_{self.to_pascal_case(method.name)}Server)
 error {{"
+                )
+                lines.append(
+                    f'\treturn status.Errorf(codes.Unimplemented, "method 
{self.to_pascal_case(method.name)} not implemented")'
+                )
+                lines.append("}")
+                lines.append("")
+        return lines
+
+    def _generate_register_server(self, service: Service) -> List[str]:
+        lines: List[str] = []
+        lines.append(
+            f"func Register{service.name}Server(s grpc.ServiceRegistrar, srv 
{service.name}Server) {{"
+        )
+        lines.append(f"\ts.RegisterService(&_{service.name}_serviceDesc, srv)")
+        lines.append("}")
+        lines.append("")
+        return lines
+
+    def _generate_server_stream_types(
+        self, service: Service, tracker: ImportTracker
+    ) -> List[str]:
+        lines: List[str] = []
+        for method in service.methods:
+            req_type = self._resolve_go_type(method.request_type, tracker)
+            res_type = self._resolve_go_type(method.response_type, tracker)
+            mode = streaming_mode(method)
+            if mode is StreamingMode.UNARY:
+                continue
+            elif mode is StreamingMode.CLIENT_STREAMING:
+                # type interface
+                lines.append(
+                    f"type 
{service.name}_{self.to_pascal_case(method.name)}Server interface {{"
+                )
+                lines.append(f"\tRecv() ({req_type}, error)")
+                lines.append(f"\tSendAndClose({res_type}) error")
+                lines.append("\tgrpc.ServerStream")
+                lines.append("}")
+                lines.append("")
+
+                # struct
+                lines.append(
+                    f"type 
{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server 
struct {{"
+                )
+                lines.append("\tgrpc.ServerStream")
+                lines.append("}")
+                lines.append("")
+
+                # methods
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server) 
Recv() ({req_type}, error) {{"
+                )
+                lines.append(f"\tm := new({req_type[1:]})")
+                lines.append("\tif err := x.ServerStream.RecvMsg(m); err != 
nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\treturn m, nil")
+                lines.append("}")
+                lines.append("")
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server) 
SendAndClose(m {res_type}) error {{"
+                )
+                lines.append("\treturn x.ServerStream.SendMsg(m)")
+                lines.append("}")
+                lines.append("")
+            elif mode is StreamingMode.SERVER_STREAMING:
+                # type interface
+                lines.append(
+                    f"type 
{service.name}_{self.to_pascal_case(method.name)}Server interface {{"
+                )
+                lines.append(f"\tSend({res_type}) error")
+                lines.append("\tgrpc.ServerStream")
+                lines.append("}")
+                lines.append("")
+
+                # struct
+                lines.append(
+                    f"type 
{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server 
struct {{"
+                )
+                lines.append("\tgrpc.ServerStream")
+                lines.append("}")
+                lines.append("")
+
+                # methods
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server) 
Send(m {res_type}) error {{"
+                )
+                lines.append("\treturn x.ServerStream.SendMsg(m)")
+                lines.append("}")
+                lines.append("")
+            else:
+                # type interface
+                lines.append(
+                    f"type 
{service.name}_{self.to_pascal_case(method.name)}Server interface {{"
+                )
+                lines.append(f"\tSend({res_type}) error")
+                lines.append(f"\tRecv() ({req_type}, error)")
+                lines.append("\tgrpc.ServerStream")
+                lines.append("}")
+                lines.append("")
+
+                # struct
+                lines.append(
+                    f"type 
{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server 
struct {{"
+                )
+                lines.append("\tgrpc.ServerStream")
+                lines.append("}")
+                lines.append("")
+
+                # methods
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server) 
Send(m {res_type}) error {{"
+                )
+                lines.append("\treturn x.ServerStream.SendMsg(m)")
+                lines.append("}")
+                lines.append("")
+                lines.append(
+                    f"func (x 
*{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server) 
Recv() ({req_type}, error) {{"
+                )
+                lines.append(f"\tm := new({req_type[1:]})")
+                lines.append("\tif err := x.ServerStream.RecvMsg(m); err != 
nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\treturn m, nil")
+                lines.append("}")
+                lines.append("")
+        return lines
+
+    def _generate_service_desc(
+        self, service: Service, tracker: ImportTracker
+    ) -> List[str]:
+        lines: List[str] = []
+        for method in service.methods:
+            req_type = self._resolve_go_type(method.request_type, tracker)
+            mode = streaming_mode(method)
+            # handlers
+            if mode is StreamingMode.UNARY:
+                lines.append(
+                    f"func 
_{service.name}_{self.to_pascal_case(method.name)}_Handler(srv interface{{}}, 
ctx context.Context, dec func(interface{{}}) error, interceptor 
grpc.UnaryServerInterceptor) (interface{{}}, error) {{"
+                )
+                lines.append(f"\tin := new({req_type[1:]})")
+                lines.append("\tif err := dec(in); err != nil {")
+                lines.append("\t\treturn nil, err")
+                lines.append("\t}")
+                lines.append("\tif interceptor == nil {")
+                lines.append(
+                    f"\t\treturn 
srv.({service.name}Server).{self.to_pascal_case(method.name)}(ctx, in)"
+                )
+                lines.append("\t}")
+                lines.append("\tinfo := &grpc.UnaryServerInfo{")
+                lines.append("\t\tServer:\tsrv,")
+                lines.append(
+                    f'\t\tFullMethod:\t"{self.get_grpc_method_path(service, 
method)}",'
+                )
+                lines.append("\t}")
+                lines.append(
+                    "\thandler := func(ctx context.Context, req interface{}) 
(interface{}, error) {"
+                )
+                lines.append(
+                    f"\t\treturn 
srv.({service.name}Server).{self.to_pascal_case(method.name)}(ctx, 
req.({req_type}))"
+                )
+                lines.append("\t}")
+                lines.append("\treturn interceptor(ctx, in, info, handler)")
+                lines.append("}")
+                lines.append("")
+            elif mode is StreamingMode.SERVER_STREAMING:
+                lines.append(
+                    f"func 
_{service.name}_{self.to_pascal_case(method.name)}_Handler(srv interface{{}}, 
stream grpc.ServerStream) error {{"
+                )
+                lines.append(f"\tm := new({req_type[1:]})")
+                lines.append("\tif err := stream.RecvMsg(m); err != nil {")
+                lines.append("\t\treturn err")
+                lines.append("\t}")
+                lines.append(
+                    f"\treturn 
srv.({service.name}Server).{self.to_pascal_case(method.name)}(m, 
&{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server{{stream}})"
+                )
+                lines.append("}")
+                lines.append("")
+            else:
+                lines.append(
+                    f"func 
_{service.name}_{self.to_pascal_case(method.name)}_Handler(srv interface{{}}, 
stream grpc.ServerStream) error {{"
+                )
+                lines.append(
+                    f"\treturn 
srv.({service.name}Server).{self.to_pascal_case(method.name)}(&{self.to_camel_case(service.name)}{self.to_pascal_case(method.name)}Server{{stream}})"
+                )
+                lines.append("}")
+                lines.append("")
+        # var
+        lines.append(f"var _{service.name}_serviceDesc = grpc.ServiceDesc{{")
+        lines.append(f'\tServiceName: 
"{self.get_grpc_service_name(service)}",')
+        lines.append(f"\tHandlerType: (*{service.name}Server)(nil),")
+        # unary type service descriptors
+        lines.append("\tMethods: []grpc.MethodDesc{")
+        lines.extend(self._generate_unary_type_desc(service))
+        lines.append("\t},")
+        # stream type service descriptors
+        lines.append("\tStreams: []grpc.StreamDesc{")
+        lines.extend(self._generate_stream_type_desc(service))
+        lines.append("\t},")
+        lines.append(f'\tMetadata: "{self.get_file_name()}.fdl",')
+        lines.append("}")
+        lines.append("")
+        return lines
+
+    def _generate_unary_type_desc(self, service: Service) -> List[str]:
+        lines: List[str] = []
+        for method in service.methods:
+            mode = streaming_mode(method)
+            if mode is StreamingMode.UNARY:
+                lines.append("\t\t{")
+                lines.append(
+                    f'\t\t\tMethodName:\t"{self.to_pascal_case(method.name)}",'
+                )
+                lines.append(
+                    
f"\t\t\tHandler:\t_{service.name}_{self.to_pascal_case(method.name)}_Handler,"
+                )
+                lines.append("\t\t},")
+        return lines
+
+    def _generate_stream_type_desc(self, service: Service) -> List[str]:
+        lines: List[str] = []
+        for method in service.methods:
+            mode = streaming_mode(method)
+            if mode is StreamingMode.UNARY:
+                continue
+            else:
+                lines.append("\t\t{")
+                lines.append(
+                    f'\t\t\tStreamName:\t"{self.to_pascal_case(method.name)}",'
+                )
+                lines.append(
+                    
f"\t\t\tHandler:\t_{service.name}_{self.to_pascal_case(method.name)}_Handler,"
+                )
+                if (
+                    mode is StreamingMode.CLIENT_STREAMING
+                    or mode is StreamingMode.BIDIRECTIONAL
+                ):
+                    lines.append("\t\t\tClientStreams:\ttrue,")
+                if (
+                    mode is StreamingMode.SERVER_STREAMING
+                    or mode is StreamingMode.BIDIRECTIONAL
+                ):
+                    lines.append("\t\t\tServerStreams:\ttrue,")
+                lines.append("\t\t},")
+        return lines
diff --git a/compiler/fory_compiler/tests/test_service_codegen.py 
b/compiler/fory_compiler/tests/test_service_codegen.py
index 2850974a4..58269d0f7 100644
--- a/compiler/fory_compiler/tests/test_service_codegen.py
+++ b/compiler/fory_compiler/tests/test_service_codegen.py
@@ -129,7 +129,7 @@ def 
test_service_definition_does_not_affect_message_codegen():
 def test_generate_services_returns_empty_list_for_unsupported_generators():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
     for generator_cls in GENERATOR_CLASSES:
-        if generator_cls in (JavaGenerator, PythonGenerator):
+        if generator_cls in (JavaGenerator, PythonGenerator, GoGenerator):
             continue
         options = GeneratorOptions(output_dir=Path("/tmp"))
         generator = generator_cls(schema, options)
@@ -218,6 +218,30 @@ def test_grpc_streaming_method_shapes():
     assert "self.client = channel.stream_unary(" in python
     assert "self.bidi = channel.stream_stream(" in python
 
+    go = next(iter(generate_service_files(schema, GoGenerator).values()))
+    assert "ClientStreams:\ttrue" in go
+    assert "ServerStreams:\ttrue" in go
+    assert "grpc.ClientStream" in go
+    assert "grpc.ServerStream" in go
+
+
+def test_go_grpc_service_codegen():
+    schema = parse_fdl(_GREETER_WITH_SERVICE)
+    files = generate_service_files(schema, GoGenerator)
+    assert len(files) == 1
+    content = next(iter(files.values()))
+    assert "func NewGreeterClient(" in content
+    assert "func RegisterGreeterServer(" in content
+    assert "type GreeterClient interface" in content
+    assert "type GreeterServer interface" in content
+    assert "type UnimplementedGreeterServer struct" in content
+    assert "CodecV2{Fory: c.fory}" in content
+    assert "type CodecV2 struct" in content
+    assert "func (c CodecV2) Marshal(v any) (mem.BufferSlice, error)" in 
content
+    assert "func (c CodecV2) Unmarshal(data mem.BufferSlice, v any) error" in 
content
+    assert '"/demo.greeter.Greeter/SayHello"' in content
+    assert "mustEmbedUnimplementedGreeterServer()" in content
+
 
 def test_java_outer_classname_service_references_nested_model_types():
     schema = parse_fdl(
diff --git a/integration_tests/grpc_tests/generate_grpc.py 
b/integration_tests/grpc_tests/generate_grpc.py
index 22ea0a446..5d3131640 100644
--- a/integration_tests/grpc_tests/generate_grpc.py
+++ b/integration_tests/grpc_tests/generate_grpc.py
@@ -33,6 +33,7 @@ SCHEMAS = [
 OUTPUTS = {
     "java": TEST_DIR / "java/src/main/java/generated",
     "python": TEST_DIR / "python/grpc_tests/generated",
+    "go": TEST_DIR / "go/generated",
 }
 
 
@@ -57,6 +58,12 @@ def main() -> int:
         )
 
     for schema in SCHEMAS:
+        # Each Go schema goes into its own subdir (named after the package) so
+        # multiple schemas with different package names can share the same
+        # generated/ root without one package's files overwriting another's.
+        pkg_name = schema.stem.replace(".", "_")
+        go_pkg_out = OUTPUTS["go"] / pkg_name
+        go_pkg_out.mkdir(parents=True, exist_ok=True)
         subprocess.check_call(
             [
                 sys.executable,
@@ -66,6 +73,7 @@ def main() -> int:
                 str(schema),
                 f"--java_out={OUTPUTS['java']}",
                 f"--python_out={OUTPUTS['python']}",
+                f"--go_out={go_pkg_out}",
                 "--grpc",
             ],
             env=env,
diff --git a/integration_tests/grpc_tests/go/go.mod 
b/integration_tests/grpc_tests/go/go.mod
new file mode 100644
index 000000000..461aef088
--- /dev/null
+++ b/integration_tests/grpc_tests/go/go.mod
@@ -0,0 +1,35 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+module github.com/apache/fory/integration_tests/grpc_tests/go
+
+go 1.25.0
+
+require (
+       github.com/apache/fory/go/fory v0.0.0
+       google.golang.org/grpc v1.68.0
+)
+
+require (
+       golang.org/x/net v0.29.0 // indirect
+       golang.org/x/sys v0.25.0 // indirect
+       golang.org/x/text v0.18.0 // indirect
+       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20240903143218-8af14fe29dc1 // indirect
+       google.golang.org/protobuf v1.34.2 // indirect
+)
+
+replace github.com/apache/fory/go/fory => ../../../go/fory
diff --git a/integration_tests/grpc_tests/go/go.sum 
b/integration_tests/grpc_tests/go/go.sum
new file mode 100644
index 000000000..177fd3c66
--- /dev/null
+++ b/integration_tests/grpc_tests/go/go.sum
@@ -0,0 +1,24 @@
+github.com/davecgh/go-spew v1.1.1 
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang/protobuf v1.5.4 
h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod 
h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod 
h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.7.0 
h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
+golang.org/x/text v0.18.0/go.mod 
h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 
h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
+google.golang.org/genproto/googleapis/rpc 
v0.0.0-20240903143218-8af14fe29dc1/go.mod 
h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
+google.golang.org/grpc v1.68.0/go.mod 
h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
+google.golang.org/protobuf v1.34.2 
h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod 
h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/integration_tests/grpc_tests/go/main.go 
b/integration_tests/grpc_tests/go/main.go
new file mode 100644
index 000000000..caa9e3909
--- /dev/null
+++ b/integration_tests/grpc_tests/go/main.go
@@ -0,0 +1,299 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Binary grpc-interop is the Go peer for Java-driven gRPC integration tests.
+// It is invoked as a subprocess by GrpcInteropTest.java and supports two 
modes:
+//
+//     server --port-file <path>  start a gRPC server and write the bound port 
to the file
+//     client --target <addr>     connect to addr and exercise all four 
streaming modes
+package main
+
+import (
+       "context"
+       "flag"
+       "fmt"
+       "io"
+       "log"
+       "net"
+       "os"
+       "strings"
+
+       "github.com/apache/fory/go/fory"
+       grpc_fdl 
"github.com/apache/fory/integration_tests/grpc_tests/go/generated/grpc_fdl"
+       "google.golang.org/grpc"
+       "google.golang.org/grpc/credentials/insecure"
+       "google.golang.org/grpc/encoding"
+)
+
+// --- helpers ----------------------------------------------------------------
+
+func newFory() *fory.Fory {
+       f := fory.New(fory.WithXlang(true), fory.WithRefTracking(true), 
fory.WithCompatible(true))
+       if err := grpc_fdl.RegisterTypes(f); err != nil {
+               log.Fatalf("RegisterTypes: %v", err)
+       }
+       return f
+}
+
+func fdlResponse(req *grpc_fdl.GrpcFdlRequest, tag string, offset int) 
*grpc_fdl.GrpcFdlResponse {
+       return &grpc_fdl.GrpcFdlResponse{
+               Id:      fmt.Sprintf("%s:%s", tag, req.Id),
+               Count:   req.Count + int32(offset),
+               Payload: fmt.Sprintf("%s:%s", tag, req.Payload),
+       }
+}
+
+func fdlAggregate(requests []*grpc_fdl.GrpcFdlRequest) 
*grpc_fdl.GrpcFdlResponse {
+       ids := make([]string, len(requests))
+       payloads := make([]string, len(requests))
+       var count int32
+       for i, req := range requests {
+               ids[i] = req.Id
+               payloads[i] = req.Payload
+               count += req.Count
+       }
+       return &grpc_fdl.GrpcFdlResponse{
+               Id:      "client:" + strings.Join(ids, "+"),
+               Count:   count,
+               Payload: "client:" + strings.Join(payloads, "+"),
+       }
+}
+
+// protoFallbackCodec overrides the built-in "proto" codec (v1 registry) with
+// Fory so the server can decode requests from Java clients, which send with 
the
+// default content-type (application/grpc) rather than application/grpc+fory.
+type protoFallbackCodec struct{ grpc_fdl.CodecV2 }
+
+func (protoFallbackCodec) Name() string { return "proto" }
+
+func (c protoFallbackCodec) Marshal(v interface{}) ([]byte, error) {
+       b, err := c.Fory.Marshal(v)
+       if err != nil {
+               return nil, err
+       }
+       out := make([]byte, len(b))
+       copy(out, b)
+       return out, nil
+}
+
+func (c protoFallbackCodec) Unmarshal(data []byte, v interface{}) error {
+       return c.Fory.Unmarshal(data, v)
+}
+
+// --- server -----------------------------------------------------------------
+
+type fdlService struct {
+       grpc_fdl.UnimplementedFdlGrpcServiceServer
+}
+
+func (s *fdlService) UnaryMessage(_ context.Context, req 
*grpc_fdl.GrpcFdlRequest) (*grpc_fdl.GrpcFdlResponse, error) {
+       return fdlResponse(req, "unary", 10), nil
+}
+
+func (s *fdlService) ServerStreamMessage(req *grpc_fdl.GrpcFdlRequest, stream 
grpc_fdl.FdlGrpcService_ServerStreamMessageServer) error {
+       for i := 0; i < 3; i++ {
+               if err := stream.Send(fdlResponse(req, fmt.Sprintf("server-%d", 
i), i)); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func (s *fdlService) ClientStreamMessage(stream 
grpc_fdl.FdlGrpcService_ClientStreamMessageServer) error {
+       var requests []*grpc_fdl.GrpcFdlRequest
+       for {
+               req, err := stream.Recv()
+               if err == io.EOF {
+                       return stream.SendAndClose(fdlAggregate(requests))
+               }
+               if err != nil {
+                       return err
+               }
+               requests = append(requests, req)
+       }
+}
+
+func (s *fdlService) BidiStreamMessage(stream 
grpc_fdl.FdlGrpcService_BidiStreamMessageServer) error {
+       index := 0
+       for {
+               req, err := stream.Recv()
+               if err == io.EOF {
+                       return nil
+               }
+               if err != nil {
+                       return err
+               }
+               if err := stream.Send(fdlResponse(req, fmt.Sprintf("bidi-%d", 
index), index)); err != nil {
+                       return err
+               }
+               index++
+       }
+}
+
+func runServer(portFile string, f *fory.Fory) error {
+       lis, err := net.Listen("tcp", "127.0.0.1:0")
+       if err != nil {
+               return fmt.Errorf("listen: %w", err)
+       }
+       codec := grpc_fdl.CodecV2{Fory: f}
+       // "fory" (v2): used by Go clients via grpc.ForceCodecV2.
+       encoding.RegisterCodecV2(codec)
+       // "proto" (v1): overrides the built-in proto codec so Java clients are
+       // also handled by Fory. RegisterCodecV2 writes to a separate registry 
and
+       // does not replace the v1 "proto" entry; RegisterCodec must be used.
+       encoding.RegisterCodec(protoFallbackCodec{codec})
+       s := grpc.NewServer()
+       grpc_fdl.RegisterFdlGrpcServiceServer(s, &fdlService{})
+
+       // Write the bound port so the Java test harness knows where to connect.
+       port := lis.Addr().(*net.TCPAddr).Port
+       if err := os.WriteFile(portFile, []byte(fmt.Sprintf("%d", port)), 
0600); err != nil {
+               return fmt.Errorf("write port file: %w", err)
+       }
+
+       return s.Serve(lis)
+}
+
+// --- client -----------------------------------------------------------------
+
+func exerciseMessageStub(stub grpc_fdl.FdlGrpcServiceClient, requests 
[]*grpc_fdl.GrpcFdlRequest) error {
+       ctx := context.Background()
+       first := requests[0]
+
+       // unary
+       got, err := stub.UnaryMessage(ctx, first)
+       if err != nil {
+               return fmt.Errorf("UnaryMessage: %w", err)
+       }
+       if want := fdlResponse(first, "unary", 10); *got != *want {
+               return fmt.Errorf("UnaryMessage: got %+v, want %+v", got, want)
+       }
+
+       // server streaming
+       ss, err := stub.ServerStreamMessage(ctx, first)
+       if err != nil {
+               return fmt.Errorf("ServerStreamMessage: %w", err)
+       }
+       for i := 0; i < 3; i++ {
+               got, err := ss.Recv()
+               if err != nil {
+                       return fmt.Errorf("ServerStreamMessage Recv[%d]: %w", 
i, err)
+               }
+               if want := fdlResponse(first, fmt.Sprintf("server-%d", i), i); 
*got != *want {
+                       return fmt.Errorf("ServerStreamMessage[%d]: got %+v, 
want %+v", i, got, want)
+               }
+       }
+       if _, err := ss.Recv(); err != io.EOF {
+               return fmt.Errorf("ServerStreamMessage: expected EOF, got %v", 
err)
+       }
+
+       // client streaming
+       cs, err := stub.ClientStreamMessage(ctx)
+       if err != nil {
+               return fmt.Errorf("ClientStreamMessage: %w", err)
+       }
+       for _, req := range requests {
+               if err := cs.Send(req); err != nil {
+                       return fmt.Errorf("ClientStreamMessage Send: %w", err)
+               }
+       }
+       csResp, err := cs.CloseAndRecv()
+       if err != nil {
+               return fmt.Errorf("ClientStreamMessage CloseAndRecv: %w", err)
+       }
+       if want := fdlAggregate(requests); *csResp != *want {
+               return fmt.Errorf("ClientStreamMessage: got %+v, want %+v", 
csResp, want)
+       }
+
+       // bidirectional streaming
+       bidi, err := stub.BidiStreamMessage(ctx)
+       if err != nil {
+               return fmt.Errorf("BidiStreamMessage: %w", err)
+       }
+       for i, req := range requests {
+               if err := bidi.Send(req); err != nil {
+                       return fmt.Errorf("BidiStreamMessage Send[%d]: %w", i, 
err)
+               }
+               got, err := bidi.Recv()
+               if err != nil {
+                       return fmt.Errorf("BidiStreamMessage Recv[%d]: %w", i, 
err)
+               }
+               if want := fdlResponse(req, fmt.Sprintf("bidi-%d", i), i); *got 
!= *want {
+                       return fmt.Errorf("BidiStreamMessage[%d]: got %+v, want 
%+v", i, got, want)
+               }
+       }
+       if err := bidi.CloseSend(); err != nil {
+               return fmt.Errorf("BidiStreamMessage CloseSend: %w", err)
+       }
+       if _, err := bidi.Recv(); err != io.EOF {
+               return fmt.Errorf("BidiStreamMessage: expected EOF after 
CloseSend, got %v", err)
+       }
+
+       return nil
+}
+
+func runClient(target string, f *fory.Fory) error {
+       conn, err := grpc.NewClient(target, 
grpc.WithTransportCredentials(insecure.NewCredentials()))
+       if err != nil {
+               return fmt.Errorf("dial %s: %w", target, err)
+       }
+       defer conn.Close()
+
+       stub := grpc_fdl.NewFdlGrpcServiceClient(conn, f)
+       requests := []*grpc_fdl.GrpcFdlRequest{
+               {Id: "fdl-a", Count: 1, Payload: "alpha"},
+               {Id: "fdl-b", Count: 2, Payload: "beta"},
+       }
+       return exerciseMessageStub(stub, requests)
+}
+
+// --- entry point ------------------------------------------------------------
+
+func main() {
+       serverCmd := flag.NewFlagSet("server", flag.ExitOnError)
+       serverPortFile := serverCmd.String("port-file", "", "path to write 
bound port")
+
+       clientCmd := flag.NewFlagSet("client", flag.ExitOnError)
+       clientTarget := clientCmd.String("target", "", "host:port to connect 
to")
+
+       if len(os.Args) < 2 {
+               log.Fatal("usage: grpc-interop <server|client> [flags]")
+       }
+
+       f := newFory()
+
+       switch os.Args[1] {
+       case "server":
+               serverCmd.Parse(os.Args[2:])
+               if *serverPortFile == "" {
+                       log.Fatal("--port-file is required")
+               }
+               if err := runServer(*serverPortFile, f); err != nil {
+                       log.Fatalf("server error: %v", err)
+               }
+       case "client":
+               clientCmd.Parse(os.Args[2:])
+               if *clientTarget == "" {
+                       log.Fatal("--target is required")
+               }
+               if err := runClient(*clientTarget, f); err != nil {
+                       log.Fatalf("client error: %v", err)
+               }
+       default:
+               log.Fatalf("unknown command %q", os.Args[1])
+       }
+}
diff --git 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
index 791c2a6a3..c494c6dc2 100644
--- 
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
+++ 
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java
@@ -111,6 +111,18 @@ public class GrpcInteropTest {
     assertFdlUnions(blocking, async, unions);
   }
 
+  // exerciseFdlMessages exercises the four FDL message streaming modes 
without union methods.
+  private void exerciseFdlMessages(ManagedChannel channel) throws 
InterruptedException {
+    grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceBlockingStub blocking =
+        grpc_fdl.FdlGrpcServiceGrpc.newBlockingStub(channel);
+    grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceStub async =
+        grpc_fdl.FdlGrpcServiceGrpc.newStub(channel);
+
+    List<grpc_fdl.GrpcFdlRequest> messages =
+        Arrays.asList(fdlRequest("fdl-a", 1, "alpha"), fdlRequest("fdl-b", 2, 
"beta"));
+    assertFdlMessages(blocking, async, messages);
+  }
+
   private void assertFdlMessages(
       grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceBlockingStub blocking,
       grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceStub async,
@@ -266,6 +278,88 @@ public class GrpcInteropTest {
             pbResponse(requests.get(0), "bidi-0", 0), 
pbResponse(requests.get(1), "bidi-1", 1)));
   }
 
+  @Test
+  public void testJavaServerGoClient() throws Exception {
+    Server server =
+        ServerBuilder.forPort(0)
+            .addService(new FdlService())
+            .build()
+            .start();
+    try {
+      runGo("go-grpc-client", "client", "--target", "127.0.0.1:" + 
server.getPort());
+    } finally {
+      server.shutdownNow();
+      server.awaitTermination(10, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testGoServerJavaClient() throws Exception {
+    Path portFile = Files.createTempFile("fory-grpc-go-", ".port");
+    Files.deleteIfExists(portFile);
+    PeerCommand command = goCommand("server", "--port-file", 
portFile.toString());
+    Process process = startPeer(command);
+    PeerOutputCollector outputCollector =
+        new PeerOutputCollector(process.getInputStream(), "go-grpc-server");
+    outputCollector.start();
+    try {
+      int port = waitForPort(process, outputCollector, portFile);
+      ManagedChannel channel =
+          ManagedChannelBuilder.forAddress("127.0.0.1", 
port).usePlaintext().build();
+      try {
+        exerciseFdlMessages(channel);
+      } finally {
+        channel.shutdownNow();
+        channel.awaitTermination(10, TimeUnit.SECONDS);
+      }
+    } finally {
+      process.destroy();
+      process.waitFor(10, TimeUnit.SECONDS);
+      if (process.isAlive()) {
+        process.destroyForcibly();
+        process.waitFor(10, TimeUnit.SECONDS);
+      }
+      outputCollector.awaitOutput();
+      Files.deleteIfExists(portFile);
+    }
+  }
+
+  private PeerCommand goCommand(String... args) {
+    Path grpcRoot = 
repoRoot().resolve("integration_tests").resolve("grpc_tests");
+    Path goRoot = grpcRoot.resolve("go");
+    List<String> command = new ArrayList<>();
+    command.add(goRoot.resolve("grpc-interop").toString());
+    command.addAll(Arrays.asList(args));
+    PeerCommand peerCommand = new PeerCommand();
+    peerCommand.command = command;
+    peerCommand.workDir = goRoot;
+    peerCommand.environment.put("NO_PROXY", "127.0.0.1,localhost");
+    peerCommand.environment.put("no_proxy", "127.0.0.1,localhost");
+    return peerCommand;
+  }
+
+  private void runGo(String peer, String... args) throws IOException, 
InterruptedException {
+    Process process = startPeer(goCommand(args));
+    PeerOutputCollector outputCollector = new 
PeerOutputCollector(process.getInputStream(), peer);
+    outputCollector.start();
+    boolean finished = process.waitFor(180, TimeUnit.SECONDS);
+    if (!finished) {
+      process.destroyForcibly();
+      process.waitFor(10, TimeUnit.SECONDS);
+      Assert.fail("Peer process timed out for " + peer + 
peerOutput(outputCollector));
+    }
+    int exitCode = process.exitValue();
+    if (exitCode != 0) {
+      Assert.fail(
+          "Peer process failed for "
+              + peer
+              + " with exit code "
+              + exitCode
+              + peerOutput(outputCollector));
+    }
+    outputCollector.awaitOutput();
+  }
+
   private PeerCommand pythonCommand(String... args) {
     Path repoRoot = repoRoot();
     Path grpcRoot = 
repoRoot.resolve("integration_tests").resolve("grpc_tests");
diff --git a/integration_tests/grpc_tests/run_tests.sh 
b/integration_tests/grpc_tests/run_tests.sh
index 7cb2a4ad6..033d27c56 100755
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/run_tests.sh
@@ -27,7 +27,9 @@ python -m pip install -v -e "${ROOT_DIR}/python"
 
 python "${SCRIPT_DIR}/generate_grpc.py"
 
+cd "${SCRIPT_DIR}/go"
+go build -o grpc-interop .
 cd "${ROOT_DIR}/integration_tests/grpc_tests/java"
-ENABLE_FORY_DEBUG_OUTPUT=1 mvn -T16 --no-transfer-progress \
+mvn -T16 --no-transfer-progress \
   -Dtest=GrpcInteropTest \
   test


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to