diff

package
v2.1.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 8 Imported by: 0

README

tools/diff

基于 struct diff 标签的结构体字段对比,适合审计日志、变更事件摘要。

快速开始

type Spec struct {
    Name  string            `diff:"名称"`
    Items []Item            `diff:"列表"`
    Meta  map[string]string `diff:"元数据"`
}

type Item struct {
    ID   string `diff:"ID,key"`
    Name string `diff:"名称"`
}

before := Spec{Name: "a", Items: []Item{{ID: "1", Name: "x"}}}
after := Spec{Name: "b", Items: []Item{{ID: "1", Name: "y"}}}

// 推荐:先归一化 nil map/slice,再对比(避免 nil vs [] 误报)
records := diff.CompareNormalized(before, after)

// 或手动追加脱敏字段
records = diff.AppendRecords(records,
    diff.MaskedChange("Secret", "密钥", "******", diff.LevelWarn),
)

fmt.Println(diff.Summary(records))
// 名称: a -> b, 列表[1].名称: x -> y

错误返回版本(类型不一致时不 panic):

records, err := diff.CompareE(before, after)
if errors.Is(err, diff.ErrTypeMismatch) {
    // before/after 类型不同
}

标签约定

标签 含义
diff:"描述" 参与对比,FieldDesc 使用该描述
diff:"-" 忽略该字段
无标签 默认参与对比(FieldDesc 用字段名);Options.OnlyTaggedFields=true 时忽略
diff:"ID,key" 切片元素的 identity;同 key 视为同一条,做原地字段 diff
diff:"名称,level=warn" 默认级别:info / warn / error

示例:

type User struct {
    ID       string `diff:"用户ID,key"`
    Name     string `diff:"姓名"`
    Password string `diff:"-"`
    Role     string `diff:"角色,level=warn"`
}

API

函数 说明
Compare / CompareE 直接对比;Compare 在类型不一致时 panic
CompareNormalized / CompareNormalizedE NormalizeNilContainers 再对比
NormalizeNilContainers 深拷贝,并把 nil map/slice 变成空容器
Summary 拼成 字段: old -> new 文本
MaskedChange / AppendRecords 手工构造/追加脱敏变更
Options
diff.Compare(a, b, &diff.Options{
    OnlyTaggedFields: true,                    // 仅对比带 diff 标签的字段
    IgnoreFields:     map[string]bool{"Meta": true},
    FieldLevels:      map[string]diff.DiffLevel{
        "Name": diff.LevelError,               // 覆盖 tag 默认级别(按 FieldPath)
    },
})

对比规则

  • 标量 / time.Time:值不等则记一条 diff。
  • 指针:nil 状态变化记一条;双方非 nil 则解引用继续比。
  • any / interface:比较动态值;类型变化记整值 diff。
  • 结构体:按导出字段递归。
  • 切片 / 数组
    • 元素为 struct(或 *struct):用 key 字段匹配;无 key 时用全部带 diff 标签的标量拼接作 identity。
    • 匹配成功:递归字段 diff,路径带 identity,如 Items[1].Name / 列表[1].名称
    • 未匹配:记新增(OldValue=新增)或移除(NewValue=移除)。
  • Map:按 key 对齐;双方都有则对 value 递归字段级对比(如 Meta[x].Name)。

DiffRecord

type DiffRecord struct {
    FieldPath string    // 如 Basic.ID、Items[1].Name、Meta[env]
    FieldDesc string    // 人类可读描述
    OldValue  any
    NewValue  any
    Level     DiffLevel // Info / Warn / Error
}

常量:

  • MarkerAdd = "新增"
  • MarkerRemove = "移除"

注意事项

  1. Compare / CompareE 要求两边类型完全一致(reflect.Type 相等)。
  2. 需要忽略「nil 切片 vs 空切片」时用 CompareNormalized
  3. NormalizeNilContainers 返回深拷贝,修改结果不会影响原对象。
  4. 切片元素路径使用 identity(有 key 时)或下标,避免多条元素变更路径撞车。
  5. Options.FieldLevels 的 key 是完整 FieldPath(含切片下标/key、map key)。

Documentation

Overview

Package diff 提供基于 struct diff 标签的结构体字段对比,用于审计日志与变更事件。

约定

  • 参与对比的字段应显式标注 diff:"描述";不参与对比用 diff:"-";未标注字段默认也会对比(可用 Options.OnlyTaggedFields 关闭)。
  • Compare / CompareNormalized 要求 before 与 after 为同一类型;CompareE / CompareNormalizedE 在类型不一致时返回 ErrTypeMismatch。
  • 切片/数组对 struct(含 *struct)元素:先用 identity 字段匹配同一条元素,再递归比较其余字段。 标量字段可加 diff:"ID,key" 仅作 identity(同 ID 改其它字段会记 in-place diff,而非整行新增/移除); 未标注 key 时退化为「全部带 diff 标签的标量字段拼接」作为 identity。 匹配后的字段路径会带 identity/下标,如 Items[1].Name。
  • Map 按 key 对齐后对 value 递归对比,路径形如 Meta[env].Name。

典型用法

records := diff.CompareNormalized(beforeSpec, afterSpec)
records = diff.AppendRecords(records, diff.MaskedChange("Value", "凭证值", "******", diff.LevelWarn))

Index

Constants

View Source
const (
	// MarkerAdd 切片/集合新增元素的占位旧值。
	MarkerAdd = "新增"
	// MarkerRemove 切片/集合移除元素的占位新值。
	MarkerRemove = "移除"
)

Variables

View Source
var ErrTypeMismatch = errors.New("diff: type mismatch")

ErrTypeMismatch 在 CompareE / CompareNormalizedE 中,当 before 与 after 类型不一致时返回。

Functions

func NormalizeNilContainers added in v2.1.10

func NormalizeNilContainers[T any](v T) T

NormalizeNilContainers 返回 v 的深拷贝,并将 struct 内 nil map/slice 归一化为空容器,避免 diff 误报。

func Summary added in v2.1.10

func Summary(records []DiffRecord) string

Summary 将多条差异格式化为可读摘要(field: old -> new)。

Types

type DiffLevel

type DiffLevel int
const (
	LevelInfo DiffLevel = iota
	LevelWarn
	LevelError
)

type DiffRecord

type DiffRecord struct {
	FieldPath string // 结构体字段路径(如 "Basic.ID")
	FieldDesc string // 字段描述(如 "基础信息.用户ID")
	OldValue  any
	NewValue  any
	Level     DiffLevel
}

func AppendRecords added in v2.1.10

func AppendRecords(records []DiffRecord, extra ...DiffRecord) []DiffRecord

AppendRecords 追加变更记录。

func Compare

func Compare(a, b any, opts ...*Options) []DiffRecord

Compare 对比两个同类型对象,返回差异列表;类型不一致时 panic。

func CompareE added in v2.1.10

func CompareE(a, b any, opts ...*Options) ([]DiffRecord, error)

CompareE 与 Compare 相同,但类型不一致时返回 ErrTypeMismatch 而非 panic。

func CompareNormalized added in v2.1.10

func CompareNormalized[T any](before, after T, opts ...*Options) []DiffRecord

CompareNormalized 在对比前对 before/after 做 nil 容器归一化。

func CompareNormalizedE added in v2.1.10

func CompareNormalizedE[T any](before, after T, opts ...*Options) ([]DiffRecord, error)

CompareNormalizedE 归一化后对比,类型不一致时返回 ErrTypeMismatch。

func MaskedChange added in v2.1.10

func MaskedChange(fieldPath, fieldDesc, mask string, level DiffLevel) DiffRecord

MaskedChange 构造脱敏字段变更(如凭证值),old/new 使用同一占位符。

func (*DiffRecord) GetNewValue added in v2.1.10

func (s *DiffRecord) GetNewValue() string

func (*DiffRecord) GetOldValue added in v2.1.10

func (s *DiffRecord) GetOldValue() string

type Options

type Options struct {
	IgnoreFields map[string]bool      // 忽略字段路径
	FieldLevels  map[string]DiffLevel // 字段级别映射
	// OnlyTaggedFields 为 true 时,仅对比显式带 diff 标签(且非 "-")的字段。
	OnlyTaggedFields bool
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL