错误处理

*Error 的 Code 分类、四个哨兵、触发条件与处理模式。

需要计算的入口都返回 (值, error)。日期格式、日期存在性、年份范围、 时辰索引在核心层前置校验;性别、语言、标识、配置这些字符串取值在绑定层校验。

纯查询方法(PalaceStarHas 等)不返回错误,查不到时返回 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 常量取值含义
ErrInvalidDateCodeInvalidDateinvalid_date日期格式非法、日期不存在,或超出公历 1583–9999
ErrInvalidTimeIndexCodeInvalidTimeIndexinvalid_time_index时辰索引越界(合法值 0–12)
ErrInvalidArgumentCodeInvalidArgumentinvalid_argument其余入参或配置非法:性别、语言、星耀标识、自定义表
ErrInternalCodeInternalinternal库内部缺陷或 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.IsCode,别解析文案。


日期相关

情形消息主体
格式不是 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

Codeinvalid_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" 都接受)。 genderlanguage 是具名类型 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,那就是名字拼错了。

本页目录