使用Gone V2 的Provide机制改造goner/grpc组件

Gone v2 项目地址:https://github.com/gone-io/gone
本文原文:https://github.com/gone-io/goner/blob/main/grpc/use-provide.md
本文用到的代码开源于:https://github.com/gone-io/goner/tree/main/grpc

Gone V2 Provider 机制介绍中我们详细介绍了Gone框架的Provide机制及其强大的依赖注入能力。理论讲解固然重要,但真正理解一个机制的价值,还需要通过实践来检验。本文将通过一个实际案例,展示如何利用Provide机制对goner/grpc组件进行改造,让我们亲身体验Gone V2框架在简化代码、提升开发体验方面的巨大潜力。

现有goner/grpc组件的使用痛点

goner/grpc组件的使用体验一直不够自然流畅。让我们先看看现有的实现方式,分析其中的痛点。

服务端示例代码

服务端实现服务时,需要通过实现RegisterGrpcServer接口方法来注册服务:

// server/main.go
package main

import (
	"context"
	"github.com/gone-io/gone/v2"
	goneGrpc "github.com/gone-io/goner/grpc"
	"google.golang.org/grpc"
	"grpc_demo/proto"
	"log"
	"os"
)

type server struct {
	gone.Flag
	proto.UnimplementedHelloServer // 嵌入UnimplementedHelloServer
}

// 重载协议中定义的服务
func (s *server) Say(ctx context.Context, in *proto.SayRequest) (*proto.SayResponse, error) {
	log.Printf("Received: %v", in.GetName())
	return &proto.SayResponse{Message: "Hello " + in.GetName()}, nil
}

// 实现 goneGrpc.Service接口的RegisterGrpcServer方法,该方法在服务器启动时会被自动调用
func (s *server) RegisterGrpcServer(server *grpc.Server) {
	proto.RegisterHelloServer(server, s)
}

func main() {
	// gone内置默认的配置组件只能从环境变量中读取配置,所以需要设置环境变量
	os.Setenv("GONE_SERVER_GRPC_PORT", "9091")

	gone.
		Load(&server{}).
		Loads(goneGrpc.ServerLoad).
		// 启动服务
		Serve()
}

客户端示例代码

客户端使用服务时,需要通过实现AddressStub方法来初始化连接:

package main

import (
	"context"
	"fmt"
	"github.com/gone-io/gone/v2"
	"github.com/gone-io/goner"
	gone_grpc "github.com/gone-io/goner/grpc"
	"google.golang.org/grpc"
	"grpc_demo/proto"
	"log"
)

type helloClient struct {
	gone.Flag
	proto.HelloClient // 嵌入HelloClient

	host string `gone:"config,server.host"`
	port string `gone:"config,server.port"`
}

// 实现 gone_grpc.Client接口的Address方法,该方法在客户端启动时会被自动调用
// 该方法的作用是告诉客户端gRPC服务的地址
func (c *helloClient) Address() string {
	return fmt.Sprintf("%s:%s", c.host, c.port)
}

// 实现 gone_grpc.Client接口的Stub方法,该方法在客户端启动时会被自动调用
// 在该方法中,完成 HelloClient的初始化
func (c *helloClient) Stub(conn *grpc.ClientConn) {
	c.HelloClient = proto.NewHelloClient(conn)
}

func main() {
	gone.
		Load(&helloClient{}).
		Loads(goner.BaseLoad, gone_grpc.ClientRegisterLoad).
		Run(func(in struct {
			hello *helloClient `gone:"*"` // 在Run方法的参数中,注入 helloClient
		}) {
			// 调用Say方法,给服务段发送消息
			say, err := in.hello.Say(context.Background(), &proto.SayRequest{Name: "gone"})
			if err != nil {
				log.Printf("er:%v", err)
				return
			}
			log.Printf("say result: %s", say.Message)
		})
}

问题总结

通过分析上述代码,我们可以总结出以下几个主要痛点:

  1. 手动注册机制违背DI原则

    • 服务端需要显式实现RegisterGrpcServer接口,这与Gone框架"自动装配"的核心理念相悖
    • 开发者需要手动管理gRPC服务的注册过程,而理想的依赖注入框架应该通过标签或约定自动完成这种绑定
  2. 客户端实现存在大量样板代码

    • 每个gRPC客户端都需要实现相同模式的Address()Stub()方法
    • 这些重复性的模板代码与Gone V2通过Provider机制消除重复代码的设计目标不符
    • 配置获取方式不够灵活,地址构建逻辑需要手写

这些问题导致开发者在使用gRPC组件时体验不佳,不符合Gone框架简洁易用的设计理念。

服务端改造方案

改造后的服务端业务代码

针对服务端的痛点,我们的改造目标是:

  1. 使*grpc.Server能够自动注入,不再需要实现RegisterGrpcServer方法
  2. 将服务注册流程放到Init方法中,使其更符合直觉和Gone的生命周期机制

改造后的服务端业务代码如下:

type server struct {
	gone.Flag
	proto.UnimplementedHelloServer              // 嵌入UnimplementedHelloServer
	grpcServer                     *grpc.Server `gone:"*"` // 注入grpc.Server
}

func (s *server) Init() {
	proto.RegisterHelloServer(s.grpcServer, s) //在Init方法中完成服务注册
}

// Say 重载协议中定义的服务
func (s *server) Say(ctx context.Context, in *proto.SayRequest) (*proto.SayResponse, error) {
	log.Printf("Received: %v", in.GetName())
	return &proto.SayResponse{Message: "Hello " + in.GetName()}, nil
}

这种改造方式带来了以下几个明显的好处:

  • 代码更加简洁,移除了不必要的接口实现
  • 更符合依赖注入的思想,通过标签自动注入grpc.Server
  • 服务注册逻辑放在Init方法中,符合Gone的组件生命周期管理

goner/grpc/server.go的改造

为了支持上述服务端业务代码的改造,我们需要对goner/grpc/server.go进行相应的修改。改造前的完整代码可以在v0.0.6/grpc/server.go查看,改造后的完整代码在grpc/server.go

主要改造点包括:

  1. 给server结构体增加Provide方法,使其成为一个Provider
  2. server.grpcServer初始化的代码放到Init方法中
type server struct {
	gone.Flag
	//...
	grpcServer     *grpc.Server
	listener       net.Listener
	//...
}

func (s *server) Init() error {
	err := s.initListener()
	if err != nil {
		return gone.ToError(err)
	}

	s.grpcServer = grpc.NewServer(
		grpc.ChainUnaryInterceptor(
			s.traceInterceptor,
			s.recoveryInterceptor,
		),
	)
	return nil
}

func (s *server) Provide() (*grpc.Server, error) {
	return s.grpcServer, nil
}

通过这种改造,gRPC服务器组件现在能够作为一个Provider向其他组件提供*grpc.Server实例,极大地简化了服务注册流程。

客户端改造方案

改造后的客户端业务代码

针对客户端的痛点,我们的改造目标是:

  1. 不再需要实现StubAddress方法
  2. 自动注入*grpc.ClientConn
  3. Init方法中完成Client的初始化
  4. 支持灵活的配置方式,自动从配置中读取服务地址

改造后的客户端业务代码如下:

package main

import (
	"context"
	"github.com/gone-io/gone/v2"
	gone_grpc "github.com/gone-io/goner/grpc"
	"google.golang.org/grpc"
	"grpc_demo/proto"
	"log"
	"os"
)

type helloClient struct {
	gone.Flag
	proto.HelloClient // 使用方法1:嵌入HelloClient,本组件只负责初始化,能力提供给第三方组件使用
	
	// 使用方法2:在本组件直接使用,不提供给第三方组件使用
	//hello *proto.HelloClient

	// config=${配置的key},address=${服务地址};config优先级更高
	clientConn *grpc.ClientConn `gone:"*,config=grpc.service.hello.address"`
	
	// config和address可以一起使用,如果config没有读取到值,降级为使用address
	//clientConn1 *grpc.ClientConn `gone:"*,config=grpc.service.hello.address,address=127.0.0.1:9090""`

	// address也可以单独使用,不推荐这种方式,意味着地址硬编码
	//clientConn2 *grpc.ClientConn `gone:"*,address=127.0.0.1:9090"`
}

func (c *helloClient) Init() {
	c.HelloClient = proto.NewHelloClient(c.clientConn)
}

func main() {
	// gone内置默认的配置组件只能从环境变量中读取配置,所以需要设置环境变量
	os.Setenv("GONE_GRPC_SERVICE_HELLO_ADDRESS", "127.0.0.1:9091")

	gone.
		Load(&helloClient{}).
		Loads(gone_grpc.ClientRegisterLoad).
		Run(func(in struct {
			hello *helloClient `gone:"*"` // 在Run方法的参数中,注入 helloClient
		}) {
			// 调用Say方法,给服务端发送消息
			say, err := in.hello.Say(context.Background(), &proto.SayRequest{Name: "gone"})
			if err != nil {
				log.Printf("er:%v", err)
				return
			}
			log.Printf("say result: %s", say.Message)
		})
}

这种改造方式带来了以下几个明显的好处:

  • 代码更加简洁,移除了不必要的接口实现
  • 提供了多种灵活的配置方式,包括:
    • 仅从配置中读取地址
    • 配置与默认地址配合使用,实现降级策略
    • 直接硬编码地址(不推荐,但支持)
  • 符合Gone框架的组件生命周期管理,在Init方法中完成初始化

goner/grpc/client.go的改造

为了支持上述客户端业务代码的改造,我们需要对goner/grpc/client.go进行相应的修改。改造前的完整代码可以在v0.0.6/grpc/client.go查看,改造后的完整代码在grpc/client.go

主要改造点包括:

  1. clientRegister结构体增加Provide方法,使其成为一个Provider,能够根据注入标签自动创建*grpc.ClientConn
  2. clientRegister上注入gone.Configure,用于根据配置键获取服务地址
  3. 实现连接缓存机制,相同地址的服务复用同一个*grpc.ClientConn,提高性能

主要代码如下:

type clientRegister struct {
	gone.Flag
	// ...
	configure gone.Configure `gone:"configure"`
	connections map[string]*grpc.ClientConn
	// ...
}

func (s *clientRegister) getConn(address string) (conn *grpc.ClientConn, err error) {
	conn = s.connections[address]
	if conn == nil {
		if conn, err = grpc.NewClient(
			address,
			grpc.WithTransportCredentials(insecure.NewCredentials()),
			grpc.WithChainUnaryInterceptor(s.traceInterceptor),
		); err != nil {
			return nil, gone.ToError(err)
		}
		s.connections[address] = conn
	}
	return
}

// Provide 实现Provider接口,根据标签配置提供grpc.ClientConn
func (s *clientRegister) Provide(tagConf string) (*grpc.ClientConn, error) {
	m, _ := gone.TagStringParse(tagConf)
	address := m["address"]
	if configKey, ok := m["config"]; ok {
		err := s.configure.Get(configKey, &address, address)
		if err != nil {
			return nil, gone.ToError(err)
		}
	}
	if address == "" {
		return nil, gone.ToError("address is empty")
	}
	return s.getConn(address)
}

通过这种改造,gRPC客户端组件现在能够:

  • 解析注入标签中的配置
  • 灵活获取服务地址(支持配置优先或地址优先)
  • 缓存连接以提高性能
  • 自动提供grpc.ClientConn实例给需要的组件

总结

通过本次改造,我们利用Gone V2的Provide机制大幅提升了goner/grpc组件的使用体验:

  1. 服务端改进

    • 移除了手动注册机制,改为自动注入
    • 使用标准的Init方法进行服务注册,符合直觉
    • 服务实现代码更加简洁明了
  2. 客户端改进

    • 移除了重复性的模板代码(Address和Stub方法)
    • 提供灵活的配置方式,支持多种地址获取策略
    • 实现连接缓存,提高性能和资源利用率
  3. 整体收益

    • 代码更符合依赖注入的理念
    • 减少了样板代码,提高开发效率
    • 更加符合Gone框架"约定优于配置"的设计理念

这次改造充分展示了Gone V2的Provider机制在简化组件使用、提升开发体验方面的强大潜力。开发者现在可以用更自然、更直观的方式使用gRPC功能,专注于业务逻辑而非框架细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dapeng-大鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值