# 错误处理 (/zh/docs/go/errors)

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



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

纯查询方法（`Palace`、`Star`、`Has` 等）不返回错误，查不到时返回 `nil` 或零值。

## \*Error [#error]

包内所有失败都返回同一个具体类型：

```go
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`
是同一套，跨语言分支逻辑可以照抄。

### 两种判断姿势 [#两种判断姿势]

```go
_, 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)
```

**输出**

```text
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
```

<Callout type="info" title="错误消息统一带 iztro: 前缀">
  `Error()` 在 `Message` 前补 `iztro: `，与调用方自己的错误容易区分；
  `Message` 字段本身不带前缀。消息主体来自核心层，三种语言绑定完全一致，
  但**文案会随版本调整**——要在程序里分支请用 `errors.Is` 或 `Code`，别解析文案。
</Callout>

***

## 日期相关 [#日期相关]

| 情形              | 例             | 消息主体                                 |
| --------------- | ------------- | ------------------------------------ |
| 格式不是 `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 '<原串>': `，
从消息就能看出走的是哪个入口。

**示例**

```go
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)
    }
}
```

**输出**

```text
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
```

消息里带上了原始输入，便于在批量处理时定位是哪一条数据出的问题。

**边界与陷阱**

<Accordions>
  <Accordion title="年份下限 1583 的由来">
    1582 年格里历改革当年有一段不存在的日期。底层历法库在这些日期上没有定义，
    因此支持范围从改革完成后的 1583 年起算。上限 9999 是农历数据表的覆盖终点。
  </Accordion>

  <Accordion title="月日不必补零">
    `"2000-8-16"` 与 `"2000-08-16"` 都接受。分隔符必须是 `-`。
  </Accordion>

  <Accordion title="农历闰月的校验">
    `ByLunar` 会检查该农历年该月是否真的存在，以及该月有多少天（大月 30、小月 29）。
    `leap` 标为闰月但那年那月无闰月时不报错，按普通月处理；`leap` 不是三个 `LeapMonth` 取值之一时返回 `ErrInvalidArgument`。
  </Accordion>
</Accordions>

***

## 时辰索引 [#时辰索引]

**触发条件**　时辰索引大于 12。

**示例**

```go
_, err := iztro.BySolar("2000-8-16", 13, iztro.GenderFemale, true, iztro.LanguageZhCN, nil)
fmt.Println(err)
```

**输出**

```text
iztro: time_index must be 0-12, got 13
```

**边界与陷阱**

<Callout type="info" title="13 个而不是 12 个">
  子时跨午夜，拆成早子时（索引 0）与晚子时（索引 12），因此合法值有 13 个。
  从小时数换算用 [`TimeToIndex`](/zh/docs/go/util#timetoindex)，它保证结果落在合法范围。
</Callout>

***

## 性别与语言 [#性别与语言]

`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`，把别的字符串变量误传进来会在编译期被挡下；
字面量拼错则在运行期落到这一类错误。

**示例**

```go
_, err := iztro.BySolar("2000-8-16", 2, "x", true, iztro.LanguageZhCN, nil)

fmt.Println(err)
fmt.Println(errors.Is(err, iztro.ErrInvalidArgument))
```

**输出**

```text
iztro: invalid gender 'x': expected 'male' or 'female'
true
```

***

## 标识相关 [#标识相关]

工具函数与安星函数收的是语言无关标识，未知标识会报错：

```go
_, err := iztro.GetBrightness("nosuch", 0, nil)
fmt.Println(err)
```

**输出**

```text
iztro: unknown star key 'nosuch'
```

<Callout type="warn" title="传译名会报错">
  这些函数只认标识不认译名。传 `"紫微"` 会得到 `unknown star key '紫微'`——
  先用 [`KeyOf`](/zh/docs/go/i18n#keyof) 换算。

  例外是 `chart.Palace()`、`chart.Star()` 这类**星盘上的查询方法**，
  它们同时接受标识与当前排盘语言的译名。
</Callout>

***

## 配置相关 [#配置相关]

`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`                  |

<Callout type="info">
  长度是**严格**校验：四化必须正好四项、亮度必须正好十二项，多一项少一项都报错。
  自定义表只收标识不收译名。
</Callout>

***

## 处理模式 [#处理模式]

**批量处理时跳过坏数据**

```go
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)
```

**输出**

```text
1 [2000-2-30 -> invalid_date 1990-3-3 -> invalid_time_index]
```

**包装成自己的错误**

```go
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))
```

**输出**

```text
排盘失败: iztro: invalid solar date '2000-2-30': day is out of range for that month
true
```

`%w` 保留原错误，调用方可以用 `errors.Is` / `errors.As` 继续判断到具体类别。

***

## 关于 nil [#关于-nil]

查询方法查不到时返回 `nil` 而非错误——查不到是正常结果，不是异常：

```go
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"))
```

**输出**

```text
true
true true
false
```

<Callout type="warn" title="取字段前先判空">
  `chart.Palace(...)`、`chart.Star(...)`、`h.ScopeItem(...)` 都可能返回 `nil`。
  直接取字段会 panic。
</Callout>

<Callout type="warn" title="名字拼错是静默的">
  上面三行都是「拼错了」而不是「盘上没有」：宫名少写一个字母得到 `nil`，
  星名少写一个字母让 `Has` 返回 `false`——与真实的「没有这颗星」无法区分。

  用包里的 `Palace*` / `Star*` 常量可以让编译器与 IDE 在写错的当场挡下。
  名字来自外部输入时，先用 [`KeyOf`](/zh/docs/go/i18n#keyof) 过一遍：
  它对无法识别的文本返回空串，可以据此拒绝非法输入。
</Callout>

<Callout type="info" title="身宫与来因宫恒存在">
  `chart.Palace("bodyPalace")` 与 `chart.Palace("originalPalace")` 在任何一张盘上都非 `nil`：
  来因宫要求宫干与生年干相同且不在子、丑二宫，而寅到酉这十宫刚好把十天干各走一遍，
  因此生年干必然命中且只命中一次。真拿到 `nil`，那就是名字拼错了。
</Callout>
