title: Go Standard Row Format sidebar_position: 7 id: go license: | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Apache Fory™ Go implements the Standard Row Format used by Java, C++, Python, and Rust in the github.com/apache/fory/go/fory/row package. It provides a reflection-based struct encoder, zero-copy readers for random field access, and the cross-language schema encoding.
Use Row Format when readers need selected fields or collection elements rather than a fully reconstructed value. Readers are views over the encoded bytes: reading one field costs a bitmap test and a slot lookup, and other fields are never touched.
Row Format is a trusted in-memory format. Decode only bytes produced by a Fory row writer for the same schema, from a source you trust. Malformed bytes surface as errors from the encoder's decode methods, but the format does not defend against hostile input the way object serialization does.
package main import ( "fmt" "github.com/apache/fory/go/fory/row" ) type UserProfile struct { Id int64 Username string Email *string Scores []int32 Preferences map[string]string IsActive bool } 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, 92, 88}, Preferences: map[string]string{"theme": "dark", "language": "en"}, IsActive: true, } rowBytes, err := encoder.ToRow(&profile) if err != nil { panic(err) } // Random access without decoding the whole struct. schema := encoder.Schema() r := row.NewRow(schema, rowBytes) fmt.Println(r.String(schema.FieldIndex("username"))) // alice fmt.Println(r.Array(schema.FieldIndex("scores")).Int32(1)) // 87 fmt.Println(r.IsNullAt(schema.FieldIndex("email"))) // false // Full decode when the whole value is needed. decoded, err := encoder.FromRow(rowBytes) if err != nil { panic(err) } fmt.Println(decoded.Preferences["theme"]) // dark }
NewEncoder[T] infers the schema from the struct type once and compiles the conversion for it. Creating an encoder is comparatively expensive; create one per struct type and reuse it.
ToRow and FromRow work with bare row bytes, which is what the other languages' toRow and BinaryRow.pointTo exchange. Encode and Decode add the framing used by the Java and Python row encoders: an 8-byte little-endian schema hash followed by the row.
framed, err := encoder.Encode(&profile) // hash + row decoded, err := encoder.Decode(framed) // verifies the hash, then decodes
The hash is a type-shape fingerprint: it folds the recursive field type ids and nothing else, so Decode rejects a writer whose field types differ but cannot detect renamed fields, changed nullability, or reordered fields of the same type. Share the schema bytes (see Schema Exchange) when peers must agree on more than the type shape.
Decode and FromRow return errors for truncated or inconsistent bytes; they never panic. Decoded values never alias the input, so the input buffer can be reused immediately.
row.NewRow(schema, bytes) creates a view over a row. Struct, Array, and Map return views over the nested bytes, and Binary returns a sub-slice of the input. These views stay valid only while the underlying bytes are alive and unmodified. String copies, because a Go string must not observe later changes to the buffer.
r := row.NewRow(schema, rowBytes)
tags := r.Array(schema.FieldIndex("tags"))
for i := 0; i < tags.NumElements(); i++ {
if !tags.IsNullAt(i) {
fmt.Println(tags.String(i))
}
}
attrs := r.Map(schema.FieldIndex("attrs"))
fmt.Println(attrs.Keys().String(0), attrs.Values().Int32(0))
Fixed-width getters return the zero value for null fields; use IsNullAt to distinguish null from zero. Variable-width getters return nil (or "") for null. Out-of-range indexes panic, as does malformed data read through a raw view; use the encoder's decode methods when an error is needed.
A Go pointer field is a nullable field: nil writes the null bit and decodes back to nil. Slices, maps, and []byte are nullable as well and distinguish nil from empty.
Strings, nested value structs, fory.Date, time.Time, and time.Duration are also nullable in the schema because their Java carriers are objects, but the Go value cannot hold nil. Decoding a null into one of these fields is an error. Use a pointer carrier (*string, *time.Time, *Inner) when nulls must round-trip, for example when reading rows written by Java with null values.
Map values are always nullable and map keys never are, matching the other languages. A []*int32 element or map[string]*int32 value carries a null element; []int32 and map[string]int32 do not.
Fields are sorted by their lowerCamel name and named by its snake_case form (UserName becomes user_name), matching Java's schema inference so both languages derive the same schema from equivalent struct definitions. Unexported fields are skipped. The fory struct tag uses the same grammar as object serialization: fory:"-", fory:"ignore", and fory:"ignore=true" skip a field; other keys are accepted and ignored by Row Format.
Changing a field name or type changes the schema. Coordinate such changes across all producers and consumers.
| Go type | Standard Row Format encoding | Nullable |
|---|---|---|
bool, int8, int16, int32, int64 | Fixed-width scalar | No |
int | Fixed-width int64 | No |
float32, float64 | Fixed-width IEEE 754 scalar | No |
fory.Date | Fixed-width date32 in epoch days | Yes |
time.Time | Fixed-width epoch microseconds | Yes |
time.Duration | Fixed-width microseconds | Yes |
string | Variable-width UTF-8 | Yes |
[]byte | Standard array of int8 | Yes |
[]T for supported element types | Standard array | Yes |
map[K]V | Standard map | Yes |
| Nested struct | Nested Standard Row | Yes |
*T for any supported non-slice, non-map T | Same encoding as T | Yes |
[]byte matches Java‘s byte[], which Java also infers as a list of int8. The row format’s binary type is available only to hand-built schemas through RowWriter.WriteBytes and Row.Binary.
Strings must be valid UTF-8. Timestamps must fit in an int64 number of microseconds. Map keys must be scalars, strings, or value structs whose exported, non-ignored fields consist of such types, so that the encoded key determines Go equality. time.Time, time.Duration, and pointers are not valid keys, including inside struct keys. Duration encoding truncates to microseconds, so distinct nanosecond keys could otherwise collapse into one entry. Use an explicit int64 key in the unit your application requires.
Unsupported: unsigned integers, fixed-size arrays, nested pointers, pointers to slices or maps, interfaces, channels, functions, recursive types, float16, and decimal.
SchemaToBytes and SchemaFromBytes implement the cross-language schema encoding shared with Java‘s SchemaEncoder and Python’s Schema.to_bytes/from_bytes. ComputeSchemaHash computes the same type-shape hash used by Encode.
schemaBytes, err := row.SchemaToBytes(encoder.Schema()) schema, err := row.SchemaFromBytes(schemaBytes) fmt.Println(schema.Equal(encoder.Schema())) // true
A Java bean and a Go struct with equivalent fields produce identical schema bytes when their nullability matches: use pointer fields for Java boxed types (Integer, String in lists) and value fields for Java primitives.
RowWriter, ArrayWriter, and MapWriter write rows for a schema you construct yourself, without a Go struct. They share one fory.ByteBuffer; a nested value is written at the buffer's current position and then attached to its parent slot with SetOffsetAndSize.
schema := row.NewSchema([]row.Field{ row.NewField("id", row.Int64Type{}, false), row.NewField("tags", row.List(row.StringType{}), true), }) w := row.NewRowWriter(schema) w.Reset() w.WriteInt64(0, 7) tags := row.NewArrayWriter(row.List(row.StringType{}).Elem, w.Buffer()) start := w.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 := w.SetOffsetAndSize(1, start, w.Buffer().WriterIndex()-start); err != nil { panic(err) } rowBytes := w.ToBytes() // valid until the buffer is written to again
Call Reset before each row; to reuse a writer for a new top-level row, set the buffer's writer index back to zero first.
An Encoder owns a reusable write buffer and is not safe for concurrent use; create one encoder per goroutine or guard it with a mutex. Writers share the same rule. Row, ArrayData, and MapData views only read, so one view can be shared by concurrent readers as long as the underlying bytes are not modified.