
本文介绍一种无需第三方库即可优雅处理 AWS S3 策略等 JSON 场景中“单值或数组”歧义字段的方法:通过自定义 MaybeSlice 类型实现 json.Marshaler 和 json.Unmarshaler 接口,使 AWS、Action、Resource 等字段既能接受 "arn:..." 单字符串,也能兼容 ["arn:...", "..."] 数组格式。
本文介绍一种无需第三方库即可优雅处理 aws s3 策略等 json 场景中“单值或数组”歧义字段的方法:通过自定义 `maybeslice` 类型实现 `json.marshaler` 和 `json.unmarshaler` 接口,使 `aws`、`action`、`resource` 等字段既能接受 `"arn:..."` 单字符串,也能兼容 `["arn:...", "..."]` 数组格式。
在使用 Go 处理 AWS S3 存储桶策略(Bucket Policy)时,一个常见痛点是:S3 服务在返回策略时,会将单元素数组(如 "Action": ["s3:GetObject"])自动简化为单值字符串("Action": "s3:GetObject"),而标准 []string 类型无法反序列化该格式,导致 json.Unmarshal 报错 json: cannot unmarshal string into Go value of type []string。
解决此问题的核心思路是放弃强制绑定 []string 类型,转而设计一个能同时容纳字符串和字符串切片的智能容器类型。推荐方案是实现一个自定义类型 MaybeSlice,并为其提供符合 encoding/json 接口规范的 MarshalJSON 和 UnmarshalJSON 方法:
type MaybeSlice []string
func (ms *MaybeSlice) MarshalJSON() ([]byte, error) {
if ms == nil {
return json.Marshal([]string{})
}
if len(*ms) == 1 {
return json.Marshal((*ms)[0]) // 输出单字符串
}
return json.Marshal([]string(*ms)) // 输出数组
}
func (ms *MaybeSlice) UnmarshalJSON(data []byte) error {
// 尝试解析为单字符串
var s string
if err := json.Unmarshal(data, &s); err == nil {
*ms = []string{s}
return nil
}
// 否则尝试解析为字符串切片
var v []string
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf("failed to unmarshal %s as string or []string: %w", string(data), err)
}
*ms = v
return nil
}
✅ 关键细节说明:
- MaybeSlice 必须是指针接收者(*MaybeSlice),否则 UnmarshalJSON 无法修改原值;
- UnmarshalJSON 采用“先试单值、再试数组”的容错策略,覆盖所有合法 S3 策略格式;
- MarshalJSON 在序列化时,若仅含一个元素则输出单字符串(与 AWS 一致),多元素时输出标准数组,确保双向兼容。
更新后的结构体定义如下(注意字段类型变更):
type Policy struct {
Version string `json:"Version"`
ID string `json:"Id"`
Statement []Statement `json:"Statement"`
}
type Statement struct {
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Principal Principal `json:"Principal"`
Action *MaybeSlice `json:"Action"` // ✅ 改为 *MaybeSlice
Resource *MaybeSlice `json:"Resource"` // ✅ 改为 *MaybeSlice
}
type Principal struct {
AWS *MaybeSlice `json:"AWS"` // ✅ 同样适配
}
使用示例:
// 解析来自 S3 的混合格式策略(含单值与数组)
raw := `{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:user/Alice"},
"Action": "s3:GetObject",
"Resource": ["arn:aws:s3:::example-bucket/*"]
}]
}`
var policy Policy
if err := json.Unmarshal([]byte(raw), &policy); err != nil {
log.Fatal(err)
}
// ✅ 成功解析:policy.Statement[0].Principal.AWS 为 ["arn:aws:iam::123456789012:user/Alice"]
// ✅ policy.Statement[0].Action 为 ["s3:GetObject"]
// 修改后重新序列化(自动适配最优格式)
policy.Statement[0].Action = &MaybeSlice{"s3:GetObject", "s3:ListBucket"}
data, _ := json.Marshal(policy)
// 输出中 "Action" 自动变为数组:["s3:GetObject","s3:ListBucket"]
⚠️ 注意事项:
- 所有 MaybeSlice 字段必须声明为指针(*MaybeSlice),避免零值解引用 panic;
- 若字段可能为空(null),需在 UnmarshalJSON 中增加对 nil 的判断(示例已补充);
- 此方案完全不依赖外部库,轻量且类型安全,适用于任何类似场景(如 CloudFormation、IAM Policy、OpenAPI Schema 等存在“单值/数组”弹性要求的 JSON 结构)。
通过这一模式,你既能保持结构体的清晰语义,又能无缝兼容 AWS 服务端的非标准 JSON 表达,真正实现「一次定义,双向通行」。











