错误码
Cherry 的「错误」分两条独立的线,不要混用:
| 包 | 用途 | 取值范围 | |
|---|---|---|---|
| 状态码 | github.com/cherry-game/cherry/code | 框架内部与业务返回给客户端的数值状态 | int32 |
| error | github.com/cherry-game/cherry/error | Go 原生 error,用于函数返回值的错误传递 | — |
状态码是给客户端看的,error 是给 Go 代码看的。
状态码
import ccode "github.com/cherry-game/cherry/code"框架状态码
框架自己会产生这些码,覆盖会话、RPC、Actor 三类失败:
| 码 | 常量 | 含义 |
|---|---|---|
| 0 | OK | 成功 |
| 10 | SessionUIDNotBind | session 未绑定 UID |
| 11 | DiscoveryNotFoundNode | 发现服务中找不到目标节点 |
| 12 | NodeRequestError | 节点请求失败 |
| 20 | RPCNetError | 网络错误 |
| 21 | RPCUnmarshalError | 反序列化错误 |
| 22 | RPCMarshalError | 序列化错误 |
| 23 | RPCRemoteExecuteError | 远程方法执行出错 |
| 24 | ActorPathIsNil | 目标 path 为空 |
| 25 | ActorFuncNameError | 函数名非法 |
| 26 | ActorConvertPathError | path 解析失败 |
| 27 | ActorMarshalError | 参数序列化失败 |
| 28 | ActorUnmarshalError | 参数反序列化失败 |
| 29 | ActorInvokeResultIsNil | invoke 结果为 nil |
| 30 | ActorSourceEqualTarget | 源与目标相同 |
| 31 | ActorPublishRemoteError | 远程 publish 失败 |
| 32 | ActorChildIDNotFound | 子 Actor ID 未找到 |
| 33 | ActorCallTimeout | 调用超时 |
| 34 | ActorIDIsNil | Actor ID 为空 |
| 35 | ActorNotFound | Actor 未找到 |
| 36 | ActorInvokeRemoteError | 远程 invoke 失败 |
| 37 | ActorResponseIsError | 响应是 error |
判定辅助函数:
go
ccode.IsOK(code) // code == 0
ccode.IsFail(code) // code != 0状态码从哪来
Call / CallWait / CallType 的返回值就是这些码:
go
code := actor.CallWait(targetPath, "funcName", arg, &reply)
if ccode.IsFail(code) {
clog.Warnf("调用失败, code = %d", code)
}业务状态码
业务码由你自己定义,和框架码不重叠即可(框架占用 0–37)。示例项目把业务码放在 internal/code:
go
const (
OK = 0
Error = 1
PIDError = 100
LoginError = 201
AccountAuthFail = 202
AccountBindFail = 203
AccountTokenValidateFail = 204
AccountNameIsExist = 205
AccountRegisterError = 206
AccountGetFail = 207
PlayerDenyLogin = 301
// ...
PlayerIDError = 306
)两套码共用一个 int32
框架码和业务码用的是同一个 int32 字段,靠取值不重叠来区分。业务码从 100 起就是为此。IsOK 只判断 == 0,所以两者可以混用同一个判定函数。
返回状态码给客户端
在 Actor 里用 ResponseCode:
go
func (p *ActorPlayer) enter(session *cproto.Session, req *pb.EnterRequest) {
newPlayerTable, errCode := db.CreatePlayer(session, req.PlayerName, serverId, row)
if code.IsFail(errCode) {
p.ResponseCode(session, errCode) // 把业务码回给客户端
return
}
// ...
}在连接层(gate 网关)用 Agent 的版本,第三个参数标记「这是错误响应」:
go
userToken, errCode := p.validateToken(req.Token)
if code.IsFail(errCode) {
agent.Response(session, errCode)
return
}
sdkRow := data.SdkConfig.Get(userToken.PID)
if sdkRow == nil {
agent.ResponseCode(session, code.PIDError, true) // true = 错误响应
return
}ResponseCode 的第三个参数是 isError
两个同名方法签名不同,容易记错:
go
// Actor 内(pomelo.ActorBase)—— 没有第三个参数
func (p *ActorBase) ResponseCode(session *cproto.Session, statusCode int32)
// 连接层(pomelo.Agent)—— 可变参数,标记错误响应
func (a *Agent) ResponseCode(session *cproto.Session, statusCode int32, isError ...bool)踢下线是另一个方法:Agent.Kick(reason, closed)。
状态码怎么传到客户端
框架在 pomelo 层做分流:OK 走正常响应,非 OK 会把状态码塞进响应的 Code 字段并标记为错误:
go
func (p *Actor) response(rsp *cproto.PomeloResponse) {
if ccode.IsOK(rsp.Code) {
agent.ResponseMID(rsp.Mid, rsp.Data, false)
} else {
agent.ResponseMID(rsp.Mid, &cproto.Response{Code: rsp.Code}, true)
}
}客户端拿到的是 { code: <状态码>, data: ... } 结构。
状态码文案表
数值状态码对玩家不友好,示例项目用一份配表做「码 → 文案」映射,由 data-config 组件 加载:
json
[
{ "code": 0, "message": "ok" },
{ "code": 100, "message": "pid 错误" },
{ "code": 201, "message": "登录异常" },
{ "code": 204, "message": "token 校验失败" }
]加载后注册进业务码包,HTTP 层用它渲染响应:
go
func RenderResult(c *cherryGin.Context, statusCode int32, data ...any) {
result := NewDataResult(statusCode) // 内部用 GetMessage(code) 填 message
c.JSON200(result)
}查不到文案时 GetMessage 返回空字符串。
error 包
import cerr "github.com/cherry-game/cherry/error"构造错误
只有四个函数:
go
func Error(text string) error // 同 errors.New
func Errorf(format string, a ...interface{}) error // 格式化
func Wrap(err error, text string) error // 包装,产物形如 err:<err>, text:<text>
func Wrapf(err error, format string, a ...interface{}) error没有 cerr.New
这个包没有 New 函数,别写成 cerr.New("..."),用 cerr.Error("...")。
go
return cerr.Error("配置缺失")
return cerr.Errorf("节点 %s 未找到", nodeID)
return cerr.Wrap(err, "读取配表失败")预定义错误
框架把常用的错误做成了可直接比较的哨兵值,按类别分组:
路由
| 变量 | 含义 |
|---|---|
RouteFieldCantEmpty | 路由字段为空 |
RouteInvalid | 路由格式非法 |
数据包
| 变量 | 含义 |
|---|---|
PacketWrongType | 包类型错误 |
PacketSizeExceed | 包大小超限 |
PacketConnectClosed | 连接已关闭 |
PacketInvalidHeader | 包头非法 |
PacketMsgSmallerThanExpected | 消息长度小于预期 |
消息
| 变量 | 含义 |
|---|---|
MessageWrongType | 消息类型错误 |
MessageInvalid | 消息非法 |
MessageRouteNotFound | 路由未找到 |
集群
| 变量 | 含义 |
|---|---|
ClusterClientIsStop | 集群客户端已停止 |
ClusterRequestTimeout | 集群请求超时 |
ClusterPacketMarshalFail | 集群包序列化失败 |
ClusterPacketUnmarshalFail | 集群包反序列化失败 |
ClusterPublishFail | 发布失败 |
ClusterRequestFail | 请求失败 |
ClusterNodeTypeIsNil | 节点类型为空 |
ClusterNodeTypeMemberNotFound | 该类型下无成员 |
其他
| 变量 | 含义 |
|---|---|
DiscoveryNotFoundNode | 发现服务找不到节点 |
ActorPathError | Actor 路径错误 |
FuncIsNil / FuncTypeError | 反射调用相关 |
ProtobufWrongValueType | protobuf 值类型错误 |
用 errors.Is 判断:
go
if errors.Is(err, cerr.PacketSizeExceed) {
clog.Warn("客户端发来的包超限")
}排查建议
| 现象 | 可能是 |
|---|---|
客户端收到 code = 10 | 消息在登录前就发出,session 还没绑定 UID |
code = 35(ActorNotFound) | Actor 路径写错,或目标 Actor 还没创建 |
code = 33(ActorCallTimeout) | 目标 Actor 处理超时,默认 3 秒,见 Actor 模型 |
code = 11 | 目标节点没启动,或 master 没先启动导致发现服务里没有它 |
| 包超限 | 见 网络数据包 - 包大小限制 |