跳到主要内容
版本:1.7

Go Standard Row Format

github.com/apache/fory/go/fory/row 实现与 Java、C++、Python 和 Rust 共享的 Standard Row Format, 提供结构体编码、字段随机访问和跨语言 Schema 编码。

Row Format 适用于可信的内存数据,例如选择性字段读取、内存映射数据和跨语言分析数据管道。 只读取可信来源、由 Fory Row writer 按相同 Schema 生成的数据;该格式不提供对象序列化针对不可信输入的防护。

基本用法​

package main

import (
"fmt"

"github.com/apache/fory/go/fory/row"
)

type UserProfile struct {
Id int64
Username string
Email *string
Scores []int32
}

func main() {
encoder, err := row.NewEncoder[UserProfile]()
if err != nil {
panic(err)
}
email := "alice@example.com"
profile := UserProfile{
Id: 12345, Username: "alice", Email: &email, Scores: []int32{95, 87},
}
data, err := encoder.ToRow(&profile)
if err != nil {
panic(err)
}
schema := encoder.Schema()
view := row.NewRow(schema, data)
fmt.Println(view.String(schema.FieldIndex("username"))) // alice
fmt.Println(view.Array(schema.FieldIndex("scores")).Int32(1)) // 87
fmt.Println(view.IsNullAt(schema.FieldIndex("email"))) // false

decoded, err := encoder.FromRow(data)
if err != nil {
panic(err)
}
fmt.Println(decoded.Username) // alice
}

创建 encoder 的成本高于复用实例。每种结构体类型应创建一次并复用,但不要在多个 goroutine 中 无保护地共享同一个 encoder。

裸 Row 与带帧消息​

ToRow 和 FromRow 处理裸 Row 字节,与其他语言的 toRow 和 BinaryRow.pointTo 对应。 Encode 和 Decode 额外使用 Java、Python Row encoder 的帧格式:八字节小端 Schema hash,随后是 Row。

framed, err := encoder.Encode(&profile)
decoded, err := encoder.Decode(framed)

hash 只包含递归字段类型 ID。它可以检测字段类型变化,但不能检测字段重命名、可空性变化或相同类型字段 的重排。如果双方需要完整校验 Schema,应交换 Schema 字节。

Decode 和 FromRow 对截断或不一致的数据返回错误。解码后的值不引用输入字节,因此之后可以复用输入缓冲区。

零拷贝读取​

row.NewRow(schema, data) 创建 Row 视图。Struct、Array 和 Map 返回嵌套字节视图, Binary 返回输入的子切片。这些视图只在底层字节有效且不被修改时有效。 String 会复制数据,避免 Go 字符串随输入缓冲区变化。

固定宽度 getter 对 null 返回零值,需要区分 null 和零时使用 IsNullAt。 变长 getter 对 null 返回 nil 或空字符串。越界索引和格式错误的数据可能导致原始视图访问 panic; 需要错误返回时使用 encoder 的解码方法。

可空性​

指针字段可空:nil 写入 null 位,并在解码时恢复为 nil。切片、Map 和 []byte 同样可空, 并且区分 nil 与空容器。

字符串、嵌套值结构体、fory.Date、time.Time 和 time.Duration 在 Schema 中可空, 但对应的 Go 值不能持有 nil,因此解码 null 会返回错误。需要保留 null 时,使用 *string、 *time.Time 或 *Inner 等指针类型。

Map 值可空,键不可空。[]*int32 元素和 map[string]*int32 值可以承载 null, []int32 和 map[string]int32 不可以。

字段顺序与名称​

字段按 lowerCamel 名称排序,并以 snake_case 命名,例如 UserName 对应 user_name。 该规则与 Java 的 Schema 推导一致。不导出的字段会被跳过。 fory:"-"、fory:"ignore" 和 fory:"ignore=true" 会跳过字段;其他 tag 选项被接受但不影响 Row Format。 更改字段名或类型时,必须协调所有读写方的 Schema。

支持的类型​

Go 类型Standard Row 编码可空
bool、int8、int16、int32、int64固定宽度标量否
intint64否
float32、float64IEEE 754 浮点数否
fory.Date自纪元起的天数 date32是
time.Time自纪元起的微秒数是
time.Duration微秒数是
stringUTF-8 字节是
[]byteint8 Standard array是
受支持元素类型的 []TStandard array是
map[K]VStandard map是
嵌套结构体嵌套 Standard Row是
受支持的非切片、非 Map 类型的 *T与 T 相同是

[]byte 与 Java byte[] 一样按 int8 列表编码。binary 类型仅可通过手动构建的 Schema、 RowWriter.WriteBytes 和 Row.Binary 使用。

字符串必须是有效 UTF-8。时间戳微秒数必须能由 int64 表示。 Map 键可以是标量、字符串或由这些类型递归组成的值结构体。 time.Time、time.Duration 和指针不能用作键,也不能出现在键结构体的有效字段中。 Duration 编码截断到微秒;如果应用需要以时间为键,请使用单位明确的 int64。

不支持无符号整数、固定长度数组、嵌套指针、切片或 Map 的指针、接口、channel、函数、递归类型、 float16 和 decimal。

Schema 交换​

SchemaToBytes 和 SchemaFromBytes 使用与 Java SchemaEncoder、Python Schema.to_bytes 和 from_bytes 一致的编码。ComputeSchemaHash 计算 Encode 使用的类型结构 hash。

schemaBytes, err := row.SchemaToBytes(encoder.Schema())
schema, err := row.SchemaFromBytes(schemaBytes)
fmt.Println(schema.Equal(encoder.Schema())) // true

Java bean 与 Go 结构体需要对应字段和一致的可空性。与 Java 装箱类型对应时使用 Go 指针, 与 Java 基本类型对应时使用 Go 值。

手动写入 Row​

使用 RowWriter、ArrayWriter 和 MapWriter 可以按手动构建的 Schema 写入,不需要 Go 结构体。 嵌套值写入共享缓冲区后,通过 SetOffsetAndSize 关联到父字段。

schema := row.NewSchema([]row.Field{
row.NewField("id", row.Int64Type{}, false),
row.NewField("tags", row.List(row.StringType{}), true),
})
writer := row.NewRowWriter(schema)
writer.Reset()
writer.WriteInt64(0, 7)
tags := row.NewArrayWriter(row.List(row.StringType{}).Elem, writer.Buffer())
start := writer.Buffer().WriterIndex()
if err := tags.Reset(2); err != nil {
panic(err)
}
if err := tags.WriteString(0, "go"); err != nil {
panic(err)
}
tags.SetNullAt(1)
if err := writer.SetOffsetAndSize(1, start, writer.Buffer().WriterIndex()-start); err != nil {
panic(err)
}
data := writer.ToBytes()

每次写 Row 前调用 Reset;为新的顶层 Row 复用 writer 时,先将缓冲区的写位置设为零。 ToBytes 返回的字节只在下一次修改缓冲区之前有效。

线程安全​

encoder 和 writer 不支持无保护的并发调用。每个 goroutine 使用独立实例,或用互斥锁保护。 Row、数组和 Map 视图可以被多个 goroutine 同时读取,前提是底层字节不被修改。