Hertz的优缺点

优点

  • 中文社区维护
  • 稳定,字节产品,有相关的开源项目参考,比如coze和coze studio
  • hz的模板支持,使得代码生成更加灵活

缺点

  • 工程体验上,不如go-kratos等项目。比如不能很好得支持buf等第三方生态。代码生成的使用场景,如多service模式下体验不好。

Template 模板

之所以将模板放在第一,是因为Hertz里面模板是过不去的坎。😄官方的一个mvc的template 或者 cwgo内置的模板

自动生成路由注册

新建一个template/package.yaml

layouts:
  # 覆盖默认 router.go 模板
  # 关键:将 Register 函数名改为包含服务名的唯一名称
  - path: router.go
    delims: ['{{', '}}']
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package {{$.PackageName}}

      import (
          "github.com/cloudwego/hertz/pkg/app/server"

      {{- range $k, $v := .HandlerPackages}}
          {{$k}} "{{$v}}"
      {{- end}}
      )

      /*
       This file will register all the routes of the services in the master idl.
       And it will update automatically when you use the "update" command for the idl.
       So don't modify the contents of the file, or your code will be deleted when it is updated.
      */

      {{define "g"}}
      {{- if eq .Path "/"}}r
      {{- else}}{{.GroupName}}{{end}}
      {{- end}}

      {{define "G"}}
      {{- if ne .Handler ""}}
          {{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.HandlerMiddleware}}Mw(), {{.Handler}})...)
      {{- end}}
      {{- if ne (len .Children) 0}}
      {{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.GroupMiddleware}}Mw()...)
      {{- end}}
      {{- range $_, $router := .Children}}
      {{- if ne .Handler ""}}
          {{template "G" $router}}
      {{- else}}
          {	{{template "G" $router}}
          }
      {{- end}}
      {{- end}}
      {{- end}}

      // Register register routes based on the IDL 'api.${HTTP Method}' annotation.
      func Register(r *server.Hertz) {
      {{template "G" .Router}}
      }      

  # 覆盖 register.go 模板
  # 在这个文件中聚合所有服务的 Register 调用
  - path: register.go
    delims: ['{{', '}}']
    update_behavior:
      type: "append"
      append_key: "service"
      insert_key: "{{.ServiceName}}"
      append_tpl: |
        {{$.DepPkgAlias}}.Register(r)        
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package {{.PackageName}}

      import (
          "github.com/cloudwego/hertz/pkg/app/server"
          {{$.DepPkgAlias}} "{{$.DepPkg}}"
      )

      // GeneratedRegister registers routers generated by IDL.
      func GeneratedRegister(r *server.Hertz){
          //INSERT_POINT: DO NOT DELETE THIS LINE!
          {{$.DepPkgAlias}}.Register(r)
      }      

然后service proto都定义为不同的go package包, admin.proto

syntax = "proto3";
package api.v1.admin;

option go_package = "app/biz/model/api/v1/admin";

user.proto

syntax = "proto3";
package api.v1.user;

option go_package = "app/biz/model/api/v1/user";

使用下面的bash脚本进行编译

#!/bin/bash

set -e

echo "🚀 开始生成所有 proto 文件..."

# 定义所有服务的 proto 文件
declare -A SERVICES=(
  ["user"]="proto/api/v1/user.proto"
  ["admin"]="proto/api/v1/admin.proto"
)

# 清理旧的生成文件(可选)
# echo "🧹 清理旧的生成文件..."
# rm -rf biz/handler biz/router

# 逐个生成每个服务
for service in "${!SERVICES[@]}"; do
  proto_file="${SERVICES[$service]}"
  echo "📦 处理 $service: $proto_file"

  hz update \
    -I . \
    -I proto \
    --idl "$proto_file" \
    --model_dir biz/model \
    --handler_dir biz/handler \
    --customize_package template/package.yaml
done

中间件白名单

在hertz里面,没有其他框架,比如kratos的whitelist的概念(通过路径来设定是否跳过某些中间件),在hertz里面,可以通过生成代码里面的biz/router里面的方式来实现,比如我的用户管理模块,我可能注册和登录不需要jwt,但是我的修改密码等地方就需要

我这个是因为api都在同一个组下面,如果整个组都需要jwt,则只需要加在组路由下就好了。

func _changepasswordMw() []app.HandlerFunc {
	// Change password requires authentication
	return []app.HandlerFunc{middleware.JwtMiddleware.MiddlewareFunc()}
}

func _loginMw() []app.HandlerFunc {
	// Login doesn't require authentication
	return nil
}

func _logoutMw() []app.HandlerFunc {
	// Logout requires authentication
	return []app.HandlerFunc{middleware.JwtMiddleware.MiddlewareFunc()}
}

func _refreshtokenMw() []app.HandlerFunc {
	// Refresh token requires authentication
	return []app.HandlerFunc{middleware.JwtMiddleware.MiddlewareFunc()}
}

Protoc插件

API文档

swagger文档

可以使用社区的插件 protoc-gen-http-swagger

OpenAPI

hz集成protoc-gen-openapi来生成openapi文档时,你还需要做以下工作:

  1. 创建proto文件时,引入openapiv3/annotations.proto和google/api/annotations.proto,运行时通过-I参数指定引入路径

你可以从以下仓库搜集: protobuf/src/google/protobuf at main · protocolbuffers/protobuf googleapis/google/api at master · googleapis/googleapis gnostic/openapiv3 at main · google/gnostic 完整的引入如下所示:

google
├── api
│   ├── annotations.proto
│   └── http.proto
├── protobuf
│   ├── any.proto
│   └── descriptor.proto
openapiv3
├── annotations.proto
└── OpenAPIv3.proto
  1. 编写 idl 文件时,写入 openapi 相关注解,更多的例子可以查看protoc-gen-openapi/examples 以下是你的proto的一个示例:
syntax = "proto3";

package api;

option go_package = "server/cmd/api";

import "hz.proto";
import "openapiv3/annotations.proto";
import "google/api/annotations.proto";

message LoginRequest {
  string code = 1[
    (api.vd) = "len($) > 0",
    (openapi.v3.property) = {
      min_length: 1;
    }
  ];
}

message LoginResponse {
  string token = 1;
  int64 expired_at = 2[(api.body) = "expired_at"];
}

message UserInfo{
  int64 account_id = 1;
  string username = 2;
  int64 phone_number = 3;
  string avatar_url = 4;
}

message GetUserInfoRequest{}

message UpdateUserRequest{
  string username = 1;
  int64 phone_number = 2;
}

message UpdateUserResponse{
}


service apiService {
  rpc Login (LoginRequest) returns (LoginResponse) {
    option (api.post) = "/auth/login";
    option(google.api.http) = {
      post: "/auth/login"
    };
  }
  rpc GetUserInfo (GetUserInfoRequest) returns (UserInfo) {
    option (api.get) = "/auth/info";
    option(google.api.http) = {
      get: "/auth/info"
    };
  }
}

此时可以生成正确的openapi文档。生成的 api 文档会自动把 请求参数和响应结果转换为驼峰的格式,如果要生成 snake 下划线连接的格式。可以尝试使用下面的参数:

api:
    protoc --proto_path=./api \
...
           --openapi_out=fq_schema_naming=true,naming=proto,default_response=false:. \
...

来源于这个issue 使用 protoc-gen-openapi 无法生成 api 文档