错误处理
*Error 的 Code 分类、四个哨兵、触发条件与处理模式。
需要计算的入口都返回 (值, error)。日期格式、日期存在性、年份范围、
时辰索引在核心层前置校验;性别、语言、标识、配置这些字符串取值在绑定层校验。
纯查询方法(Palace、Star、Has 等)不返回错误,查不到时返回 nil 或零值。
*Error
包内所有失败都返回同一个具体类型:
type Error struct {
Code string // 机器可读的类别,Code* 常量之一
Message string // 错误描述原文(英文)
}
func (e *Error) Error() string // 返回 "iztro: " + Message
func (e *Error) Unwrap() error // 返回本类别的哨兵,供 errors.Is 匹配四个类别
| 哨兵 | Code 常量 | 取值 | 含义 |
|---|---|---|---|
ErrInvalidDate | CodeInvalidDate | invalid_date | 日期格式非法、日期不存在,或超出公历 1583–9999 |
ErrInvalidTimeIndex | CodeInvalidTimeIndex | invalid_time_index | 时辰索引越界(合法值 0–12) |
ErrInvalidArgument | CodeInvalidArgument | invalid_argument | 其余入参或配置非法:性别、语言、星耀标识、自定义表 |
ErrInternal | CodeInternal | internal | 库内部缺陷或 Go 侧运行时故障,如 wasm 实例化失败、结果解码失败 |
Code 的四个取值与 Rust 的 IztroError::code()、Python 的 IztroError.code
是同一套,跨语言分支逻辑可以照抄。
两种判断姿势
_, err := iztro.BySolar("2000-2-30", 2, iztro.GenderFemale, true, iztro.LanguageZhCN, nil)
// 按类别分支:errors.Is 对哨兵匹配
fmt.Println(errors.Is(err, iztro.ErrInvalidDate))
fmt.Println(errors.Is(err, iztro.ErrInvalidTimeIndex))
// 要拿 Code 与原文:errors.As 取出具体类型
var e *iztro.Error
if errors.As(err, &e) {
fmt.Println(e.Code, "|", e.Message)
}
fmt.Println(err)输出
true
false
invalid_date | invalid solar date '2000-2-30': day is out of range for that month
iztro: invalid solar date '2000-2-30': day is out of range for that month错误消息统一带 iztro: 前缀
Error() 在 Message 前补 iztro: ,与调用方自己的错误容易区分;
Message 字段本身不带前缀。消息主体来自核心层,三种语言绑定完全一致,
但文案会随版本调整——要在程序里分支请用 errors.Is 或 Code,别解析文案。
日期相关
| 情形 | 例 | 消息主体 |
|---|---|---|
格式不是 YYYY-M-D | "2000/8/16" | expected 'YYYY-M-D' |
| 年、月、日不是数字 | "abc-8-16" | year is not a number |
| 月份越界 | "2000-13-1" | month must be within 1-12 |
| 该月没有这一天 | "2000-2-30" | day is out of range for that month |
| 年份超出支持范围 | "1500-1-1" | year must be within 1583-9999 |
Code 为 invalid_date。农历专有(只有 ByLunar 与两个 *ByLunarDate 查询会碰到):
| 情形 | 例 | 消息主体 |
|---|---|---|
| 该农历年没有这个月 | 表内缺月 | month does not exist in that lunar year |
| 该农历月没有这一天 | "2000-7-30"(七月是小月) | day is out of range for that lunar month |
农历消息的前缀是 invalid lunar date '<原串>': ,公历是 invalid solar date '<原串>': ,
从消息就能看出走的是哪个入口。
示例
for _, date := range []string{"2000-13-1", "2000-2-30", "1500-1-1"} {
if _, err := iztro.BySolar(date, 2, iztro.GenderFemale, true, iztro.LanguageZhCN, nil); err != nil {
fmt.Println(err)
}
}输出
iztro: invalid solar date '2000-13-1': month must be within 1-12
iztro: invalid solar date '2000-2-30': day is out of range for that month
iztro: invalid solar date '1500-1-1': year must be within 1583-9999消息里带上了原始输入,便于在批量处理时定位是哪一条数据出的问题。
边界与陷阱
时辰索引
触发条件 时辰索引大于 12。
示例
_, err := iztro.BySolar("2000-8-16", 13, iztro.GenderFemale, true, iztro.LanguageZhCN, nil)
fmt.Println(err)输出
iztro: time_index must be 0-12, got 13边界与陷阱
13 个而不是 12 个
子时跨午夜,拆成早子时(索引 0)与晚子时(索引 12),因此合法值有 13 个。
从小时数换算用 TimeToIndex,它保证结果落在合法范围。
性别与语言
Code 均为 invalid_argument。
| 参数 | 合法值 | 消息主体 |
|---|---|---|
gender | "male" / "female"(GenderMale / GenderFemale 常量) | invalid gender 'x': expected 'male' or 'female' |
language | 六个语言代码(Language* 常量) | invalid language 'xx': expected one of zh-CN, zh-TW, en-US, ja-JP, ko-KR, vi-VN |
语言代码大小写不敏感,连字符与下划线等价("zh-cn"、"zh_cn" 都接受)。
gender 与 language 是具名类型 Gender / Language,把别的字符串变量误传进来会在编译期被挡下;
字面量拼错则在运行期落到这一类错误。
示例
_, err := iztro.BySolar("2000-8-16", 2, "x", true, iztro.LanguageZhCN, nil)
fmt.Println(err)
fmt.Println(errors.Is(err, iztro.ErrInvalidArgument))输出
iztro: invalid gender 'x': expected 'male' or 'female'
true标识相关
工具函数与安星函数收的是语言无关标识,未知标识会报错:
_, err := iztro.GetBrightness("nosuch", 0, nil)
fmt.Println(err)输出
iztro: unknown star key 'nosuch'传译名会报错
这些函数只认标识不认译名。传 "紫微" 会得到 unknown star key '紫微'——
先用 KeyOf 换算。
例外是 chart.Palace()、chart.Star() 这类星盘上的查询方法,
它们同时接受标识与当前排盘语言的译名。
配置相关
Config 的六个开关与两张自定义表都在排盘时校验,Code 均为 invalid_argument:
| 情形 | 消息主体 |
|---|---|
| 开关取值未知 | invalid yearDivide 'nope': expected 'normal' or 'exact' |
| 四化表的天干标识未知 | invalid mutagens key 'nope': unknown heavenly stem |
| 某个天干的四化不是四项 | invalid mutagens for 'jiaHeavenly': expected 4 stars (lu, quan, ke, ji), got 1 |
| 某颗星的亮度表不是十二项 | invalid brightness for 'ziweiMaj': expected 12 entries, got 1 |
长度是严格校验:四化必须正好四项、亮度必须正好十二项,多一项少一项都报错。 自定义表只收标识不收译名。
处理模式
批量处理时跳过坏数据
rows := []struct {
Date string
TimeIndex uint8
Gender iztro.Gender
}{
{"2000-8-16", 2, "female"},
{"2000-2-30", 2, "female"},
{"1990-3-3", 13, "male"},
}
var charts []*iztro.Astrolabe
var failed []string
for _, r := range rows {
chart, err := iztro.BySolar(r.Date, r.TimeIndex, r.Gender, true, iztro.LanguageZhCN, nil)
if err != nil {
var e *iztro.Error
errors.As(err, &e)
failed = append(failed, r.Date+" -> "+e.Code)
continue
}
charts = append(charts, chart)
}
fmt.Println(len(charts), failed)输出
1 [2000-2-30 -> invalid_date 1990-3-3 -> invalid_time_index]包装成自己的错误
build := func(date string, ti uint8, gender iztro.Gender) (*iztro.Astrolabe, error) {
chart, err := iztro.BySolar(date, ti, gender, true, iztro.LanguageZhCN, nil)
if err != nil {
return nil, fmt.Errorf("排盘失败: %w", err)
}
return chart, nil
}
_, err := build("2000-2-30", 2, "female")
fmt.Println(err)
fmt.Println(errors.Is(err, iztro.ErrInvalidDate))输出
排盘失败: iztro: invalid solar date '2000-2-30': day is out of range for that month
true%w 保留原错误,调用方可以用 errors.Is / errors.As 继续判断到具体类别。
关于 nil
查询方法查不到时返回 nil 而非错误——查不到是正常结果,不是异常:
p := chart.Palace("nosuchPalace")
fmt.Println(p == nil)
s, sp := chart.Star("nosuchStar")
fmt.Println(s == nil, sp == nil)
fmt.Println(chart.Palace(iztro.PalaceSoul).Has("ziweiMj"))输出
true
true true
false取字段前先判空
chart.Palace(...)、chart.Star(...)、h.ScopeItem(...) 都可能返回 nil。
直接取字段会 panic。
名字拼错是静默的
上面三行都是「拼错了」而不是「盘上没有」:宫名少写一个字母得到 nil,
星名少写一个字母让 Has 返回 false——与真实的「没有这颗星」无法区分。
用包里的 Palace* / Star* 常量可以让编译器与 IDE 在写错的当场挡下。
名字来自外部输入时,先用 KeyOf 过一遍:
它对无法识别的文本返回空串,可以据此拒绝非法输入。
身宫与来因宫恒存在
chart.Palace("bodyPalace") 与 chart.Palace("originalPalace") 在任何一张盘上都非 nil:
来因宫要求宫干与生年干相同且不在子、丑二宫,而寅到酉这十宫刚好把十天干各走一遍,
因此生年干必然命中且只命中一次。真拿到 nil,那就是名字拼错了。