diff --git a/docs/guide/go/basic-serialization.md b/docs/guide/go/basic-serialization.md new file mode 100644 index 0000000000..130e444eb3 --- /dev/null +++ b/docs/guide/go/basic-serialization.md @@ -0,0 +1,404 @@ +--- +title: Basic Serialization +sidebar_position: 20 +id: go_basic_serialization +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +This guide covers the core serialization APIs in Fory Go. + +## Creating a Fory Instance + +Create a Fory instance and register your types before serialization: + +```go +import "github.com/apache/fory/go/fory" + +f := fory.New() + +// Register struct with a type ID +f.RegisterStruct(User{}, 1) +f.RegisterStruct(Order{}, 2) + +// Or register with a name (more flexible, less prone to ID conflicts, but higher serialization cost) +f.RegisterNamedStruct(User{}, "example.User") + +// Register enum types +f.RegisterEnum(Color(0), 3) +``` + +**Important**: The Fory instance should be reused across serialization calls. Creating a new instance involves allocating internal buffers, type caches, and resolvers, which is expensive. The default Fory instance is not thread-safe; for concurrent usage, use the thread-safe wrapper (see [Thread Safety](thread-safety)). + +See [Type Registration](type-registration) for more details. + +## Core API + +### Serialize and Deserialize + +The primary API for serialization: + +```go +// Serialize any value +data, err := f.Serialize(value) +if err != nil { + // Handle error +} + +// Deserialize into target +var result MyType +err = f.Deserialize(data, &result) +if err != nil { + // Handle error +} +``` + +### Marshal and Unmarshal + +Aliases for `Serialize` and `Deserialize` (familiar to Go developers): + +```go +data, err := f.Marshal(value) +err = f.Unmarshal(data, &result) +``` + +## Serializing Primitives + +```go +// Integers +data, _ := f.Serialize(int64(42)) +var i int64 +f.Deserialize(data, &i) // i = 42 + +// Floats +data, _ = f.Serialize(float64(3.14)) +var fl float64 +f.Deserialize(data, &fl) // fl = 3.14 + +// Strings +data, _ = f.Serialize("hello") +var s string +f.Deserialize(data, &s) // s = "hello" + +// Booleans +data, _ = f.Serialize(true) +var b bool +f.Deserialize(data, &b) // b = true +``` + +## Serializing Collections + +### Slices + +```go +// String slice +strs := []string{"a", "b", "c"} +data, _ := f.Serialize(strs) + +var result []string +f.Deserialize(data, &result) +// result = ["a", "b", "c"] + +// Integer slice +nums := []int64{1, 2, 3} +data, _ = f.Serialize(nums) + +var intResult []int64 +f.Deserialize(data, &intResult) +// intResult = [1, 2, 3] +``` + +### Maps + +```go +// String to string map +m := map[string]string{"key": "value"} +data, _ := f.Serialize(m) + +var result map[string]string +f.Deserialize(data, &result) +// result = {"key": "value"} + +// String to int map +m2 := map[string]int64{"count": 42} +data, _ = f.Serialize(m2) + +var result2 map[string]int64 +f.Deserialize(data, &result2) +// result2 = {"count": 42} +``` + +## Serializing Structs + +### Basic Struct Serialization + +Only **exported fields** (starting with uppercase) are serialized: + +```go +type User struct { + ID int64 // Serialized + Name string // Serialized + password string // NOT serialized (unexported) +} + +f.RegisterStruct(User{}, 1) + +user := &User{ID: 1, Name: "Alice", password: "secret"} +data, _ := f.Serialize(user) + +var result User +f.Deserialize(data, &result) +// result.ID = 1, result.Name = "Alice", result.password = "" +``` + +### Nested Structs + +```go +type Address struct { + City string + Country string +} + +type Person struct { + Name string + Address Address +} + +f.RegisterStruct(Address{}, 1) +f.RegisterStruct(Person{}, 2) + +person := &Person{ + Name: "Alice", + Address: Address{City: "NYC", Country: "USA"}, +} + +data, _ := f.Serialize(person) + +var result Person +f.Deserialize(data, &result) +// result.Address.City = "NYC" +``` + +### Pointer Fields + +```go +type Node struct { + Value int32 + Child *Node +} + +// Use WithTrackRef for pointer fields +f := fory.New(fory.WithTrackRef(true)) +f.RegisterStruct(Node{}, 1) + +root := &Node{ + Value: 1, + Child: &Node{Value: 2, Child: nil}, +} + +data, _ := f.Serialize(root) + +var result Node +f.Deserialize(data, &result) +// result.Child.Value = 2 +``` + +## Streaming API + +For scenarios where you want to control the buffer: + +### SerializeTo + +Serialize to an existing buffer: + +```go +buf := fory.NewByteBuffer(nil) + +// Serialize multiple values to same buffer +f.SerializeTo(buf, value1) +f.SerializeTo(buf, value2) + +// Get all serialized data +data := buf.GetByteSlice(0, buf.WriterIndex()) +``` + +### DeserializeFrom + +Deserialize from an existing buffer: + +```go +buf := fory.NewByteBuffer(data) + +var result1, result2 MyType +f.DeserializeFrom(buf, &result1) +f.DeserializeFrom(buf, &result2) +``` + +## Generic API (Type-Safe) + +Fory Go provides generic functions for type-safe serialization: + +```go +import "github.com/apache/fory/go/fory" + +type User struct { + ID int64 + Name string +} + +// Type-safe serialization +user := &User{ID: 1, Name: "Alice"} +data, err := fory.Serialize(f, user) + +// Type-safe deserialization +var result User +err = fory.Deserialize(f, data, &result) +``` + +The generic API: + +- Infers type at compile time +- Provides better type safety +- May offer performance benefits + +## Error Handling + +Always check errors from serialization operations: + +```go +data, err := f.Serialize(value) +if err != nil { + switch e := err.(type) { + case fory.Error: + fmt.Printf("Fory error: %s (kind: %d)\n", e.Error(), e.Kind()) + default: + fmt.Printf("Unknown error: %v\n", err) + } + return +} + +err = f.Deserialize(data, &result) +if err != nil { + // Handle deserialization error +} +``` + +Common error kinds: + +- `ErrKindBufferOutOfBound`: Read/write beyond buffer bounds +- `ErrKindTypeMismatch`: Type ID mismatch during deserialization +- `ErrKindUnknownType`: Unknown type encountered +- `ErrKindMaxDepthExceeded`: Recursion depth limit exceeded +- `ErrKindHashMismatch`: Struct hash mismatch (schema changed) + +See [Troubleshooting](troubleshooting) for error resolution. + +## Nil Handling + +### Nil Pointers + +```go +var ptr *User = nil +data, _ := f.Serialize(ptr) + +var result *User +f.Deserialize(data, &result) +// result = nil +``` + +### Empty Collections + +```go +// Nil slice +var slice []string = nil +data, _ := f.Serialize(slice) + +var result []string +f.Deserialize(data, &result) +// result = nil + +// Empty slice (different from nil) +empty := []string{} +data, _ = f.Serialize(empty) + +f.Deserialize(data, &result) +// result = [] (empty, not nil) +``` + +## Complete Example + +```go +package main + +import ( + "fmt" + "github.com/apache/fory/go/fory" +) + +type Order struct { + ID int64 + Customer string + Items []Item + Total float64 +} + +type Item struct { + Name string + Quantity int32 + Price float64 +} + +func main() { + f := fory.New() + f.RegisterStruct(Order{}, 1) + f.RegisterStruct(Item{}, 2) + + order := &Order{ + ID: 12345, + Customer: "Alice", + Items: []Item{ + {Name: "Widget", Quantity: 2, Price: 9.99}, + {Name: "Gadget", Quantity: 1, Price: 24.99}, + }, + Total: 44.97, + } + + // Serialize + data, err := f.Serialize(order) + if err != nil { + panic(err) + } + fmt.Printf("Serialized %d bytes\n", len(data)) + + // Deserialize + var result Order + if err := f.Deserialize(data, &result); err != nil { + panic(err) + } + + fmt.Printf("Order ID: %d\n", result.ID) + fmt.Printf("Customer: %s\n", result.Customer) + fmt.Printf("Items: %d\n", len(result.Items)) + fmt.Printf("Total: %.2f\n", result.Total) +} +``` + +## Related Topics + +- [Configuration](configuration) +- [Type Registration](type-registration) +- [Supported Types](supported-types) +- [References](references) diff --git a/docs/guide/go/codegen.md b/docs/guide/go/codegen.md new file mode 100644 index 0000000000..dd08a751bf --- /dev/null +++ b/docs/guide/go/codegen.md @@ -0,0 +1,420 @@ +--- +title: Code Generation +sidebar_position: 90 +id: go_codegen +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +:::warning Experimental Feature +Code generation is an **experimental** feature in Fory Go. The API and behavior may change in future releases. The reflection-based path remains the stable, recommended approach for most use cases. +::: + +Fory Go provides optional ahead-of-time (AOT) code generation for performance-critical paths. This eliminates reflection overhead and provides compile-time type safety. + +## Why Code Generation? + +| Aspect | Reflection-Based | Code Generation | +| ----------- | ------------------ | ---------------------- | +| Setup | Zero configuration | Requires `go generate` | +| Performance | Good | Better (no reflection) | +| Type Safety | Runtime | Compile-time | +| Maintenance | Automatic | Requires regeneration | + +**Use code generation when**: + +- Maximum performance is required +- Compile-time type safety is important +- Hot paths are performance-critical + +**Use reflection when**: + +- Simple setup is preferred +- Types change frequently +- Dynamic typing is needed +- Code generation complexity is undesirable + +## Installation + +Install the `fory` generator binary: + +```bash +go install github.com/apache/fory/go/fory/cmd/fory@latest + +GO111MODULE=on go get -u github.com/apache/fory/go/fory/cmd/fory +``` + +Ensure `$GOBIN` or `$GOPATH/bin` is in your `PATH`. + +## Basic Usage + +### Step 1: Annotate Structs + +Add the `//fory:generate` comment above structs: + +```go +package models + +//fory:generate +type User struct { + ID int64 `json:"id"` + Name string `json:"name"` +} + +//fory:generate +type Order struct { + ID int64 + Customer string + Total float64 +} +``` + +### Step 2: Add Go Generate Directive + +Add a `go:generate` directive (once per file or package): + +```go +//go:generate fory -file models.go +``` + +Or for the entire package: + +```go +//go:generate fory -pkg . +``` + +### Step 3: Run Code Generation + +```bash +go generate ./... +``` + +This creates `models_fory_gen.go` with generated serializers. + +## Generated Code Structure + +The generator creates: + +### Type Snapshot + +A compile-time check to detect struct changes: + +```go +// Snapshot of User's underlying type at generation time +type _User_expected struct { + ID int64 + Name string +} + +// Compile-time check: fails if User no longer matches +var _ = func(x User) { _ = _User_expected(x) } +``` + +### Serializer Implementation + +Strongly-typed serialization methods: + +```go +type User_ForyGenSerializer struct{} + +func (User_ForyGenSerializer) WriteTyped(f *fory.Fory, buf *fory.ByteBuffer, v *User) error { + buf.WriteInt64(v.ID) + fory.WriteString(buf, v.Name) + return nil +} + +func (User_ForyGenSerializer) ReadTyped(f *fory.Fory, buf *fory.ByteBuffer, v *User) error { + v.ID = buf.ReadInt64() + v.Name = fory.ReadString(buf) + return nil +} +``` + +### Auto-Registration + +Serializers are registered in `init()`: + +```go +func init() { + fory.RegisterGenSerializer(User{}, User_ForyGenSerializer{}) +} +``` + +## Command-Line Options + +### File-Based Generation + +Generate for a specific file: + +```bash +fory -file models.go +``` + +### Package-Based Generation + +Generate for a package: + +```bash +fory -pkg ./models +``` + +### Explicit Types (Legacy) + +Specify types explicitly: + +```bash +fory -pkg ./models -type "User,Order" +``` + +### Force Regeneration + +Force regeneration even if up-to-date: + +```bash +fory --force -file models.go +``` + +## When to Regenerate + +Regenerate when any of these change: + +- Field additions, removals, or renames +- Field type changes +- Struct tag changes +- New structs with `//fory:generate` + +### Automatic Detection + +Fory includes a compile-time guard: + +```go +// If struct changed, this fails to compile +var _ = func(x User) { _ = _User_expected(x) } +``` + +If you forget to regenerate, the build fails with a clear message. + +### Auto-Retry + +When invoked via `go generate`, the generator detects stale code and retries: + +1. Detects compile error from guard +2. Removes stale generated file +3. Regenerates fresh code + +## Supported Types + +Code generation supports: + +- All primitive types (`bool`, `int*`, `uint*`, `float*`, `string`) +- Slices of primitives and structs +- Maps with supported key/value types +- Nested structs (must also be generated) +- Pointers to structs + +### Nested Structs + +All nested structs must also have `//fory:generate`: + +```go +//fory:generate +type Address struct { + City string + Country string +} + +//fory:generate +type Person struct { + Name string + Address Address // Address must also be generated +} +``` + +## CI/CD Integration + +### Check In Generated Code + +**Recommended for libraries**: + +```bash +go generate ./... +git add *_fory_gen.go +git commit -m "Regenerate Fory serializers" +``` + +**Pros**: Consumers can build without generator; reproducible builds +**Cons**: Larger diffs; must remember to regenerate + +### Generate in Pipeline + +**Recommended for applications**: + +```yaml +steps: + - run: go install github.com/apache/fory/go/fory/cmd/fory@latest + - run: go generate ./... + - run: go build ./... +``` + +## Usage with Generated Code + +Generated code integrates transparently: + +```go +f := fory.New() + +// Fory automatically uses generated serializer if available +user := &User{ID: 1, Name: "Alice"} +data, _ := f.Serialize(user) + +var result User +f.Deserialize(data, &result) +``` + +No code changes needed - registration happens in `init()`. + +## Mixing Generated and Non-Generated + +You can mix approaches: + +```go +//fory:generate +type HotPathStruct struct { + // Performance-critical, use codegen +} + +type ColdPathStruct struct { + // Not annotated, uses reflection +} +``` + +## Limitations + +### Experimental Status + +- API may change +- Not all edge cases tested +- May have undiscovered bugs + +### Not Supported + +- Interface fields (dynamic types) +- Recursive types without pointers +- Private (unexported) fields +- Custom serializers + +### Reflection Fallback + +If codegen fails, Fory falls back to reflection: + +```go +// If User_ForyGenSerializer not found, uses reflection +f.Serialize(&User{}) +``` + +## Troubleshooting + +### "fory: command not found" + +Ensure the binary is in PATH: + +```bash +export PATH=$PATH:$(go env GOPATH)/bin +``` + +### Compile Error After Struct Change + +Regenerate: + +```bash +go generate ./... +``` + +Or force: + +```bash +fory --force -file yourfile.go +``` + +### Generated Code Out of Sync + +The compile-time guard catches this: + +``` +cannot use x (variable of type User) as type _User_expected in argument +``` + +Run `go generate` to fix. + +## Example Project Structure + +``` +myproject/ +├── models/ +│ ├── models.go # Struct definitions +│ ├── models_fory_gen.go # Generated code +│ └── generate.go # go:generate directive +├── main.go +└── go.mod +``` + +**models/generate.go**: + +```go +package models + +//go:generate fory -pkg . +``` + +**models/models.go**: + +```go +package models + +//fory:generate +type User struct { + ID int64 + Name string +} +``` + +## FAQ + +### Is codegen required? + +No. Reflection-based serialization works without code generation. + +### Does generated code work across Go versions? + +Yes. Generated code is plain Go with no version-specific features. + +### Can I mix generated and non-generated types? + +Yes. Fory automatically uses generated serializers when available. + +### How do I update generated code? + +Run `go generate ./...` after struct changes. + +### Should I commit generated files? + +For libraries: yes. For applications: either works. + +## Related Topics + +- [Basic Serialization](basic-serialization) +- [Configuration](configuration) +- [Troubleshooting](troubleshooting) diff --git a/docs/guide/go/configuration.md b/docs/guide/go/configuration.md new file mode 100644 index 0000000000..f9414a7c65 --- /dev/null +++ b/docs/guide/go/configuration.md @@ -0,0 +1,344 @@ +--- +title: Configuration +sidebar_position: 10 +id: go_configuration +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Fory Go uses a functional options pattern for configuration. This allows you to customize serialization behavior while maintaining sensible defaults. + +## Creating a Fory Instance + +### Default Configuration + +```go +import "github.com/apache/fory/go/fory" + +f := fory.New() +``` + +Default settings: + +| Option | Default | Description | +| ---------- | ------- | ------------------------------ | +| TrackRef | false | Reference tracking disabled | +| MaxDepth | 20 | Maximum nesting depth | +| IsXlang | false | Cross-language mode disabled | +| Compatible | false | Schema evolution mode disabled | + +### With Options + +```go +f := fory.New( + fory.WithTrackRef(true), + fory.WithCompatible(true), + fory.WithMaxDepth(10), +) +``` + +## Configuration Options + +### WithTrackRef + +Enable reference tracking to handle circular references and shared objects: + +```go +f := fory.New(fory.WithTrackRef(true)) +``` + +**When enabled:** + +- Objects appearing multiple times are serialized once +- Circular references are handled correctly +- Per-field `fory:"ref"` tags take effect +- Adds overhead for tracking object identity + +**When disabled (default):** + +- Each object occurrence is serialized independently +- Circular references cause stack overflow or max depth error +- Per-field `fory:"ref"` tags are ignored +- Better performance for simple data structures + +**Use reference tracking when:** + +- Data contains circular references +- Same object is referenced multiple times +- Serializing graph structures (trees with parent pointers, linked lists with cycles) + +See [References](references) for details. + +### WithCompatible + +Enable compatible mode for schema evolution: + +```go +f := fory.New(fory.WithCompatible(true)) +``` + +**When enabled:** + +- Type metadata is written to serialized data +- Supports adding/removing fields between versions +- Field names or ids are used for matching (order-independent) +- Larger serialized output due to metadata + +**When disabled (default):** + +- Compact serialization without field metadata +- Faster serialization and smaller output +- Fields matched by sorted order +- Requires consistent struct definitions across all services + +See [Schema Evolution](schema-evolution) for details. + +### WithMaxDepth + +Set the maximum nesting depth to prevent stack overflow: + +```go +f := fory.New(fory.WithMaxDepth(30)) +``` + +- Default: 20 +- Protects against deeply nested, recursive structures or malicious data +- Serialization fails with error when exceeded + +### WithXlang + +Enable cross-language serialization mode: + +```go +f := fory.New(fory.WithXlang(true)) +``` + +**When enabled:** + +- Uses cross-language type system +- Compatible with Java, Python, C++, Rust, JavaScript +- Type IDs follow xlang specification + +**When disabled (default):** + +- Go-native serialization mode +- Support more Go-native types +- Not compatible with other language implementations + +## Thread Safety + +The default `Fory` instance is **NOT thread-safe**. For concurrent use, use the thread-safe wrapper: + +```go +import "github.com/apache/fory/go/fory/threadsafe" + +// Create thread-safe Fory with same options +f := threadsafe.New( + fory.WithTrackRef(true), + fory.WithCompatible(true), +) + +// Safe for concurrent use from multiple goroutines +go func() { + data, _ := f.Serialize(value1) + // data is already copied, safe to use after return +}() +go func() { + data, _ := f.Serialize(value2) +}() +``` + +The thread-safe wrapper: + +- Uses `sync.Pool` internally for efficient instance reuse +- Automatically copies serialized data before returning +- Accepts the same configuration options as `fory.New()` + +### Global Thread-Safe Instance + +For convenience, the threadsafe package provides global functions: + +```go +import "github.com/apache/fory/go/fory/threadsafe" + +// Uses a global thread-safe instance with default configuration +data, err := threadsafe.Marshal(&myValue) +err = threadsafe.Unmarshal(data, &result) +``` + +See [Thread Safety](thread-safety) for details. + +## Buffer Management + +### Zero-Copy Behavior + +The default `Fory` instance reuses its internal buffer: + +```go +f := fory.New() + +data1, _ := f.Serialize(value1) +// WARNING: data1 becomes invalid after next Serialize call! +data2, _ := f.Serialize(value2) +// data1 now points to invalid memory + +// To keep the data, copy it: +safeCopy := make([]byte, len(data1)) +copy(safeCopy, data1) +``` + +The thread-safe wrapper automatically copies data, so this is not a concern: + +```go +f := threadsafe.New() +data1, _ := f.Serialize(value1) +data2, _ := f.Serialize(value2) +// Both data1 and data2 are valid +``` + +### Manual Buffer Control + +For high-throughput scenarios, you can manage buffers manually: + +```go +f := fory.New() +buf := fory.NewByteBuffer(nil) + +// Serialize to existing buffer +err := f.SerializeTo(buf, value) + +// Get serialized data +data := buf.GetByteSlice(0, buf.WriterIndex()) + +// Process data... + +// Reset for next use +buf.Reset() +``` + +## Configuration Examples + +### Simple Data (Default) + +For simple structs without circular references: + +```go +f := fory.New() + +type Config struct { + Host string + Port int32 +} + +f.RegisterStruct(Config{}, 1) +data, _ := f.Serialize(&Config{Host: "localhost", Port: 8080}) +``` + +### Graph Structures + +For data with circular references: + +```go +f := fory.New(fory.WithTrackRef(true)) + +type Node struct { + Value int32 + Next *Node `fory:"ref"` +} + +f.RegisterStruct(Node{}, 1) +n1 := &Node{Value: 1} +n2 := &Node{Value: 2} +n1.Next = n2 +n2.Next = n1 // Circular reference + +data, _ := f.Serialize(n1) +``` + +### Schema Evolution + +For data that may evolve over time: + +```go +// V1: original struct +type UserV1 struct { + ID int64 + Name string +} + +// V2: added Email field +type UserV2 struct { + ID int64 + Name string + Email string // New field +} + +// Serialize with V1 +f1 := fory.New(fory.WithCompatible(true)) +f1.RegisterStruct(UserV1{}, 1) +data, _ := f1.Serialize(&UserV1{ID: 1, Name: "Alice"}) + +// Deserialize into V2 - Email will have zero value +f2 := fory.New(fory.WithCompatible(true)) +f2.RegisterStruct(UserV2{}, 1) +var user UserV2 +f2.Deserialize(data, &user) +``` + +### High-Performance Concurrent + +For concurrent high-throughput scenarios: + +```go +type Request struct { + ID int64 + Payload string +} + +f := threadsafe.New( + fory.WithMaxDepth(30), +) +f.RegisterStruct(Request{}, 1) + +// Process requests concurrently +for req := range requests { + go func(r Request) { + data, _ := f.Serialize(&r) + sendResponse(data) + }(req) +} +``` + +## Best Practices + +1. **Reuse Fory instances**: Creating a Fory instance involves initialization overhead. Create once and reuse. + +2. **Use thread-safe wrapper for concurrency**: Never share a non-thread-safe Fory instance across goroutines. + +3. **Enable reference tracking only when needed**: It adds overhead for tracking object identity. + +4. **Copy serialized data if keeping it**: With the default Fory, the returned byte slice is invalidated on the next operation. + +5. **Set appropriate max depth**: Increase for deeply nested structures, but be aware of memory usage. + +6. **Use compatible mode for evolving schemas**: Enable when struct definitions may change between service versions. + +## Related Topics + +- [Basic Serialization](basic-serialization) +- [References](references) +- [Schema Evolution](schema-evolution) +- [Thread Safety](thread-safety) diff --git a/docs/guide/go/cross-language.md b/docs/guide/go/cross-language.md new file mode 100644 index 0000000000..f10fa97c55 --- /dev/null +++ b/docs/guide/go/cross-language.md @@ -0,0 +1,282 @@ +--- +title: Cross-Language Serialization +sidebar_position: 80 +id: go_cross_language +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Fory Go enables seamless data exchange with Java, Python, C++, Rust, and JavaScript. This guide covers cross-language compatibility and type mapping. + +## Enabling Cross-Language Mode + +Cross-language (xlang) mode must be explicitly enabled: + +```go +f := fory.New(fory.WithXlang(true)) +``` + +## Type Registration for Cross-Language + +Use consistent type IDs across all languages: + +### Go + +```go +type User struct { + ID int64 + Name string +} + +f := fory.New(fory.WithXlang(true)) +f.RegisterStruct(User{}, 1) +data, _ := f.Serialize(&User{ID: 1, Name: "Alice"}) +``` + +### Java + +```java +public class User { + public long id; + public String name; +} +Fory fory = Fory.builder().withXlang(true).build(); +fory.register(User.class, 1); +User user = fory.deserialize(data, User.class); +``` + +### Python + +```python +from dataclasses import dataclass +import pyfory + +@dataclass +class User: + id: pyfory.Int64Type + name: str + +fory = pyfory.Fory() +fory.register(User, type_id=1) +user = fory.deserialize(data) +``` + +## Type Mapping + +See [Type Mapping Specification](https://fory.apache.org/docs/specification/xlang_type_mapping) for detailed type mappings across all languages. + +## Field Ordering + +Cross-language serialization requires consistent field ordering. Fory sorts fields by their snake_case names alphabetically. + +Go field names are converted to snake_case for sorting: + +```go +type Example struct { + UserID int64 // -> user_id + FirstName string // -> first_name + Age int32 // -> age +} + +// Sorted order: age, first_name, user_id +``` + +Ensure other languages use matching field names that produce the same snake_case ordering, or use field IDs for explicit control: + +```go +type Example struct { + UserID int64 `fory:"id=0"` + FirstName string `fory:"id=1"` + Age int32 `fory:"id=2"` +} +``` + +## Examples + +### Go to Java + +**Go (Serializer)**: + +```go +type Order struct { + ID int64 + Customer string + Total float64 + Items []string +} + +f := fory.New(fory.WithXlang(true)) +f.RegisterStruct(Order{}, 1) + +order := &Order{ + ID: 12345, + Customer: "Alice", + Total: 99.99, + Items: []string{"Widget", "Gadget"}, +} +data, _ := f.Serialize(order) +// Send 'data' to Java service +``` + +**Java (Deserializer)**: + +```java +public class Order { + public long id; + public String customer; + public double total; + public List items; +} + +Fory fory = Fory.builder().withXlang(true).build(); +fory.register(Order.class, 1); + +Order order = fory.deserialize(data, Order.class); +``` + +### Python to Go + +**Python (Serializer)**: + +```python +from dataclasses import dataclass +import pyfory + +@dataclass +class Message: + id: pyfory.Int64Type + content: str + timestamp: pyfory.Int64Type + +fory = pyfory.Fory() +fory.register(Message, type_id=1) + +msg = Message(id=1, content="Hello from Python", timestamp=1234567890) +data = fory.serialize(msg) +``` + +**Go (Deserializer)**: + +```go +type Message struct { + ID int64 + Content string + Timestamp int64 +} + +f := fory.New(fory.WithXlang(true)) +f.RegisterStruct(Message{}, 1) + +var msg Message +f.Deserialize(data, &msg) +fmt.Println(msg.Content) // "Hello from Python" +``` + +### Nested Structures + +Cross-language nested structures require all types to be registered: + +**Go**: + +```go +type Address struct { + Street string + City string + Country string +} + +type Company struct { + Name string + Address Address +} + +f := fory.New(fory.WithXlang(true)) +f.RegisterStruct(Address{}, 1) +f.RegisterStruct(Company{}, 2) +``` + +**Java**: + +```java +public class Address { + public String street; + public String city; + public String country; +} + +public class Company { + public String name; + public Address address; +} + +fory.register(Address.class, 1); +fory.register(Company.class, 2); +``` + +## Common Issues + +### Field Name Mismatch + +Go uses PascalCase, other languages may use camelCase or snake_case. Fields are matched by their snake_case conversion: + +```go +// Go +type User struct { + FirstName string // -> first_name +} + +// Java - field name converted to snake_case must match +public class User { + public String firstName; // -> first_name (matches) +} +``` + +### Type Interpretation + +Go unsigned types map to Java signed types with the same bit pattern: + +```go +var value uint64 = 18446744073709551615 // Max uint64 +``` + +Java's `long` holds the same bits but interprets as -1. Use `Long.toUnsignedString()` in Java if unsigned interpretation is needed. + +### Nil vs Null + +Go nil slices/maps serialize differently based on configuration: + +```go +var slice []string = nil +// In xlang mode: serializes based on nullable configuration +``` + +Ensure other languages handle null appropriately. + +## Best Practices + +1. **Use consistent type IDs**: Same numeric ID for the same type across all languages +2. **Register all types**: Including nested struct types +3. **Match field ordering**: Use same snake_case names or explicit field IDs +4. **Test cross-language**: Run integration tests early and often +5. **Handle type differences**: Be aware of signed/unsigned interpretation differences + +## Related Topics + +- [Type Registration](type-registration) +- [Supported Types](supported-types) +- [Schema Evolution](schema-evolution) +- [Xlang Serialization Specification](https://fory.apache.org/docs/specification/fory_xlang_serialization_spec/) +- [Type Mapping Specification](https://fory.apache.org/docs/specification/xlang_type_mapping) diff --git a/docs/guide/go/custom-serializers.md b/docs/guide/go/custom-serializers.md new file mode 100644 index 0000000000..3bf940faf0 --- /dev/null +++ b/docs/guide/go/custom-serializers.md @@ -0,0 +1,285 @@ +--- +title: Custom Serializers +sidebar_position: 35 +id: go_custom_serializers +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Custom serializers allow you to define exactly how a type is serialized and deserialized. This is useful for types that require special handling, optimization, or cross-language compatibility. + +## When to Use Custom Serializers + +- **Special encoding**: Types that need a specific binary format +- **Third-party types**: Types from external libraries that Fory doesn't handle automatically +- **Optimization**: When you can serialize more efficiently than the default reflection-based approach +- **Cross-language compatibility**: When you need precise control over the binary format for interoperability + +## ExtensionSerializer Interface + +Custom serializers implement the `ExtensionSerializer` interface: + +```go +type ExtensionSerializer interface { + // WriteData serializes the value to the buffer. + // Only write the data - Fory handles type info and references. + // Use ctx.Buffer() to access the ByteBuffer. + // Use ctx.SetError() to report errors. + WriteData(ctx *WriteContext, value reflect.Value) + + // ReadData deserializes the value from the buffer into the provided value. + // Only read the data - Fory handles type info and references. + // Use ctx.Buffer() to access the ByteBuffer. + // Use ctx.SetError() to report errors. + ReadData(ctx *ReadContext, value reflect.Value) +} +``` + +## Basic Example + +Here's a simple custom serializer for a type with an integer field: + +```go +import ( + "reflect" + "github.com/apache/fory/go/fory" +) + +type MyExt struct { + Id int32 +} + +type MyExtSerializer struct{} + +func (s *MyExtSerializer) WriteData(ctx *fory.WriteContext, value reflect.Value) { + myExt := value.Interface().(MyExt) + ctx.Buffer().WriteVarint32(myExt.Id) +} + +func (s *MyExtSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { + id := ctx.Buffer().ReadVarint32(ctx.Err()) + value.Set(reflect.ValueOf(MyExt{Id: id})) +} + +// Register the custom serializer +f := fory.New() +err := f.RegisterExtension(MyExt{}, 100, &MyExtSerializer{}) +``` + +## Context Methods + +The `WriteContext` and `ReadContext` provide access to serialization resources: + +| Method | Description | +| ---------------- | ---------------------------------------------- | +| `Buffer()` | Returns the `*ByteBuffer` for reading/writing | +| `Err()` | Returns `*Error` for deferred error checking | +| `SetError(err)` | Sets an error on the context | +| `HasError()` | Returns true if an error has been set | +| `TypeResolver()` | Returns the type resolver for nested types | +| `RefResolver()` | Returns the reference resolver for ref support | + +## ByteBuffer Methods + +The `ByteBuffer` provides methods for reading and writing primitive types: + +### Writing Methods + +| Method | Description | +| -------------------------- | --------------------------------------------- | +| `WriteBool(v bool)` | Write a boolean | +| `WriteInt8(v int8)` | Write a signed 8-bit integer | +| `WriteInt16(v int16)` | Write a signed 16-bit integer | +| `WriteInt32(v int32)` | Write a signed 32-bit integer | +| `WriteInt64(v int64)` | Write a signed 64-bit integer | +| `WriteFloat32(v float32)` | Write a 32-bit float | +| `WriteFloat64(v float64)` | Write a 64-bit float | +| `WriteVarint32(v int32)` | Write a variable-length signed 32-bit integer | +| `WriteVarint64(v int64)` | Write a variable-length signed 64-bit integer | +| `WriteBinary(data []byte)` | Write raw bytes | + +### Reading Methods + +All read methods take an `*Error` parameter for deferred error checking: + +| Method | Description | +| ------------------------------------------- | -------------------------------------------- | +| `ReadBool(err *Error) bool` | Read a boolean | +| `ReadInt8(err *Error) int8` | Read a signed 8-bit integer | +| `ReadInt16(err *Error) int16` | Read a signed 16-bit integer | +| `ReadInt32(err *Error) int32` | Read a signed 32-bit integer | +| `ReadInt64(err *Error) int64` | Read a signed 64-bit integer | +| `ReadFloat32(err *Error) float32` | Read a 32-bit float | +| `ReadFloat64(err *Error) float64` | Read a 64-bit float | +| `ReadVarint32(err *Error) int32` | Read a variable-length signed 32-bit integer | +| `ReadVarint64(err *Error) int64` | Read a variable-length signed 64-bit integer | +| `ReadBinary(length int, err *Error) []byte` | Read raw bytes of specified length | + +## Complex Type Example + +A custom serializer for a type with multiple fields: + +```go +type Point3D struct { + X, Y, Z float64 + Label string +} + +type Point3DSerializer struct{} + +func (s *Point3DSerializer) WriteData(ctx *fory.WriteContext, value reflect.Value) { + p := value.Interface().(Point3D) + buf := ctx.Buffer() + buf.WriteFloat64(p.X) + buf.WriteFloat64(p.Y) + buf.WriteFloat64(p.Z) + // Write string as length + bytes + labelBytes := []byte(p.Label) + buf.WriteVarint32(int32(len(labelBytes))) + buf.WriteBinary(labelBytes) +} + +func (s *Point3DSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { + buf := ctx.Buffer() + err := ctx.Err() + x := buf.ReadFloat64(err) + y := buf.ReadFloat64(err) + z := buf.ReadFloat64(err) + labelLen := buf.ReadVarint32(err) + labelBytes := buf.ReadBinary(int(labelLen), err) + value.Set(reflect.ValueOf(Point3D{ + X: x, + Y: y, + Z: z, + Label: string(labelBytes), + })) +} + +f := fory.New() +f.RegisterExtension(Point3D{}, 101, &Point3DSerializer{}) +``` + +## Handling Pointers + +When your type contains pointers, handle nil values explicitly: + +```go +type OptionalValue struct { + Value *int64 +} + +type OptionalValueSerializer struct{} + +func (s *OptionalValueSerializer) WriteData(ctx *fory.WriteContext, value reflect.Value) { + ov := value.Interface().(OptionalValue) + buf := ctx.Buffer() + if ov.Value == nil { + buf.WriteBool(false) // nil flag + } else { + buf.WriteBool(true) // not nil + buf.WriteInt64(*ov.Value) + } +} + +func (s *OptionalValueSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { + buf := ctx.Buffer() + err := ctx.Err() + hasValue := buf.ReadBool(err) + if !hasValue { + value.Set(reflect.ValueOf(OptionalValue{Value: nil})) + return + } + v := buf.ReadInt64(err) + value.Set(reflect.ValueOf(OptionalValue{Value: &v})) +} +``` + +## Error Handling + +Use `ctx.SetError()` to report errors: + +```go +func (s *MySerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { + buf := ctx.Buffer() + version := buf.ReadInt8(ctx.Err()) + if ctx.HasError() { + return + } + if version != 1 { + ctx.SetError(fory.DeserializationErrorf("unsupported version: %d", version)) + return + } + // Continue reading... + value.Set(reflect.ValueOf(result)) +} +``` + +## Registration Options + +### Register by ID + +More compact serialization, requires ID coordination across languages: + +```go +f.RegisterExtension(MyType{}, 100, &MySerializer{}) +``` + +### Register by Name + +More flexible but more serialization cost, type name included in serialized data: + +```go +f.RegisterNamedExtension(MyType{}, "myapp.MyType", &MySerializer{}) +``` + +## Best Practices + +1. **Keep it simple**: Only serialize what you need +2. **Use variable-length integers**: `WriteVarint32`/`WriteVarint64` for integers that are often small +3. **Handle nil explicitly**: Check for nil pointers and slices +4. **Version your format**: Consider adding a version byte for future compatibility +5. **Test round-trips**: Always verify that `Read(Write(value)) == value` +6. **Match read/write order**: Read fields in exactly the same order you write them +7. **Check errors**: Use `ctx.HasError()` after reading to handle errors gracefully +8. **Deploy before use**: Always deploy the registered serializer to all services before sending data serialized with it. If a service receives data for an unregistered serializer, deserialization will fail + +## Testing Custom Serializers + +```go +func TestMySerializer(t *testing.T) { + f := fory.New() + f.RegisterExtension(MyType{}, 100, &MySerializer{}) + + original := MyType{Field: "test"} + + // Serialize + data, err := f.Serialize(original) + require.NoError(t, err) + + // Deserialize + var result MyType + err = f.Deserialize(data, &result) + require.NoError(t, err) + + assert.Equal(t, original, result) +} +``` + +## Related Topics + +- [Type Registration](type-registration) +- [Supported Types](supported-types) +- [Cross-Language Serialization](cross-language) diff --git a/docs/guide/go/index.md b/docs/guide/go/index.md new file mode 100644 index 0000000000..cbfe87afe6 --- /dev/null +++ b/docs/guide/go/index.md @@ -0,0 +1,162 @@ +--- +title: Overview +sidebar_position: 0 +id: go_index +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Apache Fory Go is a high-performance, cross-language serialization library for Go. It provides automatic object graph serialization with support for circular references, polymorphism, and cross-language compatibility. + +## Why Fory Go? + +- **High Performance**: Fast serialization and optimized binary protocols +- **Cross-Language**: Seamless data exchange with Java, Python, C++, Rust, and JavaScript +- **Automatic Serialization**: No IDL definitions or schema compilation required +- **Reference Tracking**: Built-in support for circular references and shared objects +- **Type Safety**: Strong typing with compile-time verification (optional codegen) +- **Schema Evolution**: Compatible mode for forward/backward compatibility +- **Thread-Safe Option**: Pool-based thread-safe wrapper for concurrent use + +## Quick Start + +### Installation + +```bash +go get github.com/apache/fory/go/fory +``` + +### Basic Usage + +```go +package main + +import ( + "fmt" + "github.com/apache/fory/go/fory" +) + +type User struct { + ID int64 + Name string + Age int32 +} + +func main() { + // Create a Fory instance + f := fory.New() + + // Register struct with a type ID + if err := f.RegisterStruct(User{}, 1); err != nil { + panic(err) + } + + // Serialize + user := &User{ID: 1, Name: "Alice", Age: 30} + data, err := f.Serialize(user) + if err != nil { + panic(err) + } + + // Deserialize + var result User + if err := f.Deserialize(data, &result); err != nil { + panic(err) + } + + fmt.Printf("Deserialized: %+v\n", result) + // Output: Deserialized: {ID:1 Name:Alice Age:30} +} +``` + +## Architecture + +Fory Go provides two serialization paths: + +### Reflection-Based (Default) + +The default path uses Go's reflection to inspect types at runtime. This works out-of-the-box with any struct. Although this mode uses reflection, it is highly optimized with type caching, inlined hot paths, delivering excellent performance for most use cases: + +```go +f := fory.New() +data, _ := f.Serialize(myStruct) +``` + +### Code Generation (Experimental) + +For performance-critical paths, Fory provides optional ahead-of-time code generation that eliminates reflection overhead. See the [Code Generation](codegen) guide for details. + +## Configuration + +Fory Go uses a functional options pattern for configuration: + +```go +f := fory.New( + fory.WithTrackRef(true), // Enable reference tracking + fory.WithCompatible(true), // Enable schema evolution + fory.WithMaxDepth(20), // Set max nesting depth +) +``` + +See [Configuration](configuration) for all available options. + +## Supported Types + +Fory Go supports a wide range of types: + +- **Primitives**: `bool`, `int8`-`int64`, `uint8`-`uint64`, `float32`, `float64`, `string` +- **Collections**: slices, maps, sets +- **Time**: `time.Time`, `time.Duration` +- **Pointers**: pointer types with automatic nil handling +- **Structs**: any struct with exported fields + +See [Supported Types](supported-types) for the complete type mapping. + +## Cross-Language Serialization + +Fory Go is fully compatible with other Fory implementations. Data serialized in Go can be deserialized in Java, Python, C++, Rust, or JavaScript: + +```go +// Go serialization +f := fory.New() +f.RegisterStruct(User{}, 1) +data, _ := f.Serialize(&User{ID: 1, Name: "Alice"}) +// 'data' can be deserialized by Java, Python, etc. +``` + +See [Cross-Language Serialization](cross-language) for type mapping and compatibility details. + +## Documentation + +| Topic | Description | +| ------------------------------------------ | -------------------------------------- | +| [Configuration](configuration) | Options and settings | +| [Basic Serialization](basic-serialization) | Core APIs and usage patterns | +| [Type Registration](type-registration) | Registering types for serialization | +| [Supported Types](supported-types) | Complete type support reference | +| [References](references) | Circular references and shared objects | +| [Struct Tags](struct-tags) | Field-level configuration | +| [Schema Evolution](schema-evolution) | Forward/backward compatibility | +| [Cross-Language](cross-language) | Multi-language serialization | +| [Code Generation](codegen) | Experimental AOT code generation | +| [Thread Safety](thread-safety) | Concurrent usage patterns | +| [Troubleshooting](troubleshooting) | Common issues and solutions | + +## Related Resources + +- [Xlang Serialization Specification](https://fory.apache.org/docs/specification/fory_xlang_serialization_spec) +- [Cross-Language Type Mapping](https://fory.apache.org/docs/specification/xlang_type_mapping) +- [GitHub Repository](https://github.com/apache/fory) diff --git a/docs/guide/go/references.md b/docs/guide/go/references.md new file mode 100644 index 0000000000..c1b34d2f66 --- /dev/null +++ b/docs/guide/go/references.md @@ -0,0 +1,356 @@ +--- +title: References +sidebar_position: 50 +id: go_references +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Fory Go supports reference tracking to handle circular references and shared objects. This is essential for serializing complex data structures like graphs, trees with parent pointers, and linked lists with cycles. + +## Enabling Reference Tracking + +Reference tracking is **disabled by default**. Enable it when creating a Fory instance: + +```go +f := fory.New(fory.WithTrackRef(true)) +``` + +**Important**: Global reference tracking must be enabled for any reference tracking to occur. When `WithTrackRef(false)` (the default), all per-field reference tags are ignored. + +## How Reference Tracking Works + +### Without Reference Tracking (Default) + +When disabled, each object is serialized independently: + +```go +f := fory.New() // TrackRef disabled by default + +shared := &Data{Value: 42} +container := &Container{A: shared, B: shared} + +data, _ := f.Serialize(container) +// 'shared' is serialized TWICE (no deduplication) +``` + +### With Reference Tracking + +When enabled, objects are tracked by identity: + +```go +f := fory.New(fory.WithTrackRef(true)) + +shared := &Data{Value: 42} +container := &Container{A: shared, B: shared} + +data, _ := f.Serialize(container) +// 'shared' is serialized ONCE, second occurrence is a reference +``` + +## Reference Flags + +Fory uses flags to indicate reference states during serialization: + +| Flag | Value | Meaning | +| ------------------ | ----- | ----------------------------------------- | +| `NullFlag` | -3 | Nil/null value | +| `RefFlag` | -2 | Reference to previously serialized object | +| `NotNullValueFlag` | -1 | Non-null value (data follows) | +| `RefValueFlag` | 0 | Reference value flag | + +## Referenceable Types + +Only certain types support reference tracking. In xlang mode, the following types can track references: + +| Type | Reference Tracked | Notes | +| ----------------------------- | ----------------- | ------------------------------ | +| `*struct` (pointer to struct) | Yes | Enable with `fory:"ref"` tag | +| `any` (interface) | Yes | Automatically tracked | +| `[]T` (slices) | Yes | Enable with `fory:"ref"` tag | +| `map[K]V` | Yes | Enable with `fory:"ref"` tag | +| `*int`, `*string`, etc. | No | Pointer to primitives excluded | +| Primitives | No | Value types | +| `time.Time`, `time.Duration` | No | Value types | +| Arrays (`[N]T`) | No | Value types | + +## Per-Field Reference Control + +By default, reference tracking is **disabled** for individual fields even when global `WithTrackRef(true)` is set. You can enable reference tracking for specific fields using the `ref` struct tag: + +```go +type Container struct { + // Enable ref tracking for this field + SharedData *Data `fory:"ref"` + + // Explicitly disable ref tracking (same as default) + SimpleData *Data `fory:"ref=false"` +} +``` + +**Important notes**: + +- Per-field tags only take effect when global `WithTrackRef(true)` is set +- When global `WithTrackRef(false)` (default), all field ref tags are ignored +- Applies to slices, maps, and pointer to struct fields +- Pointer to primitive types (e.g., `*int`, `*string`) cannot use this tag +- Default is `ref=false` (no reference tracking per field) + +See [Struct Tags](struct-tags) for more details. + +## Circular References + +Reference tracking is required for circular data structures: + +### Circular Linked List + +```go +type Node struct { + Value int32 + Next *Node `fory:"ref"` +} + +f := fory.New(fory.WithTrackRef(true)) +f.RegisterStruct(Node{}, 1) + +// Create circular list +n1 := &Node{Value: 1} +n2 := &Node{Value: 2} +n3 := &Node{Value: 3} +n1.Next = n2 +n2.Next = n3 +n3.Next = n1 // Circular reference back to n1 + +data, _ := f.Serialize(n1) + +var result Node +f.Deserialize(data, &result) +// Circular structure is preserved +// result.Next.Next.Next == &result +``` + +### Parent-Child Tree + +```go +type TreeNode struct { + Value string + Parent *TreeNode `fory:"ref"` + Children []*TreeNode `fory:"ref"` +} + +f := fory.New(fory.WithTrackRef(true)) +f.RegisterStruct(TreeNode{}, 1) + +root := &TreeNode{Value: "root"} +child1 := &TreeNode{Value: "child1", Parent: root} +child2 := &TreeNode{Value: "child2", Parent: root} +root.Children = []*TreeNode{child1, child2} + +data, _ := f.Serialize(root) + +var result TreeNode +f.Deserialize(data, &result) +// result.Children[0].Parent == &result +``` + +### Graph Structures + +```go +type GraphNode struct { + ID int32 + Neighbors []*GraphNode `fory:"ref"` +} + +f := fory.New(fory.WithTrackRef(true)) +f.RegisterStruct(GraphNode{}, 1) + +// Create a graph +a := &GraphNode{ID: 1} +b := &GraphNode{ID: 2} +c := &GraphNode{ID: 3} + +// Bidirectional connections +a.Neighbors = []*GraphNode{b, c} +b.Neighbors = []*GraphNode{a, c} +c.Neighbors = []*GraphNode{a, b} + +data, _ := f.Serialize(a) + +var result GraphNode +f.Deserialize(data, &result) +``` + +## Shared Object Deduplication + +Reference tracking also deduplicates shared objects: + +```go +type Config struct { + Setting string +} + +type Application struct { + MainConfig *Config `fory:"ref"` + BackupConfig *Config `fory:"ref"` + FallbackConfig *Config `fory:"ref"` +} + +f := fory.New(fory.WithTrackRef(true)) +f.RegisterStruct(Config{}, 1) +f.RegisterStruct(Application{}, 2) + +// Shared configuration +config := &Config{Setting: "value"} + +// Multiple references to same object +app := &Application{ + MainConfig: config, + BackupConfig: config, + FallbackConfig: config, +} + +data, _ := f.Serialize(app) +// 'config' serialized once, others are references + +var result Application +f.Deserialize(data, &result) +// result.MainConfig == result.BackupConfig == result.FallbackConfig +``` + +## Performance Considerations + +### Overhead + +Reference tracking adds overhead: + +- Memory for tracking seen objects (hash map) +- Hash lookups during serialization +- Additional bytes for reference flags and IDs + +### When to Enable + +**Enable reference tracking when**: + +- Data has circular references +- Same object referenced multiple times +- Serializing graph structures +- Object identity must be preserved + +**Disable reference tracking when**: + +- Data is tree-structured (no cycles) +- Each object appears only once +- Maximum performance is required +- Object identity doesn't matter + +### Memory Usage + +Reference tracking maintains a map of serializing objects: + +```go +// Internal reference tracking structure +type RefResolver struct { + writtenObjects map[refKey]int32 // pointer -> reference ID + readObjects []reflect.Value // reference ID -> object +} +``` + +For large object graphs, this may increase memory usage. + +## Error Handling + +### Without Reference Tracking + +Circular references without tracking cause stack overflow or max depth errors: + +```go +f := fory.New() // No reference tracking + +n1 := &Node{Value: 1} +n1.Next = n1 // Self-reference + +data, err := f.Serialize(n1) +// Error: max depth exceeded (or stack overflow) +``` + +### Invalid Reference ID + +During deserialization, an invalid reference ID produces an error: + +```go +// Error type: ErrKindInvalidRefId +``` + +This occurs when serialized data contains a reference to an object that wasn't previously serialized. + +## Complete Example + +```go +package main + +import ( + "fmt" + "github.com/apache/fory/go/fory" +) + +type Person struct { + Name string + Friends []*Person `fory:"ref"` + BestFriend *Person `fory:"ref"` +} + +func main() { + f := fory.New(fory.WithTrackRef(true)) + f.RegisterStruct(Person{}, 1) + + // Create people with mutual friendships + alice := &Person{Name: "Alice"} + bob := &Person{Name: "Bob"} + charlie := &Person{Name: "Charlie"} + + alice.Friends = []*Person{bob, charlie} + alice.BestFriend = bob + + bob.Friends = []*Person{alice, charlie} + bob.BestFriend = alice // Mutual best friends + + charlie.Friends = []*Person{alice, bob} + + // Serialize + data, err := f.Serialize(alice) + if err != nil { + panic(err) + } + fmt.Printf("Serialized %d bytes\n", len(data)) + + // Deserialize + var result Person + if err := f.Deserialize(data, &result); err != nil { + panic(err) + } + + // Verify circular references preserved + fmt.Printf("Alice's best friend: %s\n", result.BestFriend.Name) + fmt.Printf("Bob's best friend: %s\n", result.BestFriend.BestFriend.Name) + // Output: Alice (circular reference preserved) +} +``` + +## Related Topics + +- [Configuration](configuration) +- [Struct Tags](struct-tags) +- [Cross-Language Serialization](cross-language) diff --git a/docs/guide/go/schema-evolution.md b/docs/guide/go/schema-evolution.md new file mode 100644 index 0000000000..5624e81043 --- /dev/null +++ b/docs/guide/go/schema-evolution.md @@ -0,0 +1,340 @@ +--- +title: Schema Evolution +sidebar_position: 70 +id: go_schema_evolution +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Schema evolution allows your data structures to change over time while maintaining compatibility with previously serialized data. Fory Go supports this through compatible mode. + +## Enabling Compatible Mode + +Enable compatible mode when creating a Fory instance: + +```go +f := fory.New(fory.WithCompatible(true)) +``` + +## How It Works + +### Without Compatible Mode (Default) + +- Compact serialization without metadata +- Struct hash is checked during deserialization +- Any schema change causes `ErrKindHashMismatch` + +### With Compatible Mode + +- Type metadata is written to serialized data +- Supports adding, removing, and reordering fields +- Enables forward and backward compatibility + +## Supported Schema Changes + +### Adding Fields + +New fields can be added; they receive zero values when deserializing old data: + +```go +// Version 1 +type UserV1 struct { + ID int64 + Name string +} + +// Version 2 (added Email) +type UserV2 struct { + ID int64 + Name string + Email string // New field +} + +f := fory.New(fory.WithCompatible(true)) +f.RegisterStruct(UserV1{}, 1) + +// Serialize with V1 +userV1 := &UserV1{ID: 1, Name: "Alice"} +data, _ := f.Serialize(userV1) + +// Deserialize with V2 +f2 := fory.New(fory.WithCompatible(true)) +f2.RegisterStruct(UserV2{}, 1) + +var userV2 UserV2 +f2.Deserialize(data, &userV2) +// userV2.Email = "" (zero value) +``` + +### Removing Fields + +Removed fields are skipped during deserialization: + +```go +// Version 1 +type ConfigV1 struct { + Host string + Port int32 + Timeout int64 + Debug bool // Will be removed +} + +// Version 2 (removed Debug) +type ConfigV2 struct { + Host string + Port int32 + Timeout int64 + // Debug field removed +} + +f := fory.New(fory.WithCompatible(true)) +f.RegisterStruct(ConfigV1{}, 1) + +// Serialize with V1 +config := &ConfigV1{Host: "localhost", Port: 8080, Timeout: 30, Debug: true} +data, _ := f.Serialize(config) + +// Deserialize with V2 +f2 := fory.New(fory.WithCompatible(true)) +f2.RegisterStruct(ConfigV2{}, 1) + +var configV2 ConfigV2 +f2.Deserialize(data, &configV2) +// Debug field data is skipped +``` + +### Reordering Fields + +Field order can change between versions: + +```go +// Version 1 +type PersonV1 struct { + FirstName string + LastName string + Age int32 +} + +// Version 2 (reordered) +type PersonV2 struct { + Age int32 // Moved up + LastName string + FirstName string // Moved down +} +``` + +Compatible mode handles this automatically by matching fields by name. + +## Incompatible Changes + +Some changes are NOT supported, even in compatible mode: + +### Type Changes + +```go +// NOT SUPPORTED +type V1 struct { + Value int32 // int32 +} + +type V2 struct { + Value string // Changed to string - INCOMPATIBLE +} +``` + +### Renaming Fields + +```go +// NOT SUPPORTED (treated as remove + add) +type V1 struct { + UserName string +} + +type V2 struct { + Username string // Different name - NOT a rename +} +``` + +This is treated as removing `UserName` and adding `Username`, resulting in data loss. + +## Best Practices + +### 1. Use Compatible Mode for Persistent Data + +```go +// For data stored in databases, files, or caches +f := fory.New(fory.WithCompatible(true)) +``` + +### 2. Provide Default Values + +```go +type ConfigV2 struct { + Host string + Port int32 + Timeout int64 + Retries int32 // New field +} + +func NewConfigV2() *ConfigV2 { + return &ConfigV2{ + Retries: 3, // Default value + } +} + +// After deserialize, apply defaults +if config.Retries == 0 { + config.Retries = 3 +} +``` + +## Cross-Language Schema Evolution + +Schema evolution works across languages: + +### Go (Producer) + +```go +type MessageV1 struct { + ID int64 + Content string +} + +f := fory.New(fory.WithCompatible(true)) +f.RegisterStruct(MessageV1{}, 1) +data, _ := f.Serialize(&MessageV1{ID: 1, Content: "Hello"}) +``` + +### Java (Consumer with newer schema) + +```java +public class Message { + long id; + String content; + String author; // New field in Java +} + +Fory fory = Fory.builder() + .withXlang(true) + .withCompatibleMode(true) + .build(); +fory.register(Message.class, 1); +Message msg = fory.deserialize(data, Message.class); +// msg.author will be null +``` + +## Performance Considerations + +Compatible mode mainly affects serialized size: + +| Aspect | Schema Consistent | Compatible Mode | +| ------------------ | ----------------- | -------------------------------------------------------- | +| Serialized Size | Smaller | Larger (includes metadata, especially without field IDs) | +| Speed | Fast | Similar (metadata is just memcpy) | +| Schema Flexibility | None | Full | + +**Note**: Using field IDs (`fory:"id=N"`) reduces metadata size in compatible mode. + +**Recommendation**: Use compatible mode for: + +- Persistent storage +- Cross-service communication +- Long-lived caches + +Use schema consistent mode for: + +- In-memory operations +- Same-version communication +- Minimum serialized size + +## Error Handling + +### Hash Mismatch (Schema Consistent Mode) + +```go +f := fory.New() // Compatible mode disabled + +// Schema changed without compatible mode +err := f.Deserialize(oldData, &newStruct) +// Error: ErrKindHashMismatch +``` + +### Unknown Fields + +In compatible mode, unknown fields are skipped silently. To detect them: + +```go +// Currently, Fory skips unknown fields automatically +// No explicit API for detecting unknown fields +``` + +## Complete Example + +```go +package main + +import ( + "fmt" + "github.com/apache/fory/go/fory" +) + +// V1: Initial schema +type ProductV1 struct { + ID int64 + Name string + Price float64 +} + +// V2: Added fields +type ProductV2 struct { + ID int64 + Name string + Price float64 + Description string // New + InStock bool // New +} + +func main() { + // Serialize with V1 + f1 := fory.New(fory.WithCompatible(true)) + f1.RegisterStruct(ProductV1{}, 1) + + product := &ProductV1{ID: 1, Name: "Widget", Price: 9.99} + data, _ := f1.Serialize(product) + fmt.Printf("V1 serialized: %d bytes\n", len(data)) + + // Deserialize with V2 + f2 := fory.New(fory.WithCompatible(true)) + f2.RegisterStruct(ProductV2{}, 1) + + var productV2 ProductV2 + if err := f2.Deserialize(data, &productV2); err != nil { + panic(err) + } + + fmt.Printf("ID: %d\n", productV2.ID) + fmt.Printf("Name: %s\n", productV2.Name) + fmt.Printf("Price: %.2f\n", productV2.Price) + fmt.Printf("Description: %q (zero value)\n", productV2.Description) + fmt.Printf("InStock: %v (zero value)\n", productV2.InStock) +} +``` + +## Related Topics + +- [Configuration](configuration) +- [Cross-Language Serialization](cross-language) +- [Troubleshooting](troubleshooting) diff --git a/docs/guide/go/struct-tags.md b/docs/guide/go/struct-tags.md new file mode 100644 index 0000000000..6075848efe --- /dev/null +++ b/docs/guide/go/struct-tags.md @@ -0,0 +1,335 @@ +--- +title: Struct Tags +sidebar_position: 60 +id: go_struct_tags +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Fory Go uses struct tags to customize field-level serialization behavior. This allows fine-grained control over how individual fields are serialized. + +## Tag Syntax + +The general syntax for Fory struct tags: + +```go +type MyStruct struct { + Field Type `fory:"option1,option2=value"` +} +``` + +Multiple options are separated by commas (`,`). + +## Available Tags + +### Field ID + +Use `id=N` to assign a numeric ID to a field for compact encoding: + +```go +type User struct { + ID int64 `fory:"id=0"` + Name string `fory:"id=1"` + Age int32 `fory:"id=2"` +} +``` + +**Benefits**: + +- Smaller serialized size (numeric IDs vs field names) +- Faster serialization/deserialization +- Required for optimal cross-language compatibility + +**Notes**: + +- IDs must be unique within a struct +- IDs must be >= 0 +- If not specified, field name is used (larger payload) + +### Ignoring Fields + +Use `-` to exclude a field from serialization: + +```go +type User struct { + ID int64 + Name string + Password string `fory:"-"` // Not serialized +} +``` + +The `Password` field will not be included in serialized output and will remain at its zero value after deserialization. + +### Nullable + +Use `nullable` to control whether null flags are written for pointer fields: + +```go +type Record struct { + // Write null flag for this field (allows nil values) + OptionalData *Data `fory:"nullable"` + + // Skip null flag (field must not be nil) + RequiredData *Data `fory:"nullable=false"` +} +``` + +**Notes**: + +- Only applies to pointer, slice, and map fields +- When `nullable=false`, serializing a nil value will cause an error +- Default is `false` (no null flag written) + +### Reference Tracking + +Control per-field reference tracking for slices, maps, or pointer to struct fields: + +```go +type Container struct { + // Enable reference tracking for this field + SharedData *Data `fory:"ref"` + + // Disable reference tracking for this field + SimpleData *Data `fory:"ref=false"` +} +``` + +**Notes**: + +- Applies to slices, maps, and pointer to struct fields +- Pointer to primitive types (e.g., `*int`, `*string`) cannot use this tag +- Default is `ref=false` (no reference tracking) +- When global `WithTrackRef(false)` is set, field ref tags are ignored +- When global `WithTrackRef(true)` is set, use `ref=false` to disable for specific fields + +**Use cases**: + +- Enable for fields that may be circular or shared +- Disable for fields that are always unique (optimization) + +### Encoding + +Use `encoding` to control how numeric fields are encoded: + +```go +type Metrics struct { + // Variable-length encoding (default, smaller for small values) + Count int64 `fory:"encoding=varint"` + + // Fixed-length encoding (consistent size) + Timestamp int64 `fory:"encoding=fixed"` + + // Tagged encoding (includes type tag) + Value int64 `fory:"encoding=tagged"` +} +``` + +**Supported encodings**: + +| Type | Options | Default | +| -------- | --------------------------- | -------- | +| `int32` | `varint`, `fixed` | `varint` | +| `uint32` | `varint`, `fixed` | `varint` | +| `int64` | `varint`, `fixed`, `tagged` | `varint` | +| `uint64` | `varint`, `fixed`, `tagged` | `varint` | + +**When to use**: + +- `varint`: Best for values that are often small (default) +- `fixed`: Best for values that use full range (e.g., timestamps, hashes) +- `tagged`: When type information needs to be preserved + +**Shorthand for int32/uint32**: + +Use `compress` as a convenience tag for int32/uint32 fields: + +```go +type Data struct { + SmallValue int32 `fory:"compress"` // Same as encoding=varint (default) + FixedValue uint32 `fory:"compress=false"` // Same as encoding=fixed +} +``` + +## Combining Tags + +Multiple tags can be combined using comma separator: + +```go +type Document struct { + ID int64 `fory:"id=0,encoding=fixed"` + Content string `fory:"id=1"` + Author *User `fory:"id=2,ref"` +} +``` + +## Integration with Other Tags + +Fory tags coexist with other struct tags: + +```go +type User struct { + ID int64 `json:"id" fory:"id=0"` + Name string `json:"name,omitempty" fory:"id=1"` + Password string `json:"-" fory:"-"` +} +``` + +Each tag namespace is independent. + +## Field Visibility + +Only **exported fields** (starting with uppercase) are considered: + +```go +type User struct { + ID int64 // Serialized + Name string // Serialized + password string // NOT serialized (unexported, no tag needed) +} +``` + +Unexported fields are always ignored, regardless of tags. + +## Field Ordering + +Fields are serialized in a consistent order based on: + +1. Field name (alphabetically in snake_case) +2. Field type + +This ensures cross-language compatibility where field order matters. + +## Struct Hash + +Fory computes a hash of struct fields for version checking: + +- Hash includes field names and types +- Hash is written to serialized data +- Mismatch triggers `ErrKindHashMismatch` + +Struct field changes affect the hash: + +```go +// These produce different hashes +type V1 struct { + UserID int64 +} + +type V2 struct { + UserId int64 // Different field name = different hash +} +``` + +## Examples + +### API Response Struct + +```go +type APIResponse struct { + Status int32 `json:"status" fory:"id=0"` + Message string `json:"message" fory:"id=1"` + Data any `json:"data" fory:"id=2"` + Internal string `json:"-" fory:"-"` // Ignored in both JSON and Fory +} +``` + +### Caching with Shared References + +```go +type CacheEntry struct { + Key string + Value *CachedData `fory:"ref"` // May be shared + Metadata *Metadata `fory:"ref=false"` // Always unique + ExpiresAt int64 +} +``` + +### Document with Circular References + +```go +type Document struct { + ID int64 + Title string + Parent *Document `fory:"ref"` // May reference self or siblings + Children []*Document `fory:"ref"` +} +``` + +## Tag Parsing Errors + +Invalid tags produce errors during registration: + +```go +type BadStruct struct { + Field int `fory:"invalid=option=format"` +} + +f := fory.New() +err := f.RegisterStruct(BadStruct{}, 1) +// Error: ErrKindInvalidTag +``` + +## Best Practices + +1. **Use `-` for sensitive data**: Passwords, tokens, internal state +2. **Enable ref tracking for shared objects**: When the same pointer appears multiple times +3. **Disable ref tracking for simple fields**: Optimization when you know the field is unique +4. **Keep names consistent**: Cross-language names should match +5. **Document tag usage**: Especially for non-obvious configurations + +## Common Patterns + +### Ignoring Computed Fields + +```go +type Rectangle struct { + Width float64 + Height float64 + Area float64 `fory:"-"` // Computed, don't serialize +} + +func (r *Rectangle) ComputeArea() { + r.Area = r.Width * r.Height +} +``` + +### Circular Structure with Parent + +```go +type TreeNode struct { + Value string + Parent *TreeNode `fory:"ref"` // Circular back-reference + Children []*TreeNode `fory:"ref"` +} +``` + +### Mixed Serialization Needs + +```go +type Session struct { + ID string + UserID int64 + Token string `fory:"-"` // Security: don't serialize + User *User `fory:"ref"` // May be shared across sessions + CreatedAt int64 +} +``` + +## Related Topics + +- [References](references) +- [Basic Serialization](basic-serialization) +- [Schema Evolution](schema-evolution) diff --git a/docs/guide/go/supported-types.md b/docs/guide/go/supported-types.md new file mode 100644 index 0000000000..5b68d50c74 --- /dev/null +++ b/docs/guide/go/supported-types.md @@ -0,0 +1,369 @@ +--- +title: Supported Types +sidebar_position: 40 +id: go_supported_types +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Fory Go supports a wide range of Go types for serialization. This guide covers all supported types and their cross-language mappings. + +## Primitive Types + +| Go Type | Fory TypeId | Encoding | Notes | +| ---------------- | ------------ | --------------------- | --------------------------------- | +| `bool` | BOOL (1) | 1 byte | | +| `int8` | INT8 (2) | 1 byte, signed | | +| `int16` | INT16 (3) | 2 bytes, signed | Little-endian | +| `int32` | INT32 (4) | Varint | Variable-length encoding | +| `int64` | INT64 (6) | Varint | Variable-length encoding | +| `int` | INT32/INT64 | Varint | Platform-dependent (32 or 64 bit) | +| `uint8` / `byte` | UINT8 (9) | 1 byte, unsigned | | +| `uint16` | UINT16 (10) | 2 bytes, unsigned | Little-endian | +| `uint32` | UINT32 (11) | Varuint | Variable-length encoding | +| `uint64` | UINT64 (13) | Varuint | Variable-length encoding | +| `float32` | FLOAT32 (17) | 4 bytes | IEEE 754 | +| `float64` | FLOAT64 (18) | 8 bytes | IEEE 754 | +| `string` | STRING (19) | Length-prefixed UTF-8 | | + +### Integer Encoding + +Fory uses variable-length integer encoding (varint) for better compression: + +- Small values use fewer bytes +- Negative values use ZigZag encoding +- Platform `int` maps to `int32` on 32-bit, `int64` on 64-bit systems + +```go +f := fory.New() + +// All integer types supported +var i8 int8 = 127 +var i16 int16 = 32767 +var i32 int32 = 2147483647 +var i64 int64 = 9223372036854775807 + +data, _ := f.Serialize(i64) // Uses varint encoding +``` + +## Collection Types + +### Slices + +| Go Type | Fory TypeId | Notes | +| --------------- | ------------- | --------------------- | +| `[]bool` | BOOL_ARRAY | Optimized encoding | +| `[]int8` | INT8_ARRAY | Optimized encoding | +| `[]int16` | INT16_ARRAY | Optimized encoding | +| `[]int32` | INT32_ARRAY | Optimized encoding | +| `[]int64` | INT64_ARRAY | Optimized encoding | +| `[]float32` | FLOAT32_ARRAY | Optimized encoding | +| `[]float64` | FLOAT64_ARRAY | Optimized encoding | +| `[]string` | LIST | Generic list encoding | +| `[]T` (any) | LIST (20) | Any serializable type | +| `[]I` (any/any) | LIST | Any interface type | + +```go +f := fory.New() + +// Primitive slices (optimized) +ints := []int32{1, 2, 3, 4, 5} +data, _ := f.Serialize(ints) + +// String slices +strs := []string{"a", "b", "c"} +data, _ = f.Serialize(strs) + +// Struct slices +users := []User{{ID: 1}, {ID: 2}} +data, _ = f.Serialize(users) + +// Dynamic slices +dynamic := []any{1, "hello", true} +data, _ = f.Serialize(dynamic) +``` + +### Maps + +| Go Type | Fory TypeId | Notes | +| -------------------- | ----------- | ----------------------- | +| `map[string]string` | MAP (22) | Optimized | +| `map[string]int64` | MAP | Optimized | +| `map[string]int32` | MAP | Optimized | +| `map[string]int` | MAP | Optimized | +| `map[string]float64` | MAP | Optimized | +| `map[string]bool` | MAP | Optimized | +| `map[int32]int32` | MAP | Optimized | +| `map[int64]int64` | MAP | Optimized | +| `map[int]int` | MAP | Optimized | +| `map[string]any` | MAP | Dynamic values | +| `map[any]any` | MAP | Dynamic keys and values | + +```go +f := fory.New() + +// String key maps +m1 := map[string]string{"key": "value"} +m2 := map[string]int64{"count": 42} + +// Integer key maps +m3 := map[int32]int32{1: 100, 2: 200} + +// Dynamic maps +m4 := map[string]any{ + "name": "Alice", + "age": int64(30), +} +``` + +### Sets + +Fory provides a generic `Set[T]` type (uses `map[T]struct{}` for zero memory overhead): + +```go +// Create a set of strings +s := fory.NewSet[string]() +s.Add("a", "b", "c") + +// Check membership +if s.Contains("a") { + fmt.Println("found") +} + +// Serialize +data, _ := f.Serialize(s) +``` + +## Time Types + +| Go Type | Fory TypeId | Notes | +| --------------- | -------------- | -------------------- | +| `time.Time` | TIMESTAMP (34) | Nanosecond precision | +| `time.Duration` | DURATION (33) | Nanosecond precision | + +```go +import "time" + +f := fory.New() + +// Timestamp +t := time.Now() +data, _ := f.Serialize(t) + +// Duration +d := 5 * time.Second +data, _ = f.Serialize(d) +``` + +## Struct Types + +| Category | Fory TypeId | Notes | +| ----------------------- | ---------------------------- | -------------------------------- | +| Struct | STRUCT (25) | Registered by ID, no evolution | +| Compatible Struct | COMPATIBLE_STRUCT (26) | With schema evolution | +| Named Struct | NAMED_STRUCT (27) | Registered by name, no evolution | +| Named Compatible Struct | NAMED_COMPATIBLE_STRUCT (28) | Named with schema evolution | + +### Struct Requirements + +1. **Exported fields only**: Fields starting with uppercase are serialized +2. **Supported field types**: All types listed in this document +3. **Registration**: Structs should be registered for cross-language use + +```go +type User struct { + ID int64 // Serialized + Name string // Serialized + Age int32 // Serialized + password string // NOT serialized (unexported) +} + +f := fory.New() +f.RegisterStruct(User{}, 1) + +user := &User{ID: 1, Name: "Alice", Age: 30, password: "secret"} +data, _ := f.Serialize(user) +``` + +### Nested Structs + +```go +type Address struct { + Street string + City string + Country string +} + +type Company struct { + Name string + Address Address + Founded int32 +} + +f := fory.New() +f.RegisterStruct(Address{}, 1) +f.RegisterStruct(Company{}, 2) +``` + +## Pointer Types + +| Go Type | Behavior | +| ------- | ---------------------------------------- | +| `*T` | Nil-able, reference tracked (if enabled) | +| `**T` | Nested pointers supported | + +```go +f := fory.New(fory.WithTrackRef(true)) + +type Node struct { + Value int32 + Left *Node + Right *Node +} + +f.RegisterStruct(Node{}, 1) + +root := &Node{ + Value: 1, + Left: &Node{Value: 2}, + Right: &Node{Value: 3}, +} + +data, _ := f.Serialize(root) +``` + +### Nil Handling + +```go +var ptr *User = nil +data, _ := f.Serialize(ptr) + +var result *User +f.Deserialize(data, &result) +// result == nil +``` + +## Interface Types + +| Go Type | Fory TypeId | Notes | +| ------- | ----------- | ------------------ | +| `any` | UNION (31) | Polymorphic values | + +```go +f := fory.New() + +// Serialize any +var value any = "hello" +data, _ := f.Serialize(value) + +var result any +f.Deserialize(data, &result) +// result = "hello" (string) +``` + +For struct interfaces, register all possible concrete types: + +```go +type Shape interface { + Area() float64 +} + +type Circle struct { + Radius float64 +} + +func (c Circle) Area() float64 { + return 3.14159 * c.Radius * c.Radius +} + +f := fory.New() +f.RegisterStruct(Circle{}, 1) + +var shape Shape = Circle{Radius: 5.0} +data, _ := f.Serialize(shape) +``` + +## Binary Data + +| Go Type | Fory TypeId | Notes | +| -------- | ----------- | --------------------- | +| `[]byte` | BINARY (37) | Variable-length bytes | + +```go +f := fory.New() + +data := []byte{0x01, 0x02, 0x03, 0x04} +serialized, _ := f.Serialize(data) + +var result []byte +f.Deserialize(serialized, &result) +``` + +## Enum Types + +Go uses integer types for enums: + +```go +type Status int32 + +const ( + StatusPending Status = 0 + StatusActive Status = 1 + StatusComplete Status = 2 +) + +f := fory.New() +f.RegisterEnum(Status(0), 1) + +status := StatusActive +data, _ := f.Serialize(status) +``` + +## Cross-Language Type Mapping + +| Go Type | Java | Python | C++ | Rust | +| --------------- | -------- | --------- | ------------------ | ------------ | +| `bool` | boolean | bool | bool | bool | +| `int8` | byte | int | int8_t | i8 | +| `int16` | short | int | int16_t | i16 | +| `int32` | int | int | int32_t | i32 | +| `int64` | long | int | int64_t | i64 | +| `float32` | float | float | float | f32 | +| `float64` | double | float | double | f64 | +| `string` | String | str | std::string | String | +| `[]T` | List | list | std::vector | Vec | +| `map[K]V` | Map | dict | std::unordered_map | HashMap | +| `time.Time` | Instant | datetime | - | - | +| `time.Duration` | Duration | timedelta | - | - | + +See [Cross-Language Serialization](cross-language) for detailed mapping. + +## Unsupported Types + +The following Go types are **not supported**: + +- Channels (`chan T`) +- Functions (`func()`) +- Complex numbers (`complex64`, `complex128`) +- Unsafe pointers (`unsafe.Pointer`) + +Attempting to serialize these types will result in an error. + +## Related Topics + +- [Type Registration](type-registration) +- [Cross-Language Serialization](cross-language) +- [References](references) diff --git a/docs/guide/go/thread-safety.md b/docs/guide/go/thread-safety.md new file mode 100644 index 0000000000..1f475b3a1a --- /dev/null +++ b/docs/guide/go/thread-safety.md @@ -0,0 +1,347 @@ +--- +title: Thread Safety +sidebar_position: 100 +id: go_thread_safety +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +This guide covers concurrent usage patterns for Fory Go, including the thread-safe wrapper and best practices for multi-goroutine environments. + +## Default Fory Instance + +The default `Fory` instance is **not thread-safe**: + +```go +f := fory.New() + +// NOT SAFE: Concurrent access from multiple goroutines +go func() { + f.Serialize(value1) // Race condition! +}() +go func() { + f.Serialize(value2) // Race condition! +}() +``` + +### Why Not Thread-Safe? + +For performance, Fory reuses internal state: + +- Buffer is cleared and reused between calls +- Reference resolvers are reset +- Context objects are recycled + +This avoids allocations but requires exclusive access. + +## Thread-Safe Wrapper + +For concurrent use, use the `threadsafe` package: + +```go +import "github.com/apache/fory/go/fory/threadsafe" + +// Create thread-safe Fory +f := threadsafe.New() + +// Safe for concurrent use +go func() { + data, _ := f.Serialize(value1) +}() +go func() { + data, _ := f.Serialize(value2) +}() +``` + +### How It Works + +The thread-safe wrapper uses `sync.Pool`: + +1. **Acquire**: Gets a Fory instance from the pool +2. **Use**: Performs serialization/deserialization +3. **Copy**: Copies result data (buffer will be reused) +4. **Release**: Returns instance to pool + +```go +// Simplified implementation +func (f *Fory) Serialize(v any) ([]byte, error) { + fory := f.pool.Get().(*fory.Fory) + defer f.pool.Put(fory) + + data, err := fory.Serialize(v) + if err != nil { + return nil, err + } + + // Copy because underlying buffer will be reused + result := make([]byte, len(data)) + copy(result, data) + return result, nil +} +``` + +### API + +```go +// Create thread-safe instance +f := threadsafe.New() + +// Instance methods +data, err := f.Serialize(value) +err = f.Deserialize(data, &target) + +// Generic functions +data, err := threadsafe.Serialize(f, &value) +err = threadsafe.Deserialize(f, data, &target) + +// Global convenience functions +data, err := threadsafe.Marshal(&value) +err = threadsafe.Unmarshal(data, &target) +``` + +## Type Registration + +Type registration should be done before concurrent use: + +```go +f := threadsafe.New() + +// Register types BEFORE concurrent access +f.RegisterStruct(User{}, 1) +f.RegisterStruct(Order{}, 2) + +// Now safe to use concurrently +go func() { + f.Serialize(&User{ID: 1}) +}() +``` + +### Thread-Safe Registration + +The thread-safe wrapper handles registration safely: + +```go +// Safe: Registration is synchronized +f := threadsafe.New() +f.RegisterStruct(User{}, 1) // Thread-safe +``` + +However, for best performance, register all types at startup before concurrent use. + +## Zero-Copy Considerations + +### Non-Thread-Safe Instance + +With the default Fory, returned byte slices are views into the internal buffer: + +```go +f := fory.New() + +data1, _ := f.Serialize(value1) +// data1 is valid + +data2, _ := f.Serialize(value2) +// data1 is NOW INVALID (buffer was reused) +``` + +### Thread-Safe Instance + +The thread-safe wrapper copies data automatically: + +```go +f := threadsafe.New() + +data1, _ := f.Serialize(value1) +data2, _ := f.Serialize(value2) +// Both data1 and data2 are valid (independent copies) +``` + +This is safer but has allocation overhead. + +## Performance Comparison + +| Scenario | Non-Thread-Safe | Thread-Safe | +| ------------------- | --------------- | ---------------------- | +| Single goroutine | Fastest | Slower (pool overhead) | +| Multiple goroutines | Unsafe | Safe, good scaling | +| Memory allocations | Minimal | Per-call copy | +| Buffer reuse | Yes | Per-pool-instance | + +### Benchmarking + +```go +func BenchmarkNonThreadSafe(b *testing.B) { + f := fory.New() + f.RegisterStruct(User{}, 1) + user := &User{ID: 1, Name: "Alice"} + + for i := 0; i < b.N; i++ { + data, _ := f.Serialize(user) + _ = data + } +} + +func BenchmarkThreadSafe(b *testing.B) { + f := threadsafe.New() + f.RegisterStruct(User{}, 1) + user := &User{ID: 1, Name: "Alice"} + + for i := 0; i < b.N; i++ { + data, _ := f.Serialize(user) + _ = data + } +} +``` + +## Patterns + +### Per-Goroutine Instance + +For maximum performance with known goroutine count: + +```go +func worker(id int) { + // Each worker has its own Fory instance + f := fory.New() + f.RegisterStruct(User{}, 1) + + for task := range tasks { + data, _ := f.Serialize(task) + process(data) + } +} + +// Start workers +for i := 0; i < numWorkers; i++ { + go worker(i) +} +``` + +### Shared Thread-Safe Instance + +For dynamic goroutine count or simplicity: + +```go +// Single shared instance +var f = threadsafe.New() + +func init() { + f.RegisterStruct(User{}, 1) +} + +func handleRequest(user *User) []byte { + // Safe from any goroutine + data, _ := f.Serialize(user) + return data +} +``` + +### HTTP Handler Example + +```go +var fory = threadsafe.New() + +func init() { + fory.RegisterStruct(Response{}, 1) +} + +func handler(w http.ResponseWriter, r *http.Request) { + response := &Response{ + Status: "ok", + Data: getData(), + } + + // Safe: threadsafe.Fory handles concurrency + data, err := fory.Serialize(response) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(data) +} +``` + +## Common Mistakes + +### Sharing Non-Thread-Safe Instance + +```go +// WRONG: Race condition +var f = fory.New() + +func handler1() { + f.Serialize(value1) // Race! +} + +func handler2() { + f.Serialize(value2) // Race! +} +``` + +**Fix**: Use `threadsafe.New()` or per-goroutine instances. + +### Keeping Reference to Buffer + +```go +// WRONG: Buffer invalidated on next call +f := fory.New() +data, _ := f.Serialize(value1) +savedData := data // Just copies the slice header! + +f.Serialize(value2) // Invalidates data and savedData +``` + +**Fix**: Clone the data or use thread-safe wrapper. + +```go +// Correct: Clone the data +data, _ := f.Serialize(value1) +savedData := make([]byte, len(data)) +copy(savedData, data) + +// Or use thread-safe (auto-copies) +f := threadsafe.New() +data, _ := f.Serialize(value1) // Already copied +``` + +### Registering Types Concurrently + +```go +// RISKY: Concurrent registration +go func() { + f.RegisterStruct(TypeA{}, 1) +}() +go func() { + f.Serialize(value) // May not see TypeA +}() +``` + +**Fix**: Register all types before concurrent use. + +## Best Practices + +1. **Register types at startup**: Before any concurrent operations +2. **Clone data if keeping references**: With non-thread-safe instance +3. **Use per-worker instances for hot paths**: Eliminates pool contention +4. **Profile before optimizing**: Thread-safe overhead may be negligible + +## Related Topics + +- [Configuration](configuration) +- [Basic Serialization](basic-serialization) +- [Troubleshooting](troubleshooting) diff --git a/docs/guide/go/troubleshooting.md b/docs/guide/go/troubleshooting.md new file mode 100644 index 0000000000..f6cd38875c --- /dev/null +++ b/docs/guide/go/troubleshooting.md @@ -0,0 +1,448 @@ +--- +title: Troubleshooting +sidebar_position: 110 +id: go_troubleshooting +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +This guide covers common issues and solutions when using Fory Go. + +## Error Types + +Fory Go uses typed errors with specific error kinds: + +```go +type Error struct { + kind ErrorKind + message string + // Additional context fields +} + +func (e Error) Kind() ErrorKind { return e.kind } +func (e Error) Error() string { return e.message } +``` + +### Error Kinds + +| Kind | Value | Description | +| ------------------------------ | ----- | ------------------------------- | +| `ErrKindOK` | 0 | No error | +| `ErrKindBufferOutOfBound` | 1 | Read/write beyond buffer bounds | +| `ErrKindTypeMismatch` | 2 | Type ID mismatch | +| `ErrKindUnknownType` | 3 | Unknown type encountered | +| `ErrKindSerializationFailed` | 4 | General serialization failure | +| `ErrKindDeserializationFailed` | 5 | General deserialization failure | +| `ErrKindMaxDepthExceeded` | 6 | Recursion depth limit exceeded | +| `ErrKindNilPointer` | 7 | Unexpected nil pointer | +| `ErrKindInvalidRefId` | 8 | Invalid reference ID | +| `ErrKindHashMismatch` | 9 | Struct hash mismatch | +| `ErrKindInvalidTag` | 10 | Invalid fory struct tag | + +## Common Errors and Solutions + +### ErrKindUnknownType + +**Error**: `unknown type encountered` + +**Cause**: Type not registered before serialization/deserialization. + +**Solution**: + +```go +f := fory.New() + +// Register type before use +f.RegisterStruct(User{}, 1) + +// Now serialization works +data, _ := f.Serialize(&User{ID: 1}) +``` + +### ErrKindTypeMismatch + +**Error**: `type mismatch: expected X, got Y` + +**Cause**: Serialized data has different type than expected. + +**Solutions**: + +1. **Use correct target type**: + +```go +// Wrong: Deserializing User into Order +var order Order +f.Deserialize(userData, &order) // Error! + +// Correct +var user User +f.Deserialize(userData, &user) +``` + +2. **Ensure consistent registration**: + +```go +// Serializer +f1 := fory.New() +f1.RegisterStruct(User{}, 1) + +// Deserializer - must use same ID +f2 := fory.New() +f2.RegisterStruct(User{}, 1) // Same ID! +``` + +### ErrKindHashMismatch + +**Error**: `hash X is not consistent with Y for type Z` + +**Cause**: Struct definition changed between serialization and deserialization. + +**Solutions**: + +1. **Enable compatible mode**: + +```go +f := fory.New(fory.WithCompatible(true)) +``` + +2. **Ensure struct definitions match**: + +```go +// Both serializer and deserializer must have same struct +type User struct { + ID int64 + Name string +} +``` + +3. **Regenerate codegen** (if using): + +```bash +go generate ./... +``` + +### ErrKindMaxDepthExceeded + +**Error**: `max depth exceeded` + +**Cause**: Data nesting exceeds maximum depth limit. + +**Possible causes**: + +- Deeply nested data structures exceeding the default limit (20) +- Unintended circular references without reference tracking enabled +- **Malicious data**: Attackers may craft deeply nested payloads to cause resource exhaustion + +**Solutions**: + +1. **Increase max depth** (default is 20): + +```go +f := fory.New(fory.WithMaxDepth(50)) +``` + +2. **Enable reference tracking** (for circular data): + +```go +f := fory.New(fory.WithTrackRef(true)) +``` + +3. **Check for unintended circular references** in your data. + +4. **Validate untrusted data**: When deserializing data from untrusted sources, do not blindly increase max depth. Consider validating input size and structure before deserialization. + +### ErrKindBufferOutOfBound + +**Error**: `buffer out of bound: offset=X, need=Y, size=Z` + +**Cause**: Reading beyond available data. + +**Solutions**: + +1. **Ensure complete data transfer**: + +```go +// Wrong: Truncated data +data := fullData[:100] +f.Deserialize(data, &target) // Error if data was larger + +// Correct: Use full data +f.Deserialize(fullData, &target) +``` + +2. **Check for data corruption**: Verify data integrity during transmission. + +### ErrKindInvalidRefId + +**Error**: `invalid reference ID` + +**Cause**: Reference to non-existent object in serialized data. + +**Solutions**: + +1. **Ensure reference tracking consistency**: + +```go +// Serializer and deserializer must have same setting +f1 := fory.New(fory.WithTrackRef(true)) +f2 := fory.New(fory.WithTrackRef(true)) // Must match! +``` + +2. **Check for data corruption**. + +### ErrKindInvalidTag + +**Error**: `invalid fory struct tag` + +**Cause**: Invalid struct tag configuration. + +**Common causes**: + +1. **Invalid tag ID**: ID must be >= -1 + +```go +// Wrong: negative ID (other than -1) +type Bad struct { + Field int `fory:"id=-5"` +} + +// Correct +type Good struct { + Field int `fory:"id=0"` +} +``` + +2. **Duplicate tag IDs**: Each field must have a unique ID within the struct + +```go +// Wrong: duplicate IDs +type Bad struct { + Field1 int `fory:"id=0"` + Field2 int `fory:"id=0"` // Duplicate! +} + +// Correct +type Good struct { + Field1 int `fory:"id=0"` + Field2 int `fory:"id=1"` +} +``` + +## Cross-Language Issues + +### Field Order Mismatch + +**Symptom**: Data deserializes but fields have wrong values. + +**Cause**: Different field ordering between languages. In non-compatible mode, fields are sorted by their snake_case names. CamelCase field names (e.g., `FirstName`) are converted to snake_case (e.g., `first_name`) for sorting. + +**Solutions**: + +1. **Ensure converted snake_case names are consistent**: Field names across languages must produce the same snake_case ordering: + +```go +type User struct { + FirstName string // Go: FirstName -> first_name + LastName string // Go: LastName -> last_name + // Sorted alphabetically by snake_case: first_name, last_name +} +``` + +2. **Use field IDs for consistent ordering**: Field IDs (non-negative integers) act as aliases for field names, used for both sorting and field matching during deserialization: + +```go +type User struct { + FirstName string `fory:"id=0"` + LastName string `fory:"id=1"` +} +``` + +Ensure the same field IDs are used across all languages for corresponding fields. + +### Name Registration Mismatch + +**Symptom**: `unknown type` in other languages. + +**Solution**: Use identical names: + +```go +// Go +f.RegisterNamedStruct(User{}, "example.User") + +// Java - must match exactly +fory.register(User.class, "example.User"); + +// Python +fory.register(User, typename="example.User") +``` + +## Performance Issues + +### Slow Serialization + +**Possible causes**: + +1. **Large object graphs**: Reduce data size or serialize incrementally. + +2. **Excessive reference tracking**: Disable if not needed: + +```go +f := fory.New(fory.WithTrackRef(false)) +``` + +3. **Deep nesting**: Flatten data structures where possible. + +### High Memory Usage + +**Possible causes**: + +1. **Large serialized data**: Process in chunks. + +2. **Reference tracking overhead**: Disable if not needed. + +3. **Buffer not released**: Reuse buffers: + +```go +buf := fory.NewByteBuffer(nil) +f.SerializeTo(buf, value) +// Process data +buf.Reset() // Reuse for next serialization +``` + +### Thread Contention + +**Symptom**: Slowdown under concurrent load. + +**Solutions**: + +1. **Use per-goroutine instances** for hot paths: + +```go +func worker() { + f := fory.New() // Each worker has own instance + for task := range tasks { + f.Serialize(task) + } +} +``` + +2. **Profile pool usage** with thread-safe wrapper. + +## Debugging Techniques + +### Enable Debug Output + +Set environment variable: + +```bash +ENABLE_FORY_DEBUG_OUTPUT=1 go test ./... +``` + +### Inspect Serialized Data + +```go +data, _ := f.Serialize(value) +fmt.Printf("Serialized %d bytes\n", len(data)) +fmt.Printf("Header: %x\n", data[:4]) // Magic + flags +``` + +### Check Type Registration + +```go +// Verify type is registered +f := fory.New() +err := f.RegisterStruct(User{}, 1) +if err != nil { + fmt.Printf("Registration failed: %v\n", err) +} +``` + +### Compare Struct Hashes + +If getting hash mismatch, compare struct definitions: + +```go +// Print struct info for debugging +t := reflect.TypeOf(User{}) +for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + fmt.Printf("Field: %s, Type: %s\n", f.Name, f.Type) +} +``` + +## Testing Tips + +### Test Round-Trip + +```go +func TestRoundTrip(t *testing.T) { + f := fory.New() + f.RegisterStruct(User{}, 1) + + original := &User{ID: 1, Name: "Alice"} + + data, err := f.Serialize(original) + require.NoError(t, err) + + var result User + err = f.Deserialize(data, &result) + require.NoError(t, err) + + assert.Equal(t, original.ID, result.ID) + assert.Equal(t, original.Name, result.Name) +} +``` + +### Test Cross-Language + +```bash +cd java/fory-core +FORY_GO_JAVA_CI=1 mvn test -Dtest=org.apache.fory.xlang.GoXlangTest +``` + +### Test Schema Evolution + +```go +func TestSchemaEvolution(t *testing.T) { + f1 := fory.New(fory.WithCompatible(true)) + f1.RegisterStruct(UserV1{}, 1) + + data, _ := f1.Serialize(&UserV1{ID: 1, Name: "Alice"}) + + f2 := fory.New(fory.WithCompatible(true)) + f2.RegisterStruct(UserV2{}, 1) + + var result UserV2 + err := f2.Deserialize(data, &result) + require.NoError(t, err) +} +``` + +## Getting Help + +If you encounter issues not covered here: + +1. **Check GitHub Issues**: [github.com/apache/fory/issues](https://github.com/apache/fory/issues) +2. **Enable debug output**: `ENABLE_FORY_DEBUG_OUTPUT=1` +3. **Create minimal reproduction**: Isolate the problem +4. **Report the issue**: Include Go version, Fory version, and minimal code + +## Related Topics + +- [Configuration](configuration) +- [Cross-Language Serialization](cross-language) +- [Schema Evolution](schema-evolution) +- [Thread Safety](thread-safety) diff --git a/docs/guide/go/type-registration.md b/docs/guide/go/type-registration.md new file mode 100644 index 0000000000..87090d604a --- /dev/null +++ b/docs/guide/go/type-registration.md @@ -0,0 +1,262 @@ +--- +title: Type Registration +sidebar_position: 30 +id: go_type_registration +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +Type registration tells Fory how to identify and serialize your custom types. Registration is required for struct, enum, and extension types. + +## Why Register Types? + +1. **Type Identification**: Fory needs to identify the actual type during deserialization +2. **Polymorphism**: When deserializing interface types, Fory must know which concrete type to create +3. **Cross-Language Compatibility**: Other languages need to recognize and deserialize your types + +## Struct Registration + +### Register by ID + +Register a struct with a numeric type ID for compact serialization: + +```go +type User struct { + ID int64 + Name string +} + +f := fory.New() +err := f.RegisterStruct(User{}, 1) +if err != nil { + panic(err) +} +``` + +**ID Guidelines**: + +- IDs must be unique within your application +- IDs must be consistent across all languages for cross-language serialization +- Use the same ID for the same type in serializer and deserializer + +### Register by Name + +Register a struct with a type name string. This is more flexible but has higher serialization cost: + +```go +f := fory.New() +err := f.RegisterNamedStruct(User{}, "example.User") +if err != nil { + panic(err) +} +``` + +**Name Guidelines**: + +- Use fully-qualified names following `namespace.TypeName` convention +- Names must be unique and consistent across all languages +- Names are case-sensitive + +## Enum Registration + +Go doesn't have native enums, but you can register integer types as enums: + +### Register by ID + +```go +type Status int32 + +const ( + StatusPending Status = 0 + StatusActive Status = 1 + StatusComplete Status = 2 +) + +f := fory.New() +err := f.RegisterEnum(Status(0), 1) +``` + +### Register by Name + +```go +err := f.RegisterNamedEnum(Status(0), "example.Status") +``` + +## Extension Types + +For types requiring custom serialization logic, register as extension types with a custom serializer: + +```go +f := fory.New() + +// Register by ID +err := f.RegisterExtension(CustomType{}, 1, &CustomSerializer{}) + +// Or register by name +err = f.RegisterNamedExtension(CustomType{}, "example.Custom", &CustomSerializer{}) +``` + +See [Custom Serializers](custom-serializers) for details on implementing the `ExtensionSerializer` interface. + +## Registration Scope + +Type registration is per-Fory-instance: + +```go +f1 := fory.New() +f2 := fory.New() + +// Types registered on f1 are NOT available on f2 +f1.RegisterStruct(User{}, 1) + +// f2 cannot deserialize User unless also registered +f2.RegisterStruct(User{}, 1) +``` + +## Registration Timing + +Register types after creating a Fory instance and before any serialize/deserialize calls: + +```go +f := fory.New() + +// Register before use +f.RegisterStruct(User{}, 1) +f.RegisterStruct(Order{}, 2) + +// Now serialize/deserialize +data, _ := f.Serialize(&User{ID: 1, Name: "Alice"}) +``` + +## Nested Type Registration + +Register all struct types in the object graph, including nested types: + +```go +type Address struct { + City string + Country string +} + +type Person struct { + Name string + Address Address +} + +f := fory.New() + +// Register ALL struct types used in the object graph +f.RegisterStruct(Address{}, 1) +f.RegisterStruct(Person{}, 2) +``` + +## Cross-Language Registration + +For cross-language serialization, types must be registered consistently across all languages. + +### Using IDs + +All languages use the same numeric ID: + +**Go**: + +```go +f.RegisterStruct(User{}, 1) +``` + +**Java**: + +```java +fory.register(User.class, 1); +``` + +**Python**: + +```python +fory.register(User, type_id=1) +``` + +### Using Names + +All languages use the same type name: + +**Go**: + +```go +f.RegisterNamedStruct(User{}, "example.User") +``` + +**Java**: + +```java +fory.register(User.class, "example.User"); +``` + +**Python**: + +```python +fory.register(User, typename="example.User") +``` + +**Rust**: + +```rust +#[derive(Fory)] +struct User { + id: i64, + name: String, +} + +let mut fory = Fory::default(); +fory.register_by_name::("example.User")?; +``` + +## Best Practices + +1. **Register early**: Register all types at application startup before any serialization +2. **Be consistent**: Use the same ID or name across all languages and all instances +3. **Register all types**: Include nested struct types, not just top-level types +4. **Prefer IDs for performance**: Numeric IDs have lower serialization overhead than names +5. **Use names for flexibility**: Names are easier to manage and less prone to conflicts + +## Common Errors + +### Unregistered Type + +``` +error: unknown type encountered +``` + +**Solution**: Register the type before serialization/deserialization. + +### ID/Name Mismatch + +Data serialized with one ID or name cannot be deserialized if registered with a different ID or name. + +**Solution**: Use consistent IDs or names across serializer and deserializer. + +### Duplicate Registration + +Two types registered with the same ID will conflict. + +**Solution**: Ensure unique IDs for each type. + +## Related Topics + +- [Basic Serialization](basic-serialization) +- [Cross-Language Serialization](cross-language) +- [Supported Types](supported-types) +- [Troubleshooting](troubleshooting) diff --git a/docs/guide/xlang/field-reference-tracking.md b/docs/guide/xlang/field-reference-tracking.md index e003c59f55..4e67ac5b28 100644 --- a/docs/guide/xlang/field-reference-tracking.md +++ b/docs/guide/xlang/field-reference-tracking.md @@ -113,13 +113,13 @@ By default, **most fields do not track references** even when global `refTrackin ### Default Behavior by Language -| Language | Default Ref Tracking | Types That Track Refs by Default | -| -------- | -------------------- | -------------------------------- | -| Java | No | None (use annotation to enable) | -| Python | No | None (use annotation to enable) | -| Go | No | Pointer types (`*T`) | -| C++ | No | `std::shared_ptr` | -| Rust | No | `Rc`, `Arc`, `Weak` | +| Language | Default Ref Tracking | Types That Track Refs by Default | +| -------- | -------------------- | --------------------------------- | +| Java | No | None (use annotation to enable) | +| Python | No | None (use annotation to enable) | +| Go | No | None (use `fory:"ref"` to enable) | +| C++ | No | `std::shared_ptr` | +| Rust | No | `Rc`, `Arc`, `Weak` | ### Customizing Per-Field Ref Tracking @@ -181,11 +181,11 @@ struct Document { type Document struct { Title string - // Pointer types track refs by default - Author *Author + // Enable ref tracking for pointer to struct + Author *Author `fory:"ref"` - // Use struct tag to control ref tracking - Tags []Tag `fory:"trackRef"` + // Enable ref tracking for slice + Tags []Tag `fory:"ref"` } ``` diff --git a/go/README.md b/go/README.md index 5231dda750..0f5985e087 100644 --- a/go/README.md +++ b/go/README.md @@ -2,220 +2,142 @@ Fory is a blazingly fast multi-language serialization framework powered by just-in-time compilation and zero-copy. -Fory Go provides two serialization paths: a high-performance code generation path and a reflection-based path. The code generation path is recommended for production use as it offers better performance and broader type support. - -## Supported Types - -Fory Go supports the following types for both reflection-based serialization and code generation: - -### Basic Data Types - -- `bool` -- `int8`, `int16`, `int32`, `int64`, `int` -- `uint8` (byte) -- `float32`, `float64` -- `string` - -### Collection Types - -- `[]bool`, `[]int16`, `[]int32`, `[]int64` -- `[]float32`, `[]float64` -- `[]string` -- `[]interface{}` (dynamic slice) -- `map[string]string`, `map[int]int`, `map[string]int` - -## Fory Go Codegen (optional) - -This repository includes an optional ahead-of-time (AOT) code generator for Fory. The runtime reflection-based path continues to work; codegen exists to provide additional performance, type safety and zero-reflection overhead for hot paths. You can adopt it incrementally, per package or per file. - -### Why codegen (rationale) - -- Faster (no reflection on the hot path) -- Type-safe serialization/deserialization with predictable layouts -- Smaller GC pressure and fewer allocations -- Compile-time guards to detect stale generated code when struct definitions change - -Note: Code generation is not mandatory. If you prefer simple workflows, you can keep using the reflection-based API. - -### Install the generator - -The generator binary is `fory`. - -- Go 1.16+ (recommended): +## Installation ```bash -go install github.com/apache/fory/go/fory/cmd/fory@latest +go get github.com/apache/fory/go/fory ``` -- Go 1.13+: - -```bash -# Inside a module-enabled environment -GO111MODULE=on go get -u github.com/apache/fory/go/fory/cmd/fory - -# Or clone the repo and install from source -git clone https://github.com/apache/fory.git -cd fory/go/fory -go install ./cmd/fory -``` - -Ensure $GOBIN or $GOPATH/bin is on your PATH so that `fory` is discoverable by `go generate`. - -### Usage: annotate and generate - -1. Mark structs for generation with `//fory:generate`, and add a `go:generate` directive. File-based generation is recommended. +## Quick Start ```go -package yourpkg - -//fory:generate -type User struct { - ID int64 `json:"id"` - Name string `json:"name"` -} - -//go:generate fory -file structs.go -``` - -Then run: - -```bash -go generate -``` - -The generator will create `structs_fory_gen.go` next to your source file and register serializers in init(). - -2. Explicit types (legacy mode) are also supported: +package main -```bash -fory -pkg ./models -type "User,Order" -``` - -### When to re-run `go generate` - -Re-run generation whenever any of the following change for generated structs: - -- Field additions/removals/renames -- Field type changes or tag changes -- New structs annotated with `//fory:generate` - -Fory adds a compile-time guard in generated files to detect stale code. If you forget to re-generate, your build will fail with a clear message. The generator also includes a smart auto-retry: when invoked via `go generate`, it detects this situation, removes the stale generated file, and retries automatically. You can force this behavior manually with: - -```bash -fory --force -file structs.go -``` - -### What gets generated (simplified example) - -Below is a minimal illustration. Actual output includes strongly-typed serializers, interface-compatible methods, registration, and a compile-time snapshot of your struct. - -```go -// Code generated by fory. DO NOT EDIT. -package yourpkg +import ( + "fmt" + "github.com/apache/fory/go/fory" +) -// Snapshot of User's underlying type at generation time. -type _User_expected struct { +type User struct { ID int64 Name string + Age int32 } -// Compile-time check: fails if User no longer matches the snapshot. -// If this fails, run: go generate -var _ = func(x User) { _ = _User_expected(x) } - -type User_ForyGenSerializer struct{} - -func (User_ForyGenSerializer) WriteTyped(f *fory.Fory, buf *fory.ByteBuffer, v *User) error { - // write fields in a stable order - buf.WriteInt64(v.ID) - fory.WriteString(buf, v.Name) - return nil -} - -func (User_ForyGenSerializer) ReadTyped(f *fory.Fory, buf *fory.ByteBuffer, v *User) error { - v.ID = buf.ReadInt64() - v.Name = fory.ReadString(buf) - return nil +func main() { + // Create a Fory instance + f := fory.New() + + // Register struct with a type ID + if err := f.RegisterStruct(User{}, 1); err != nil { + panic(err) + } + + // Serialize + user := &User{ID: 1, Name: "Alice", Age: 30} + data, err := f.Serialize(user) + if err != nil { + panic(err) + } + fmt.Printf("Serialized %d bytes\n", len(data)) + + // Deserialize + var result User + if err := f.Deserialize(data, &result); err != nil { + panic(err) + } + fmt.Printf("Deserialized: %+v\n", result) } ``` -### CI and version control (should I check in generated code?) - -Both models are supported; choose based on your workflow: +## Supported Types -- Check in generated code (recommended for libraries) - - Pros: Consumers can build without the generator; reproducible builds - - Cons: Larger diffs; must remember to re-generate before commit +### Basic Data Types -- Do not check in; generate in CI/release pipeline (recommended for apps) - - Add a step to your pipeline, e.g.: - - `go generate ./...` - - Optionally `fory --force -file ` for targeted regeneration +- `bool` +- `int8`, `int16`, `int32`, `int64`, `int` +- `uint8` (byte), `uint16`, `uint32`, `uint64` +- `float32`, `float64` +- `string` -Regardless of your choice, the compile-time guard ensures that stale code is noticed early. If a build fails due to the guard in a local environment, run: +### Collection Types -```bash -go generate -# If needed -fory --force -file -``` +- `[]bool`, `[]int16`, `[]int32`, `[]int64` +- `[]float32`, `[]float64` +- `[]string` +- `[]interface{}` (dynamic slice) +- `map[string]string`, `map[int]int`, `map[string]int`, and more -### FAQ +### Time Types -- Is codegen required? No. Fory works without it via reflection. -- Does generated code work across Go versions? Yes, it’s plain Go code; keep your toolchain consistent in CI. -- Can I mix generated and non-generated structs? Yes, adoption is incremental and per file. +- `time.Time` +- `time.Duration` ## Configuration Options -Fory Go supports several configuration options through the functional options pattern: +Fory Go supports configuration through functional options: -### Compatible Mode (Meta share mode) +```go +// Enable reference tracking for circular references +f := fory.New(fory.WithTrackRef(true)) -Compatible mode enables meta information sharing, which allows for schema evolution: +// Enable compatible mode for schema evolution +f := fory.New(fory.WithCompatible(true)) -```go -// Enable compatible mode with meta share -fory := NewForyWithOptions(WithCompatible(true)) +// Set maximum nesting depth +f := fory.New(fory.WithMaxDepth(20)) + +// Combine multiple options +f := fory.New( + fory.WithTrackRef(true), + fory.WithCompatible(true), +) ``` -### Reference Tracking +## Cross-Language Serialization -Enable reference tracking: +Fory Go enables seamless data exchange with Java, Python, C++, Rust, and JavaScript: ```go -fory := NewForyWithOptions(WithRefTracking(true)) +// Go +f := fory.New() +f.RegisterNamedStruct(User{}, "example.User") +data, _ := f.Serialize(&User{ID: 1, Name: "Alice"}) +// 'data' can be deserialized by Java, Python, etc. ``` -### Combined Options +## Thread Safety -You can combine multiple options: +The default Fory instance is not thread-safe. For concurrent use: ```go -fory := NewForyWithOptions( - WithCompatible(true), - WithRefTracking(true), -) -``` +import "github.com/apache/fory/go/fory/threadsafe" -## Best Practices +f := threadsafe.New() -### Type Registration Patterns +// Safe for concurrent use +go func() { f.Serialize(value1) }() +go func() { f.Serialize(value2) }() +``` -Choose the right registration approach for your use case: +## Documentation -```go -// Register using an explicit namespace and type name pair. -func (f *Fory) RegisterByNamespace(User{}, "example", "user") error +For comprehensive documentation, see the [Fory Go Guide](https://fory.apache.org/docs/guide/go/). -// Register using a name -func (f *Fory) RegisterNamedType(User{}, "example.user") error +Topics covered: -// Register using a pre-assigned numeric type identifier. -func (f *Fory) Register(User{}, 101) error -``` +- [Configuration](https://fory.apache.org/docs/guide/go/configuration) - Options and settings +- [Basic Serialization](https://fory.apache.org/docs/guide/go/basic-serialization) - Core APIs and usage patterns +- [Type Registration](https://fory.apache.org/docs/guide/go/type-registration) - Registering types for serialization +- [Supported Types](https://fory.apache.org/docs/guide/go/supported-types) - Complete type support reference +- [References](https://fory.apache.org/docs/guide/go/references) - Circular references and shared objects +- [Schema Evolution](https://fory.apache.org/docs/guide/go/schema-evolution) - Forward/backward compatibility +- [Cross-Language](https://fory.apache.org/docs/guide/go/cross-language) - Multi-language serialization +- [Code Generation](https://fory.apache.org/docs/guide/go/codegen) - Experimental AOT code generation +- [Thread Safety](https://fory.apache.org/docs/guide/go/thread-safety) - Concurrent usage patterns +- [Troubleshooting](https://fory.apache.org/docs/guide/go/troubleshooting) - Common issues and solutions -## How to test +## How to Test ```bash cd go/fory @@ -230,6 +152,6 @@ cd go/fory gofmt -s -w . ``` -When using Go's gofmt -s -w . command on Windows, ensure your source files use Unix-style line endings (LF) instead of Windows-style (CRLF). Go tools expect LF by default, and mismatched line endings may cause unexpected behavior or unnecessary changes in version control. +When using Go's `gofmt -s -w .` command on Windows, ensure your source files use Unix-style line endings (LF) instead of Windows-style (CRLF). Go tools expect LF by default, and mismatched line endings may cause unexpected behavior or unnecessary changes in version control. Before committing, you can use `git config core.autocrlf input` to take effect on future commits. diff --git a/go/fory/array.go b/go/fory/array.go index ea222cd010..d7a5c7f5ce 100644 --- a/go/fory/array.go +++ b/go/fory/array.go @@ -89,7 +89,7 @@ func (s arraySerializer) Write(ctx *WriteContext, refMode RefMode, writeType boo s.WriteData(ctx, value) } -func (s arraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s arraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() length := int(buf.ReadVaruint32(err)) @@ -103,7 +103,7 @@ func (s arraySerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s arraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -223,7 +223,7 @@ func (s *arrayConcreteValueSerializer) Write(ctx *WriteContext, refMode RefMode, s.WriteData(ctx, value) } -func (s *arrayConcreteValueSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s *arrayConcreteValueSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() length := int(buf.ReadVaruint32(err)) @@ -258,7 +258,7 @@ func (s *arrayConcreteValueSerializer) ReadData(ctx *ReadContext, type_ reflect. if trackRefs { s.elemSerializer.Read(ctx, RefModeTracking, false, false, elem) } else { - s.elemSerializer.ReadData(ctx, elem.Type(), elem) + s.elemSerializer.ReadData(ctx, elem) } if ctx.HasError() { return @@ -271,7 +271,7 @@ func (s *arrayConcreteValueSerializer) Read(ctx *ReadContext, refMode RefMode, r if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) if ctx.HasError() { return } @@ -312,11 +312,11 @@ func (s arrayDynSerializer) Write(ctx *WriteContext, refMode RefMode, writeType s.WriteData(ctx, value) } -func (s arrayDynSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s arrayDynSerializer) ReadData(ctx *ReadContext, value reflect.Value) { // Create a temp slice to read into, then copy back to array sliceType := reflect.SliceOf(value.Type().Elem()) tempSlice := reflect.MakeSlice(sliceType, value.Len(), value.Len()) - s.sliceSerializer.ReadData(ctx, sliceType, tempSlice) + s.sliceSerializer.ReadData(ctx, tempSlice) if ctx.HasError() { return } @@ -335,7 +335,7 @@ func (s arrayDynSerializer) Read(ctx *ReadContext, refMode RefMode, readType boo if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s arrayDynSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -367,7 +367,7 @@ func (s byteArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeType s.WriteData(ctx, value) } -func (s byteArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s byteArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() length := buf.ReadLength(err) @@ -388,7 +388,7 @@ func (s byteArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType bo if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s byteArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/array_primitive.go b/go/fory/array_primitive.go index 1e0b261e35..a6a3e469d0 100644 --- a/go/fory/array_primitive.go +++ b/go/fory/array_primitive.go @@ -60,15 +60,15 @@ func (s boolArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeType s.WriteData(ctx, value) } -func (s boolArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s boolArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() length := buf.ReadLength(err) if ctx.HasError() { return } - if length != type_.Len() { - ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, type_)) + if length != value.Type().Len() { + ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, value.Type())) return } if length > 0 { @@ -84,7 +84,7 @@ func (s boolArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType bo if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s boolArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -125,15 +125,15 @@ func (s int8ArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeType s.WriteData(ctx, value) } -func (s int8ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int8ArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() length := buf.ReadLength(err) if ctx.HasError() { return } - if length != type_.Len() { - ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, type_)) + if length != value.Type().Len() { + ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, value.Type())) return } if length > 0 { @@ -149,7 +149,7 @@ func (s int8ArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType bo if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int8ArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -191,7 +191,7 @@ func (s int16ArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeTyp s.WriteData(ctx, value) } -func (s int16ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int16ArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() size := buf.ReadLength(err) @@ -199,8 +199,8 @@ func (s int16ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, val if ctx.HasError() { return } - if length != type_.Len() { - ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, type_)) + if length != value.Type().Len() { + ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, value.Type())) return } if length > 0 { @@ -221,7 +221,7 @@ func (s int16ArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType b if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int16ArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -263,7 +263,7 @@ func (s int32ArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeTyp s.WriteData(ctx, value) } -func (s int32ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int32ArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() size := buf.ReadLength(err) @@ -271,8 +271,8 @@ func (s int32ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, val if ctx.HasError() { return } - if length != type_.Len() { - ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, type_)) + if length != value.Type().Len() { + ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, value.Type())) return } if length > 0 { @@ -293,7 +293,7 @@ func (s int32ArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType b if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int32ArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -335,7 +335,7 @@ func (s int64ArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeTyp s.WriteData(ctx, value) } -func (s int64ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int64ArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() size := buf.ReadLength(err) @@ -343,8 +343,8 @@ func (s int64ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, val if ctx.HasError() { return } - if length != type_.Len() { - ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, type_)) + if length != value.Type().Len() { + ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, value.Type())) return } if length > 0 { @@ -365,7 +365,7 @@ func (s int64ArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType b if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int64ArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -407,7 +407,7 @@ func (s float32ArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeT s.WriteData(ctx, value) } -func (s float32ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s float32ArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() size := buf.ReadLength(err) @@ -415,8 +415,8 @@ func (s float32ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, v if ctx.HasError() { return } - if length != type_.Len() { - ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, type_)) + if length != value.Type().Len() { + ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, value.Type())) return } if length > 0 { @@ -437,7 +437,7 @@ func (s float32ArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s float32ArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -479,7 +479,7 @@ func (s float64ArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeT s.WriteData(ctx, value) } -func (s float64ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s float64ArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() size := buf.ReadLength(err) @@ -487,8 +487,8 @@ func (s float64ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, v if ctx.HasError() { return } - if length != type_.Len() { - ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, type_)) + if length != value.Type().Len() { + ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, value.Type())) return } if length > 0 { @@ -509,7 +509,7 @@ func (s float64ArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s float64ArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -550,15 +550,15 @@ func (s uint8ArraySerializer) Write(ctx *WriteContext, refMode RefMode, writeTyp s.WriteData(ctx, value) } -func (s uint8ArraySerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s uint8ArraySerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() length := buf.ReadLength(err) if ctx.HasError() { return } - if length != type_.Len() { - ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, type_)) + if length != value.Type().Len() { + ctx.SetError(DeserializationErrorf("array length %d does not match type %v", length, value.Type())) return } if length > 0 { @@ -574,7 +574,7 @@ func (s uint8ArraySerializer) Read(ctx *ReadContext, refMode RefMode, readType b if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s uint8ArraySerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/codegen/decoder.go b/go/fory/codegen/decoder.go index a6adbfd9b2..77853c7d51 100644 --- a/go/fory/codegen/decoder.go +++ b/go/fory/codegen/decoder.go @@ -63,14 +63,14 @@ func generateReadTyped(buf *bytes.Buffer, s *StructInfo) error { func generateReadInterface(buf *bytes.Buffer, s *StructInfo) error { // Generate ReadData method (reflect.Value-based API) fmt.Fprintf(buf, "// ReadData provides reflect.Value interface compatibility (implements fory.Serializer)\n") - fmt.Fprintf(buf, "func (g *%s_ForyGenSerializer) ReadData(ctx *fory.ReadContext, type_ reflect.Type, value reflect.Value) {\n", s.Name) + fmt.Fprintf(buf, "func (g *%s_ForyGenSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) {\n", s.Name) fmt.Fprintf(buf, "\tg.initHash(ctx.TypeResolver())\n") fmt.Fprintf(buf, "\t// Convert reflect.Value to concrete type and delegate to typed method\n") fmt.Fprintf(buf, "\tvar v *%s\n", s.Name) fmt.Fprintf(buf, "\tif value.Kind() == reflect.Ptr {\n") fmt.Fprintf(buf, "\t\tif value.IsNil() {\n") - fmt.Fprintf(buf, "\t\t\t// For pointer types, allocate using type_.Elem()\n") - fmt.Fprintf(buf, "\t\t\tvalue.Set(reflect.New(type_.Elem()))\n") + fmt.Fprintf(buf, "\t\t\t// For pointer types, allocate using value.Type().Elem()\n") + fmt.Fprintf(buf, "\t\t\tvalue.Set(reflect.New(value.Type().Elem()))\n") fmt.Fprintf(buf, "\t\t}\n") fmt.Fprintf(buf, "\t\tv = value.Interface().(*%s)\n", s.Name) fmt.Fprintf(buf, "\t} else {\n") diff --git a/go/fory/codegen/generator.go b/go/fory/codegen/generator.go index 53590747aa..65d0cf22fd 100644 --- a/go/fory/codegen/generator.go +++ b/go/fory/codegen/generator.go @@ -509,7 +509,7 @@ func generateReadMethod(buf *bytes.Buffer, s *StructInfo) error { fmt.Fprintf(buf, "\tif readType {\n") fmt.Fprintf(buf, "\t\tctx.TypeResolver().ReadTypeInfo(ctx.Buffer(), err)\n") fmt.Fprintf(buf, "\t}\n") - fmt.Fprintf(buf, "\tg.ReadData(ctx, value.Type(), value)\n") + fmt.Fprintf(buf, "\tg.ReadData(ctx, value)\n") fmt.Fprintf(buf, "}\n\n") return nil } diff --git a/go/fory/enum.go b/go/fory/enum.go index acda646a1e..e61ace5e90 100644 --- a/go/fory/enum.go +++ b/go/fory/enum.go @@ -59,7 +59,7 @@ func (s *enumSerializer) Write(ctx *WriteContext, refMode RefMode, writeType boo s.WriteData(ctx, value) } -func (s *enumSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s *enumSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() ordinal := ctx.buffer.ReadVaruint32Small7(err) if ctx.HasError() { @@ -90,7 +90,7 @@ func (s *enumSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s *enumSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/extension.go b/go/fory/extension.go new file mode 100644 index 0000000000..b155138206 --- /dev/null +++ b/go/fory/extension.go @@ -0,0 +1,105 @@ +// 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 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package fory + +import ( + "reflect" +) + +// extensionSerializerAdapter wraps an ExtensionSerializer to implement the full Serializer interface. +// This adapter handles reference tracking, type info writing/reading, and delegates the actual +// data serialization to the user-provided ExtensionSerializer. +type extensionSerializerAdapter struct { + type_ reflect.Type + typeTag string + userSerial ExtensionSerializer +} + +func (s *extensionSerializerAdapter) GetType() reflect.Type { return s.type_ } + +func (s *extensionSerializerAdapter) WriteData(ctx *WriteContext, value reflect.Value) { + // Delegate to user's serializer + s.userSerial.WriteData(ctx, value) +} + +func (s *extensionSerializerAdapter) Write(ctx *WriteContext, refMode RefMode, writeType bool, hasGenerics bool, value reflect.Value) { + _ = hasGenerics // not used for extension serializers + buf := ctx.Buffer() + switch refMode { + case RefModeTracking: + refWritten, err := ctx.RefResolver().WriteRefOrNull(buf, value) + if err != nil { + ctx.SetError(FromError(err)) + return + } + if refWritten { + return + } + case RefModeNullOnly: + if isNil(value) { + buf.WriteInt8(NullFlag) + return + } + buf.WriteInt8(NotNullValueFlag) + } + if writeType { + typeInfo, err := ctx.TypeResolver().getTypeInfo(value, true) + if err != nil { + ctx.SetError(FromError(err)) + return + } + ctx.TypeResolver().WriteTypeInfo(buf, typeInfo, ctx.Err()) + } + s.WriteData(ctx, value) +} + +func (s *extensionSerializerAdapter) ReadData(ctx *ReadContext, value reflect.Value) { + // Delegate to user's serializer + s.userSerial.ReadData(ctx, value) +} + +func (s *extensionSerializerAdapter) Read(ctx *ReadContext, refMode RefMode, readType bool, hasGenerics bool, value reflect.Value) { + _ = hasGenerics // not used for extension serializers + buf := ctx.Buffer() + ctxErr := ctx.Err() + switch refMode { + case RefModeTracking: + refID, refErr := ctx.RefResolver().TryPreserveRefId(buf) + if refErr != nil { + ctx.SetError(FromError(refErr)) + return + } + if refID < int32(NotNullValueFlag) { + obj := ctx.RefResolver().GetReadObject(refID) + if obj.IsValid() { + value.Set(obj) + } + return + } + case RefModeNullOnly: + flag := buf.ReadInt8(ctxErr) + if flag == NullFlag { + return + } + } + s.ReadData(ctx, value) +} + +func (s *extensionSerializerAdapter) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { + s.Read(ctx, refMode, false, false, value) +} diff --git a/go/fory/field_info.go b/go/fory/field_info.go index 7739894eca..5dd9ab356c 100644 --- a/go/fory/field_info.go +++ b/go/fory/field_info.go @@ -439,6 +439,17 @@ func isStructField(t reflect.Type) bool { return false } +// isSetReflectType checks if a reflect.Type is a Set type (map[T]struct{}) +// fory.Set[T] is defined as map[T]struct{} where the value type is empty struct +func isSetReflectType(t reflect.Type) bool { + if t.Kind() != reflect.Map { + return false + } + // Check if value type is empty struct (struct{}) + elemType := t.Elem() + return elemType.Kind() == reflect.Struct && elemType.NumField() == 0 +} + // isStructFieldType checks if a FieldType represents a type that needs type info written // This is used to determine if type info was written for the field in compatible mode // In compatible mode, Java writes type info for struct and ext types, but NOT for enum types @@ -823,8 +834,8 @@ func typeIdFromKind(type_ reflect.Type) TypeId { return LIST } case reflect.Map: - // map[T]bool is used to represent a Set in Go - if type_.Elem().Kind() == reflect.Bool { + // fory.Set[T] is defined as map[T]struct{} - check for struct{} elem type + if isSetReflectType(type_) { return SET } return MAP diff --git a/go/fory/fory.go b/go/fory/fory.go index 2fab2a91fd..26e66017e1 100644 --- a/go/fory/fory.go +++ b/go/fory/fory.go @@ -79,8 +79,8 @@ type Config struct { func defaultConfig() Config { return Config{ TrackRef: false, // Match Java's default: reference tracking disabled - MaxDepth: 100, - IsXlang: true, + MaxDepth: 20, + IsXlang: false, } } @@ -200,12 +200,12 @@ func NewFory(opts ...Option) *Fory { return New(opts...) } -// Register registers a struct type with a numeric ID for cross-language serialization. +// RegisterStruct registers a struct type with a numeric ID for cross-language serialization. // This is compatible with Java's fory.register(Class, int) method. // type_ can be either a reflect.Type or an instance of the type // typeID should be the user type ID in the range 0-8192 (the internal type ID will be added automatically) // Note: For enum types, use RegisterEnum instead. -func (f *Fory) Register(type_ interface{}, typeID uint32) error { +func (f *Fory) RegisterStruct(type_ interface{}, typeID uint32) error { var t reflect.Type if rt, ok := type_.(reflect.Type); ok { t = rt @@ -216,10 +216,10 @@ func (f *Fory) Register(type_ interface{}, typeID uint32) error { } } - // Only struct types are supported via Register + // Only struct types are supported via RegisterStruct // For enums, use RegisterEnum if t.Kind() != reflect.Struct { - return fmt.Errorf("Register only supports struct types; for enum types use RegisterEnum. Got: %v", t.Kind()) + return fmt.Errorf("RegisterStruct only supports struct types; for enum types use RegisterEnum. Got: %v", t.Kind()) } // Determine the internal type ID based on config @@ -234,14 +234,14 @@ func (f *Fory) Register(type_ interface{}, typeID uint32) error { // Calculate full type ID: (userID << 8) | internalTypeID fullTypeID := (typeID << 8) | uint32(internalTypeID) - return f.typeResolver.RegisterByID(t, fullTypeID) + return f.typeResolver.RegisterStruct(t, fullTypeID) } -// RegisterByName registers a named struct type for cross-language serialization +// RegisterNamedStruct registers a named struct type for cross-language serialization // type_ can be either a reflect.Type or an instance of the type // typeName can include a namespace prefix separated by "." (e.g., "example.Foo") -// Note: For enum types, use RegisterEnumByName instead. -func (f *Fory) RegisterByName(type_ interface{}, typeName string) error { +// Note: For enum types, use RegisterNamedEnum instead. +func (f *Fory) RegisterNamedStruct(type_ interface{}, typeName string) error { var t reflect.Type if rt, ok := type_.(reflect.Type); ok { t = rt @@ -252,7 +252,7 @@ func (f *Fory) RegisterByName(type_ interface{}, typeName string) error { } } if t.Kind() != reflect.Struct { - return fmt.Errorf("RegisterByName only supports struct types; for enum types use RegisterEnumByName. Got: %v", t.Kind()) + return fmt.Errorf("RegisterNamedStruct only supports struct types; for enum types use RegisterNamedEnum. Got: %v", t.Kind()) } // Split typeName by last "." to extract namespace and type name namespace := "" @@ -261,7 +261,7 @@ func (f *Fory) RegisterByName(type_ interface{}, typeName string) error { namespace = typeName[:lastDot] name = typeName[lastDot+1:] } - return f.typeResolver.RegisterNamedType(t, 0, namespace, name) + return f.typeResolver.RegisterNamedStruct(t, 0, namespace, name) } // RegisterEnum registers an enum type with a numeric ID for cross-language serialization. @@ -292,14 +292,14 @@ func (f *Fory) RegisterEnum(type_ interface{}, typeID uint32) error { // Calculate full type ID: (userID << 8) | ENUM fullTypeID := (typeID << 8) | uint32(ENUM) - return f.typeResolver.RegisterEnumByID(t, fullTypeID) + return f.typeResolver.RegisterEnum(t, fullTypeID) } -// RegisterEnumByName registers an enum type with a name for cross-language serialization. +// RegisterNamedEnum registers an enum type with a name for cross-language serialization. // In Go, enums are typically defined as int-based types (e.g., type Color int32). // type_ can be either a reflect.Type or an instance of the enum type // typeName can include a namespace prefix separated by "." (e.g., "example.Color") -func (f *Fory) RegisterEnumByName(type_ interface{}, typeName string) error { +func (f *Fory) RegisterNamedEnum(type_ interface{}, typeName string) error { var t reflect.Type if rt, ok := type_.(reflect.Type); ok { t = rt @@ -316,7 +316,7 @@ func (f *Fory) RegisterEnumByName(type_ interface{}, typeName string) error { reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: // OK default: - return fmt.Errorf("RegisterEnumByName only supports numeric types (Go enums); got: %v", t.Kind()) + return fmt.Errorf("RegisterNamedEnum only supports numeric types (Go enums); got: %v", t.Kind()) } // Split typeName by last "." to extract namespace and type name @@ -326,13 +326,13 @@ func (f *Fory) RegisterEnumByName(type_ interface{}, typeName string) error { namespace = typeName[:lastDot] name = typeName[lastDot+1:] } - return f.typeResolver.RegisterEnumByName(t, namespace, name) + return f.typeResolver.RegisterNamedEnum(t, namespace, name) } -// RegisterExtensionType registers a type as an extension type with a numeric ID. +// RegisterExtension registers a type as an extension type with a numeric ID. // Extension types use a custom serializer provided by the user. // typeID should be the user type ID in the range 0-8192. -func (f *Fory) RegisterExtensionType(type_ interface{}, typeID uint32, serializer ExtensionSerializer) error { +func (f *Fory) RegisterExtension(type_ interface{}, typeID uint32, serializer ExtensionSerializer) error { var t reflect.Type if rt, ok := type_.(reflect.Type); ok { t = rt @@ -342,10 +342,10 @@ func (f *Fory) RegisterExtensionType(type_ interface{}, typeID uint32, serialize t = t.Elem() } } - return f.typeResolver.RegisterExtensionTypeByID(t, typeID, serializer) + return f.typeResolver.RegisterExtension(t, typeID, serializer) } -// RegisterExtensionTypeByName registers a type as an extension type (NAMED_EXT) for cross-language serialization. +// RegisterNamedExtension registers a type as an extension type (NAMED_EXT) for cross-language serialization. // Extension types use a custom serializer provided by the user. // This is used for types with custom serializers in cross-language serialization. // @@ -365,8 +365,8 @@ func (f *Fory) RegisterExtensionType(type_ interface{}, typeID uint32, serialize // } // // // Register with custom serializer -// f.RegisterExtensionTypeByName(MyExt{}, "my_ext", &MyExtSerializer{}) -func (f *Fory) RegisterExtensionTypeByName(type_ interface{}, typeName string, serializer ExtensionSerializer) error { +// f.RegisterNamedExtension(MyExt{}, "my_ext", &MyExtSerializer{}) +func (f *Fory) RegisterNamedExtension(type_ interface{}, typeName string, serializer ExtensionSerializer) error { var t reflect.Type if rt, ok := type_.(reflect.Type); ok { t = rt @@ -376,7 +376,7 @@ func (f *Fory) RegisterExtensionTypeByName(type_ interface{}, typeName string, s t = t.Elem() } } - return f.typeResolver.RegisterExtensionType(t, "", typeName, serializer) + return f.typeResolver.RegisterNamedExtension(t, "", typeName, serializer) } // Reset clears internal state for reuse diff --git a/go/fory/fory_compatible_test.go b/go/fory/fory_compatible_test.go index a171ba22e1..62ecbb1253 100644 --- a/go/fory/fory_compatible_test.go +++ b/go/fory/fory_compatible_test.go @@ -120,7 +120,7 @@ type NestedOuterIncompatible struct { } func TestMetaShareEnabled(t *testing.T) { - fory := NewForyWithOptions(WithCompatible(true)) + fory := NewForyWithOptions(WithXlang(true), WithCompatible(true)) assert.True(t, fory.config.Compatible, "Expected compatible mode to be enabled") assert.NotNil(t, fory.metaContext, "Expected metaContext to be initialized when compatible=true") @@ -128,7 +128,7 @@ func TestMetaShareEnabled(t *testing.T) { } func TestMetaShareDisabled(t *testing.T) { - fory := NewForyWithOptions(WithCompatible(false)) + fory := NewForyWithOptions(WithXlang(true), WithCompatible(false)) assert.False(t, fory.config.Compatible, "Expected compatible mode to be disabled") assert.Nil(t, fory.metaContext, "Expected metaContext to be nil when compatible=false") @@ -176,10 +176,10 @@ func TestCompatibleSerializationScenarios(t *testing.T) { } }(), writerSetup: func(f *Fory) error { - return f.RegisterByName(ComplexObject2{}, "test.ComplexObject2") + return f.RegisterNamedStruct(ComplexObject2{}, "test.ComplexObject2") }, readerSetup: func(f *Fory) error { - return f.RegisterByName(ComplexObject2{}, "test.ComplexObject2") + return f.RegisterNamedStruct(ComplexObject2{}, "test.ComplexObject2") }, assertFunc: func(t *testing.T, input interface{}, output interface{}) { in := input.(ComplexObject1) @@ -353,10 +353,10 @@ func TestCompatibleSerializationScenarios(t *testing.T) { } }(), writerSetup: func(f *Fory) error { - return f.RegisterByName(SimpleDataClass{}, "SimpleDataClass") + return f.RegisterNamedStruct(SimpleDataClass{}, "SimpleDataClass") }, readerSetup: func(f *Fory) error { - return f.RegisterByName(SimpleDataClass{}, "SimpleDataClass") + return f.RegisterNamedStruct(SimpleDataClass{}, "SimpleDataClass") }, assertFunc: func(t *testing.T, input interface{}, output interface{}) { in := input.(PointerDataClass) @@ -381,10 +381,10 @@ func TestCompatibleSerializationScenarios(t *testing.T) { } }(), writerSetup: func(f *Fory) error { - return f.RegisterByName(SimpleDataClass{}, "SimpleDataClass") + return f.RegisterNamedStruct(SimpleDataClass{}, "SimpleDataClass") }, readerSetup: func(f *Fory) error { - return f.RegisterByName(InconsistentDataClass{}, "SimpleDataClass") + return f.RegisterNamedStruct(InconsistentDataClass{}, "SimpleDataClass") }, assertFunc: func(t *testing.T, input interface{}, output interface{}) { in := input.(PointerDataClass) @@ -430,13 +430,13 @@ func TestCompatibleSerializationScenarios(t *testing.T) { Inner: SimpleDataClass{Name: "inner", Age: 18, Active: true}, }, writerSetup: func(f *Fory) error { - if err := f.RegisterByName(SimpleDataClass{}, "SimpleDataClass"); err != nil { + if err := f.RegisterNamedStruct(SimpleDataClass{}, "SimpleDataClass"); err != nil { return err } return nil }, readerSetup: func(f *Fory) error { - if err := f.RegisterByName(SimpleDataClass{}, "SimpleDataClass"); err != nil { + if err := f.RegisterNamedStruct(SimpleDataClass{}, "SimpleDataClass"); err != nil { return err } return nil @@ -458,13 +458,13 @@ func TestCompatibleSerializationScenarios(t *testing.T) { Inner: SimpleDataClass{Name: "inner", Age: 18, Active: true}, }, writerSetup: func(f *Fory) error { - if err := f.RegisterByName(SimpleDataClass{}, "SimpleDataClass"); err != nil { + if err := f.RegisterNamedStruct(SimpleDataClass{}, "SimpleDataClass"); err != nil { return err } return nil }, readerSetup: func(f *Fory) error { - if err := f.RegisterByName(InconsistentDataClass{}, "SimpleDataClass"); err != nil { + if err := f.RegisterNamedStruct(InconsistentDataClass{}, "SimpleDataClass"); err != nil { return err } return nil @@ -501,23 +501,23 @@ type compatibilityCase struct { func runCompatibilityCase(t *testing.T, tc compatibilityCase) { t.Helper() - writer := NewForyWithOptions(WithCompatible(true)) + writer := NewForyWithOptions(WithXlang(true), WithCompatible(true)) if tc.writerSetup != nil { err := tc.writerSetup(writer) assert.NoError(t, err) } - err := writer.RegisterByName(tc.writeType, tc.tag) + err := writer.RegisterNamedStruct(tc.writeType, tc.tag) assert.NoError(t, err) data, err := writer.Marshal(tc.input) assert.NoError(t, err) - reader := NewForyWithOptions(WithCompatible(true)) + reader := NewForyWithOptions(WithXlang(true), WithCompatible(true)) if tc.readerSetup != nil { err = tc.readerSetup(reader) assert.NoError(t, err) } - err = reader.RegisterByName(tc.readType, tc.tag) + err = reader.RegisterNamedStruct(tc.readType, tc.tag) assert.NoError(t, err) target := reflect.New(reflect.TypeOf(tc.readType)) diff --git a/go/fory/fory_test.go b/go/fory/fory_test.go index c74c6be6cf..6a9a3811fc 100644 --- a/go/fory/fory_test.go +++ b/go/fory/fory_test.go @@ -121,7 +121,7 @@ func commonArray() []interface{} { func TestSerializePrimitives(t *testing.T) { for _, referenceTracking := range []bool{false, true} { - fory := NewFory(WithRefTracking(referenceTracking)) + fory := NewFory(WithXlang(true), WithRefTracking(referenceTracking)) for _, value := range primitiveData() { serde(t, fory, value) } @@ -130,7 +130,7 @@ func TestSerializePrimitives(t *testing.T) { func TestSerializeInterface(t *testing.T) { for _, referenceTracking := range []bool{false, true} { - fory := NewFory(WithRefTracking(referenceTracking)) + fory := NewFory(WithXlang(true), WithRefTracking(referenceTracking)) var a interface{} a = -1 serde(t, fory, a) @@ -148,7 +148,7 @@ func TestSerializeInterface(t *testing.T) { func TestSerializePtr(t *testing.T) { for _, referenceTracking := range []bool{false, true} { - fory := NewFory(WithRefTracking(referenceTracking)) + fory := NewFory(WithXlang(true), WithRefTracking(referenceTracking)) a := -100 b := &a serde(t, fory, b) @@ -164,7 +164,7 @@ func TestSerializePtr(t *testing.T) { func TestSerializeSlice(t *testing.T) { for _, referenceTracking := range []bool{false, true} { - fory := NewFory(WithRefTracking(referenceTracking)) + fory := NewFory(WithXlang(true), WithRefTracking(referenceTracking)) serde(t, fory, []byte{0, 1, MaxUint8}) // serde(t, fory, []int8{MinInt8, -1, 0, 1, MaxInt8}) serde(t, fory, []int16{MinInt16, -1, 0, 1, MaxInt16}) @@ -184,7 +184,7 @@ func TestSerializeSlice(t *testing.T) { func TestSerializeMap(t *testing.T) { for _, referenceTracking := range []bool{false, true} { - fory := NewFory(WithRefTracking(referenceTracking)) + fory := NewFory(WithXlang(true), WithRefTracking(referenceTracking)) // "str1" is deserialized by interface type, which will be set to map key whose type is string. // so we need to save interface dynamic value type instead of interface value in reference resolver. { @@ -210,9 +210,55 @@ func TestSerializeMap(t *testing.T) { } } +func TestSerializeSet(t *testing.T) { + for _, referenceTracking := range []bool{false, true} { + fory := NewFory(WithXlang(true), WithRefTracking(referenceTracking)) + + // Test Set[string] + { + s := NewSet[string]() + s.Add("a", "b", "c") + data, err := fory.Marshal(s) + require.NoError(t, err) + var result Set[string] + err = fory.Unmarshal(data, &result) + require.NoError(t, err) + require.Equal(t, 3, result.Len()) + require.True(t, result.Contains("a")) + require.True(t, result.Contains("b")) + require.True(t, result.Contains("c")) + } + + // Test Set[int32] + { + s := NewSet[int32]() + s.Add(1, 2, 3, 100) + data, err := fory.Marshal(s) + require.NoError(t, err) + var result Set[int32] + err = fory.Unmarshal(data, &result) + require.NoError(t, err) + require.Equal(t, 4, result.Len()) + require.True(t, result.Contains(1)) + require.True(t, result.Contains(100)) + } + + // Test empty set + { + s := NewSet[string]() + data, err := fory.Marshal(s) + require.NoError(t, err) + var result Set[string] + err = fory.Unmarshal(data, &result) + require.NoError(t, err) + require.Equal(t, 0, result.Len()) + } + } +} + func TestSerializeArray(t *testing.T) { for _, referenceTracking := range []bool{false, true} { - fory := NewFory(WithRefTracking(referenceTracking)) + fory := NewFory(WithXlang(true), WithRefTracking(referenceTracking)) for _, data := range commonArray() { serde(t, fory, data) } @@ -227,7 +273,7 @@ func TestSerializeStructSimple(t *testing.T) { type A struct { F1 []string } - require.Nil(t, fory.RegisterByName(A{}, "example.A")) + require.Nil(t, fory.RegisterNamedStruct(A{}, "example.A")) serde(t, fory, A{}) serde(t, fory, &A{}) serde(t, fory, A{F1: []string{"str1", "", "str2"}}) @@ -237,7 +283,7 @@ func TestSerializeStructSimple(t *testing.T) { F1 []string F2 map[string]int32 } - require.Nil(t, fory.RegisterByName(SimpleB{}, "example.SimpleB")) + require.Nil(t, fory.RegisterNamedStruct(SimpleB{}, "example.SimpleB")) serde(t, fory, SimpleB{}) serde(t, fory, SimpleB{ F1: []string{"str1", "", "str2"}, @@ -250,17 +296,17 @@ func TestSerializeStructSimple(t *testing.T) { } func TestRegisterById(t *testing.T) { - fory := NewFory(WithRefTracking(false)) + fory := NewFory(WithXlang(true), WithRefTracking(false)) type simple struct { Field string } - require.NoError(t, fory.RegisterByName(simple{}, "simple")) + require.NoError(t, fory.RegisterNamedStruct(simple{}, "simple")) serde(t, fory, simple{Field: "value"}) } func TestSerializeBeginWithMagicNumber(t *testing.T) { strSlice := []string{"str1", "str1", "", "", "str2"} - fory := NewFory(WithRefTracking(true)) + fory := NewFory(WithXlang(true), WithRefTracking(true)) bytes, err := fory.Marshal(strSlice) require.Nil(t, err, fmt.Sprintf("serialize value %s with type %s failed: %s", reflect.ValueOf(strSlice), reflect.TypeOf(strSlice), err)) @@ -305,8 +351,8 @@ func newFoo() Foo { func TestSerializeStruct(t *testing.T) { for _, referenceTracking := range []bool{false, true} { - fory := NewFory(WithRefTracking(referenceTracking)) - require.Nil(t, fory.RegisterByName(Bar{}, "example.Bar")) + fory := NewFory(WithXlang(true), WithRefTracking(referenceTracking)) + require.Nil(t, fory.RegisterNamedStruct(Bar{}, "example.Bar")) serde(t, fory, &Bar{}) bar := Bar{F1: 1, F2: "str"} serde(t, fory, bar) @@ -316,14 +362,14 @@ func TestSerializeStruct(t *testing.T) { F1 Bar F2 interface{} } - require.Nil(t, fory.RegisterByName(A{}, "example.A")) + require.Nil(t, fory.RegisterNamedStruct(A{}, "example.A")) serde(t, fory, A{}) serde(t, fory, &A{}) // Use int64 for interface{} fields since xlang deserializes integers to int64 serde(t, fory, A{F1: Bar{F1: 1, F2: "str"}, F2: int64(-1)}) serde(t, fory, &A{F1: Bar{F1: 1, F2: "str"}, F2: int64(-1)}) - require.Nil(t, fory.RegisterByName(Foo{}, "example.Foo")) + require.Nil(t, fory.RegisterNamedStruct(Foo{}, "example.Foo")) foo := newFoo() serde(t, fory, foo) serde(t, fory, &foo) @@ -331,12 +377,12 @@ func TestSerializeStruct(t *testing.T) { } func TestSerializeCircularReference(t *testing.T) { - fory := NewFory(WithRefTracking(true)) + fory := NewFory(WithXlang(true), WithRefTracking(true)) { type A struct { A1 *A } - require.Nil(t, fory.RegisterByName(A{}, "example.A")) + require.Nil(t, fory.RegisterNamedStruct(A{}, "example.A")) // If use `A{}` instead of `&A{}` and pass `a` instead of `&a`, there will be serialization data duplication // and can't be deserialized by other languages too. // TODO(chaokunyang) If pass by value(have a copy) and there are some inner value reference, return a readable @@ -356,7 +402,7 @@ func TestSerializeCircularReference(t *testing.T) { F2 *CircularRefB F3 *CircularRefB } - require.Nil(t, fory.RegisterByName(CircularRefB{}, "example.CircularRefB")) + require.Nil(t, fory.RegisterNamedStruct(CircularRefB{}, "example.CircularRefB")) b := &CircularRefB{F1: "str"} b.F2 = b b.F3 = b @@ -372,7 +418,7 @@ func TestSerializeCircularReference(t *testing.T) { } func TestSerializeComplexReference(t *testing.T) { - fory := NewFory(WithRefTracking(true)) + fory := NewFory(WithXlang(true), WithRefTracking(true)) type A struct { F1 string F2 *A @@ -384,8 +430,8 @@ func TestSerializeComplexReference(t *testing.T) { F3 *A F4 *B } - require.Nil(t, fory.RegisterByName(A{}, "example.ComplexRefA")) - require.Nil(t, fory.RegisterByName(B{}, "example.ComplexRefB")) + require.Nil(t, fory.RegisterNamedStruct(A{}, "example.ComplexRefA")) + require.Nil(t, fory.RegisterNamedStruct(B{}, "example.ComplexRefB")) a := &A{F1: "str"} a.F2 = a @@ -412,7 +458,7 @@ func TestSerializeComplexReference(t *testing.T) { } func TestSerializeCommonReference(t *testing.T) { - fory := NewFory(WithRefTracking(true)) + fory := NewFory(WithXlang(true), WithRefTracking(true)) var values []interface{} values = append(values, commonSlice()...) values = append(values, commonMap()...) @@ -432,7 +478,7 @@ func TestSerializeCommonReference(t *testing.T) { // TODO: Re-enable when zero-copy serialization API is updated /* func TestSerializeZeroCopy(t *testing.T) { - fory := NewFory(WithRefTracking(true)) + fory := NewFory(WithXlang(true), WithRefTracking(true)) list := []interface{}{"str", make([]byte, 1000)} buf := NewByteBuffer(nil) var bufferObjects []BufferObject @@ -503,9 +549,9 @@ func serde(t *testing.T, fory *Fory, value interface{}) { // go tool pprof -text -nodecount=10 ./fory.test mem.out func BenchmarkMarshal(b *testing.B) { - fory := NewFory(WithRefTracking(true)) - require.Nil(b, fory.RegisterByName(Foo{}, "example.Foo")) - require.Nil(b, fory.RegisterByName(Bar{}, "example.Bar")) + fory := NewFory(WithXlang(true), WithRefTracking(true)) + require.Nil(b, fory.RegisterNamedStruct(Foo{}, "example.Foo")) + require.Nil(b, fory.RegisterNamedStruct(Bar{}, "example.Bar")) value := benchData() for i := 0; i < b.N; i++ { _, err := fory.Marshal(value) @@ -516,9 +562,9 @@ func BenchmarkMarshal(b *testing.B) { } func BenchmarkUnmarshal(b *testing.B) { - fory := NewFory(WithRefTracking(true)) - require.Nil(b, fory.RegisterByName(Foo{}, "example.Foo")) - require.Nil(b, fory.RegisterByName(Bar{}, "example.Bar")) + fory := NewFory(WithXlang(true), WithRefTracking(true)) + require.Nil(b, fory.RegisterNamedStruct(Foo{}, "example.Foo")) + require.Nil(b, fory.RegisterNamedStruct(Bar{}, "example.Bar")) value := benchData() data, err := fory.Marshal(value) if err != nil { @@ -543,7 +589,7 @@ func benchData() interface{} { } func ExampleFory_Serialize() { - f := New() + f := New(WithXlang(true)) list := []interface{}{true, false, "str", -1.1, 1, make([]int32, 5), make([]float64, 5)} bytes, err := f.Serialize(list) if err != nil { @@ -582,7 +628,7 @@ specifying concrete map, slice, or array types for deserialization. */ func TestMapEachIndividually(t *testing.T) { - fory := NewFory(WithRefTracking(true)) + fory := NewFory(WithXlang(true), WithRefTracking(true)) for _, srcAny := range commonMap() { srcType := reflect.TypeOf(srcAny) endPtr := reflect.New(srcType) @@ -597,7 +643,7 @@ func TestMapEachIndividually(t *testing.T) { } func TestArrayEachIndividually(t *testing.T) { - fory := NewFory(WithRefTracking(true)) + fory := NewFory(WithXlang(true), WithRefTracking(true)) for _, srcAny := range commonArray() { srcType := reflect.TypeOf(srcAny) t.Logf("Testing type: %v", srcType) @@ -613,7 +659,7 @@ func TestArrayEachIndividually(t *testing.T) { } func TestSliceEachIndividually(t *testing.T) { - fory := NewFory(WithRefTracking(true)) + fory := NewFory(WithXlang(true), WithRefTracking(true)) for _, srcAny := range commonSlice() { srcType := reflect.TypeOf(srcAny) endPtr := reflect.New(srcType) @@ -635,11 +681,11 @@ func TestStructWithNestedSlice(t *testing.T) { Items []Item } - fory := NewFory(WithRefTracking(true)) - if err := fory.RegisterByName(Example{}, "Example"); err != nil { + fory := NewFory(WithXlang(true), WithRefTracking(true)) + if err := fory.RegisterNamedStruct(Example{}, "Example"); err != nil { panic(err) } - if err := fory.RegisterByName(Item{}, "Item"); err != nil { + if err := fory.RegisterNamedStruct(Item{}, "Item"); err != nil { panic(err) } diff --git a/go/fory/fory_typed_test.go b/go/fory/fory_typed_test.go index 193c915c58..8108076572 100644 --- a/go/fory/fory_typed_test.go +++ b/go/fory/fory_typed_test.go @@ -142,7 +142,7 @@ func TestSerializeGenericComplex(t *testing.T) { Name string Value int32 } - err := f.RegisterByName(TestStruct{}, "example.TestStruct") + err := f.RegisterNamedStruct(TestStruct{}, "example.TestStruct") require.NoError(t, err) original := TestStruct{Name: "test", Value: 100} @@ -211,7 +211,7 @@ func TestSerializeDeserializeRoundTrip(t *testing.T) { ID int64 Name string } - f.RegisterByName(CustomStruct{}, "test.CustomStruct") + f.RegisterNamedStruct(CustomStruct{}, "test.CustomStruct") original := CustomStruct{ID: 123, Name: "test"} data, err := Serialize(f, &original) diff --git a/go/fory/map.go b/go/fory/map.go index 6aabbe3729..cf2f12ae0f 100644 --- a/go/fory/map.go +++ b/go/fory/map.go @@ -282,15 +282,16 @@ func (s mapSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, ha if readMapRefAndType(ctx, refMode, readType, value) || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } // ReadData deserializes map data using chunk protocol -func (s mapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s mapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() refResolver := ctx.RefResolver() typeResolver := ctx.TypeResolver() + type_ := value.Type() // Initialize map if value.IsNil() { @@ -412,7 +413,7 @@ func (s mapSerializer) readSingleValue(ctx *ReadContext, buf *ByteBuffer, ctxErr valType = staticType } v := reflect.New(valType).Elem() - ti.Serializer.ReadData(ctx, valType, v) + ti.Serializer.ReadData(ctx, v) if ctx.HasError() { return reflect.Value{} } diff --git a/go/fory/map_primitive.go b/go/fory/map_primitive.go index cb7c2f9d8e..327e90218e 100644 --- a/go/fory/map_primitive.go +++ b/go/fory/map_primitive.go @@ -747,9 +747,9 @@ func (s stringStringMapSerializer) Write(ctx *WriteContext, refMode RefMode, wri writeMapStringString(ctx.buffer, value.Interface().(map[string]string), hasGenerics) } -func (s stringStringMapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s stringStringMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { if value.IsNil() { - value.Set(reflect.MakeMap(type_)) + value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) result := readMapStringString(ctx.buffer, ctx.Err()) @@ -761,7 +761,7 @@ func (s stringStringMapSerializer) Read(ctx *ReadContext, refMode RefMode, readT if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s stringStringMapSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -782,9 +782,9 @@ func (s stringInt64MapSerializer) Write(ctx *WriteContext, refMode RefMode, writ writeMapStringInt64(ctx.buffer, value.Interface().(map[string]int64), hasGenerics) } -func (s stringInt64MapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s stringInt64MapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { if value.IsNil() { - value.Set(reflect.MakeMap(type_)) + value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) result := readMapStringInt64(ctx.buffer, ctx.Err()) @@ -796,7 +796,7 @@ func (s stringInt64MapSerializer) Read(ctx *ReadContext, refMode RefMode, readTy if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s stringInt64MapSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -817,9 +817,9 @@ func (s stringIntMapSerializer) Write(ctx *WriteContext, refMode RefMode, writeT writeMapStringInt(ctx.buffer, value.Interface().(map[string]int), hasGenerics) } -func (s stringIntMapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s stringIntMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { if value.IsNil() { - value.Set(reflect.MakeMap(type_)) + value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) result := readMapStringInt(ctx.buffer, ctx.Err()) @@ -831,7 +831,7 @@ func (s stringIntMapSerializer) Read(ctx *ReadContext, refMode RefMode, readType if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s stringIntMapSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -852,9 +852,9 @@ func (s stringFloat64MapSerializer) Write(ctx *WriteContext, refMode RefMode, wr writeMapStringFloat64(ctx.buffer, value.Interface().(map[string]float64), hasGenerics) } -func (s stringFloat64MapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s stringFloat64MapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { if value.IsNil() { - value.Set(reflect.MakeMap(type_)) + value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) result := readMapStringFloat64(ctx.buffer, ctx.Err()) @@ -866,7 +866,7 @@ func (s stringFloat64MapSerializer) Read(ctx *ReadContext, refMode RefMode, read if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s stringFloat64MapSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -887,9 +887,9 @@ func (s stringBoolMapSerializer) Write(ctx *WriteContext, refMode RefMode, write writeMapStringBool(ctx.buffer, value.Interface().(map[string]bool), hasGenerics) } -func (s stringBoolMapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s stringBoolMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { if value.IsNil() { - value.Set(reflect.MakeMap(type_)) + value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) result := readMapStringBool(ctx.buffer, ctx.Err()) @@ -901,7 +901,7 @@ func (s stringBoolMapSerializer) Read(ctx *ReadContext, refMode RefMode, readTyp if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s stringBoolMapSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -922,9 +922,9 @@ func (s int32Int32MapSerializer) Write(ctx *WriteContext, refMode RefMode, write writeMapInt32Int32(ctx.buffer, value.Interface().(map[int32]int32), hasGenerics) } -func (s int32Int32MapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int32Int32MapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { if value.IsNil() { - value.Set(reflect.MakeMap(type_)) + value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) result := readMapInt32Int32(ctx.buffer, ctx.Err()) @@ -936,7 +936,7 @@ func (s int32Int32MapSerializer) Read(ctx *ReadContext, refMode RefMode, readTyp if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int32Int32MapSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -957,9 +957,9 @@ func (s int64Int64MapSerializer) Write(ctx *WriteContext, refMode RefMode, write writeMapInt64Int64(ctx.buffer, value.Interface().(map[int64]int64), hasGenerics) } -func (s int64Int64MapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int64Int64MapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { if value.IsNil() { - value.Set(reflect.MakeMap(type_)) + value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) result := readMapInt64Int64(ctx.buffer, ctx.Err()) @@ -971,7 +971,7 @@ func (s int64Int64MapSerializer) Read(ctx *ReadContext, refMode RefMode, readTyp if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int64Int64MapSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -992,9 +992,9 @@ func (s intIntMapSerializer) Write(ctx *WriteContext, refMode RefMode, writeType writeMapIntInt(ctx.buffer, value.Interface().(map[int]int), hasGenerics) } -func (s intIntMapSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s intIntMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { if value.IsNil() { - value.Set(reflect.MakeMap(type_)) + value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) result := readMapIntInt(ctx.buffer, ctx.Err()) @@ -1006,7 +1006,7 @@ func (s intIntMapSerializer) Read(ctx *ReadContext, refMode RefMode, readType bo if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s intIntMapSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/pointer.go b/go/fory/pointer.go index 7c3ef54818..6dfa12a5b1 100644 --- a/go/fory/pointer.go +++ b/go/fory/pointer.go @@ -94,12 +94,12 @@ func (s *ptrToValueSerializer) Write(ctx *WriteContext, refMode RefMode, writeTy s.WriteData(ctx, value) } -func (s *ptrToValueSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s *ptrToValueSerializer) ReadData(ctx *ReadContext, value reflect.Value) { // Check if value is already allocated (for circular reference handling) var newVal reflect.Value if value.IsNil() { // Allocate new value - newVal = reflect.New(type_.Elem()) + newVal = reflect.New(value.Type().Elem()) value.Set(newVal) } else { // Value already allocated (circular reference case) @@ -110,7 +110,7 @@ func (s *ptrToValueSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, va // This allows circular references to work correctly ctx.RefResolver().Reference(value) - s.valueSerializer.ReadData(ctx, type_.Elem(), newVal.Elem()) + s.valueSerializer.ReadData(ctx, newVal.Elem()) } func (s *ptrToValueSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, hasGenerics bool, value reflect.Value) { @@ -157,13 +157,13 @@ func (s *ptrToValueSerializer) Read(ctx *ReadContext, refMode RefMode, readType value.Set(reflect.New(value.Type().Elem())) } ctx.RefResolver().Reference(value) - structSer.ReadData(ctx, value.Type().Elem(), value.Elem()) + structSer.ReadData(ctx, value.Elem()) return } } } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s *ptrToValueSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -210,9 +210,9 @@ func (s *ptrToInterfaceSerializer) Write(ctx *WriteContext, refMode RefMode, wri s.WriteData(ctx, value) } -func (s *ptrToInterfaceSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s *ptrToInterfaceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { // Create a new interface pointer - newVal := reflect.New(type_.Elem()) + newVal := reflect.New(value.Type().Elem()) // Use ReadValue to handle the polymorphic interface value with ref tracking and type info ctx.ReadValue(newVal.Elem(), RefModeTracking, true) @@ -248,7 +248,7 @@ func (s *ptrToInterfaceSerializer) Read(ctx *ReadContext, refMode RefMode, readT } } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s *ptrToInterfaceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/primitive.go b/go/fory/primitive.go index d8978b88ac..a2341ab435 100644 --- a/go/fory/primitive.go +++ b/go/fory/primitive.go @@ -46,7 +46,7 @@ func (s boolSerializer) Write(ctx *WriteContext, refMode RefMode, writeType bool s.WriteData(ctx, value) } -func (s boolSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s boolSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetBool(ctx.buffer.ReadBool(err)) } @@ -65,7 +65,7 @@ func (s boolSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, h if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s boolSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -93,7 +93,7 @@ func (s int8Serializer) Write(ctx *WriteContext, refMode RefMode, writeType bool s.WriteData(ctx, value) } -func (s int8Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int8Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetInt(int64(ctx.buffer.ReadInt8(err))) } @@ -112,7 +112,7 @@ func (s int8Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool, h if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int8Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -139,7 +139,7 @@ func (s byteSerializer) Write(ctx *WriteContext, refMode RefMode, writeType bool s.WriteData(ctx, value) } -func (s byteSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s byteSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetUint(uint64(ctx.buffer.ReadUint8(err))) } @@ -158,7 +158,7 @@ func (s byteSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, h if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s byteSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -185,7 +185,7 @@ func (s uint16Serializer) Write(ctx *WriteContext, refMode RefMode, writeType bo s.WriteData(ctx, value) } -func (s uint16Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s uint16Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetUint(uint64(ctx.buffer.ReadUint16(err))) } @@ -204,7 +204,7 @@ func (s uint16Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s uint16Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -231,7 +231,7 @@ func (s uint32Serializer) Write(ctx *WriteContext, refMode RefMode, writeType bo s.WriteData(ctx, value) } -func (s uint32Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s uint32Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetUint(uint64(ctx.buffer.ReadVaruint32(err))) } @@ -250,7 +250,7 @@ func (s uint32Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s uint32Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -277,7 +277,7 @@ func (s uint64Serializer) Write(ctx *WriteContext, refMode RefMode, writeType bo s.WriteData(ctx, value) } -func (s uint64Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s uint64Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetUint(ctx.buffer.ReadVaruint64(err)) } @@ -296,7 +296,7 @@ func (s uint64Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s uint64Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -322,7 +322,7 @@ func (s int16Serializer) Write(ctx *WriteContext, refMode RefMode, writeType boo s.WriteData(ctx, value) } -func (s int16Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int16Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetInt(int64(ctx.buffer.ReadInt16(err))) } @@ -340,7 +340,7 @@ func (s int16Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int16Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -366,7 +366,7 @@ func (s int32Serializer) Write(ctx *WriteContext, refMode RefMode, writeType boo s.WriteData(ctx, value) } -func (s int32Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int32Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetInt(int64(ctx.buffer.ReadVarint32(err))) } @@ -384,7 +384,7 @@ func (s int32Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int32Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -410,7 +410,7 @@ func (s int64Serializer) Write(ctx *WriteContext, refMode RefMode, writeType boo s.WriteData(ctx, value) } -func (s int64Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s int64Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetInt(ctx.buffer.ReadVarint64(err)) } @@ -428,7 +428,7 @@ func (s int64Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s int64Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -452,7 +452,7 @@ func (s intSerializer) Write(ctx *WriteContext, refMode RefMode, writeType bool, s.WriteData(ctx, value) } -func (s intSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s intSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetInt(ctx.buffer.ReadVarint64(err)) } @@ -470,7 +470,7 @@ func (s intSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, ha if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s intSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -496,7 +496,7 @@ func (s float32Serializer) Write(ctx *WriteContext, refMode RefMode, writeType b s.WriteData(ctx, value) } -func (s float32Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s float32Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetFloat(float64(ctx.buffer.ReadFloat32(err))) } @@ -514,7 +514,7 @@ func (s float32Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s float32Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -540,7 +540,7 @@ func (s float64Serializer) Write(ctx *WriteContext, refMode RefMode, writeType b s.WriteData(ctx, value) } -func (s float64Serializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s float64Serializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.SetFloat(ctx.buffer.ReadFloat64(err)) } @@ -558,7 +558,7 @@ func (s float64Serializer) Read(ctx *ReadContext, refMode RefMode, readType bool if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s float64Serializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/reader.go b/go/fory/reader.go index 67f680fc05..bb034edadd 100644 --- a/go/fory/reader.go +++ b/go/fory/reader.go @@ -612,7 +612,7 @@ func (c *ReadContext) ReadValue(value reflect.Value, refMode RefMode, readType b if actualType == nil { // Unknown type - skip the data using the serializer (skipStructSerializer) if typeInfo.Serializer != nil { - typeInfo.Serializer.ReadData(c, nil, reflect.Value{}) + typeInfo.Serializer.ReadData(c, reflect.Value{}) } // Leave interface value as nil for unknown types return @@ -659,7 +659,7 @@ func (c *ReadContext) ReadValue(value reflect.Value, refMode RefMode, readType b readTarget = newValue } - typeInfo.Serializer.ReadData(c, actualType, readTarget) + typeInfo.Serializer.ReadData(c, readTarget) if c.HasError() { return } @@ -769,7 +769,7 @@ func (c *ReadContext) ReadStruct(value reflect.Value) { } // Read struct data directly - serializer.ReadData(c, structType, readTarget) + serializer.ReadData(c, readTarget) } // ReadInto reads a value using a specific serializer with optional ref/type info @@ -833,7 +833,7 @@ func (c *ReadContext) ReadArrayValue(target reflect.Value, refMode RefMode, read tempSlice.Set(reflect.MakeSlice(sliceType, target.Len(), target.Len())) // Use ReadData to read slice data (ref/type already handled) - serializer.ReadData(c, sliceType, tempSlice) + serializer.ReadData(c, tempSlice) if c.HasError() { return } diff --git a/go/fory/ref_resolver_test.go b/go/fory/ref_resolver_test.go index cfabb602fc..aed8e92e4b 100644 --- a/go/fory/ref_resolver_test.go +++ b/go/fory/ref_resolver_test.go @@ -148,11 +148,11 @@ func TestRefTrackingLargeCount(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := New(WithRefTracking(true)) + f := New(WithXlang(true), WithRefTracking(true)) - err := f.RegisterByName(&Inner{}, fmt.Sprintf("RefTest_Inner_%d", tt.count)) + err := f.RegisterNamedStruct(&Inner{}, fmt.Sprintf("RefTest_Inner_%d", tt.count)) require.NoError(t, err) - err = f.RegisterByName(&Outer{}, fmt.Sprintf("RefTest_Outer_%d", tt.count)) + err = f.RegisterNamedStruct(&Outer{}, fmt.Sprintf("RefTest_Outer_%d", tt.count)) require.NoError(t, err) original := make([]Outer, tt.count) diff --git a/go/fory/serializer.go b/go/fory/serializer.go index 3d5ed82e92..b79088874f 100644 --- a/go/fory/serializer.go +++ b/go/fory/serializer.go @@ -85,7 +85,7 @@ type Serializer interface { // This method should ONLY be used by collection serializers for nested element deserialization. // For general deserialization, use ReadFull instead. // Errors are set on the context via ctx.SetError(). - ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) + ReadData(ctx *ReadContext, value reflect.Value) // ReadWithTypeInfo deserializes with pre-read type information. // @@ -109,7 +109,7 @@ type Serializer interface { // ExtensionSerializer is a simplified interface for user-implemented extension serializers. // Users implement this interface to provide custom serialization logic for types -// registered via RegisterExtensionTypeByName. +// registered via RegisterNamedExtension. // // Unlike the full Serializer interface, ExtensionSerializer only requires implementing // the core data serialization logic - reference tracking, type info, and protocol @@ -119,117 +119,26 @@ type Serializer interface { // // type MyExtSerializer struct{} // -// func (s *MyExtSerializer) Write(buf *ByteBuffer, value interface{}) error { -// myExt := value.(MyExt) -// buf.WriteVarint32(myExt.Id) -// return nil +// func (s *MyExtSerializer) WriteData(ctx *WriteContext, value reflect.Value) { +// myExt := value.Interface().(MyExt) +// ctx.Buffer().WriteVarint32(myExt.Id) // } // -// func (s *MyExtSerializer) Read(buf *ByteBuffer) (interface{}, error) { -// id := buf.ReadVarint32(err) -// return MyExt{Id: id}, nil +// func (s *MyExtSerializer) ReadData(ctx *ReadContext, value reflect.Value) { +// id := ctx.Buffer().ReadVarint32(ctx.Err()) +// value.Set(reflect.ValueOf(MyExt{Id: id})) // } // // // Register with custom serializer -// f.RegisterExtensionTypeByName(MyExt{}, "my_ext", &MyExtSerializer{}) +// f.RegisterNamedExtension(MyExt{}, "my_ext", &MyExtSerializer{}) type ExtensionSerializer interface { - // Write serializes the value's data to the buffer. + // WriteData serializes the value's data to the buffer. // Only write the data fields - don't write ref flags or type info. - Write(buf *ByteBuffer, value interface{}) error + // Errors should be set on the context via ctx.SetError(). + WriteData(ctx *WriteContext, value reflect.Value) - // Read deserializes the value's data from the buffer. + // ReadData deserializes the value's data from the buffer into the provided value. // Only read the data fields - don't read ref flags or type info. - // Returns the deserialized value. - Read(buf *ByteBuffer) (interface{}, error) -} - -// extensionSerializerAdapter wraps an ExtensionSerializer to implement the full Serializer interface. -// This adapter handles reference tracking, type info writing/reading, and delegates the actual -// data serialization to the user-provided ExtensionSerializer. -type extensionSerializerAdapter struct { - type_ reflect.Type - typeTag string - userSerial ExtensionSerializer -} - -func (s *extensionSerializerAdapter) GetType() reflect.Type { return s.type_ } - -func (s *extensionSerializerAdapter) WriteData(ctx *WriteContext, value reflect.Value) { - // Delegate to user's serializer - if err := s.userSerial.Write(ctx.Buffer(), value.Interface()); err != nil { - ctx.SetError(FromError(err)) - } -} - -func (s *extensionSerializerAdapter) Write(ctx *WriteContext, refMode RefMode, writeType bool, hasGenerics bool, value reflect.Value) { - _ = hasGenerics // not used for extension serializers - buf := ctx.Buffer() - switch refMode { - case RefModeTracking: - refWritten, err := ctx.RefResolver().WriteRefOrNull(buf, value) - if err != nil { - ctx.SetError(FromError(err)) - return - } - if refWritten { - return - } - case RefModeNullOnly: - if isNil(value) { - buf.WriteInt8(NullFlag) - return - } - buf.WriteInt8(NotNullValueFlag) - } - if writeType { - typeInfo, err := ctx.TypeResolver().getTypeInfo(value, true) - if err != nil { - ctx.SetError(FromError(err)) - return - } - ctx.TypeResolver().WriteTypeInfo(buf, typeInfo, ctx.Err()) - } - s.WriteData(ctx, value) -} - -func (s *extensionSerializerAdapter) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { - // Delegate to user's serializer - result, err := s.userSerial.Read(ctx.Buffer()) - if err != nil { - ctx.SetError(FromError(err)) - return - } - // Set the result into the value - value.Set(reflect.ValueOf(result)) -} - -func (s *extensionSerializerAdapter) Read(ctx *ReadContext, refMode RefMode, readType bool, hasGenerics bool, value reflect.Value) { - _ = hasGenerics // not used for extension serializers - buf := ctx.Buffer() - ctxErr := ctx.Err() - switch refMode { - case RefModeTracking: - refID, refErr := ctx.RefResolver().TryPreserveRefId(buf) - if refErr != nil { - ctx.SetError(FromError(refErr)) - return - } - if refID < int32(NotNullValueFlag) { - obj := ctx.RefResolver().GetReadObject(refID) - if obj.IsValid() { - value.Set(obj) - } - return - } - case RefModeNullOnly: - flag := buf.ReadInt8(ctxErr) - if flag == NullFlag { - return - } - } - s.ReadData(ctx, value.Type(), value) -} - -func (s *extensionSerializerAdapter) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { - s.Read(ctx, refMode, false, false, value) + // Errors should be set on the context via ctx.SetError(). + ReadData(ctx *ReadContext, value reflect.Value) } diff --git a/go/fory/set.go b/go/fory/set.go index 768b36fd61..137f196535 100644 --- a/go/fory/set.go +++ b/go/fory/set.go @@ -21,16 +21,57 @@ import ( "reflect" ) -// GenericSet type. -// TODO use golang generics; support more concrete key types -type GenericSet map[interface{}]bool +// Set is a generic set type using Go generics. +// Uses struct{} as value type for zero memory overhead. +type Set[T comparable] map[T]struct{} -func (s GenericSet) Add(values ...interface{}) { +// NewSet creates a new empty Set. +func NewSet[T comparable]() Set[T] { + return make(Set[T]) +} + +// Add adds one or more elements to the set. +func (s Set[T]) Add(values ...T) { for _, v := range values { - s[v] = true + s[v] = struct{}{} } } +// Remove removes an element from the set. +func (s Set[T]) Remove(value T) { + delete(s, value) +} + +// Contains checks if an element is in the set. +func (s Set[T]) Contains(value T) bool { + _, ok := s[value] + return ok +} + +// Len returns the number of elements in the set. +func (s Set[T]) Len() int { + return len(s) +} + +// Values returns all elements as a slice. +func (s Set[T]) Values() []T { + result := make([]T, 0, len(s)) + for v := range s { + result = append(result, v) + } + return result +} + +// Clear removes all elements from the set. +func (s Set[T]) Clear() { + for k := range s { + delete(s, k) + } +} + +// emptyStructVal is a pre-created reflect.Value of struct{}{} to avoid repeated allocations +var emptyStructVal = reflect.ValueOf(struct{}{}) + type setSerializer struct { } @@ -255,9 +296,10 @@ func (s setSerializer) writeDifferentTypes(ctx *WriteContext, buf *ByteBuffer, k } // Read deserializes a set from the buffer into the provided reflect.Value -func (s setSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s setSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() + type_ := value.Type() // ReadData collection length from buffer length := int(buf.ReadVaruint32(err)) if length == 0 { @@ -311,14 +353,14 @@ func (s setSerializer) readSameType(ctx *ReadContext, buf *ByteBuffer, value ref if refID < int32(NotNullValueFlag) { // Use existing reference if available elem := ctx.RefResolver().GetReadObject(refID) - value.SetMapIndex(reflect.ValueOf(elem), reflect.ValueOf(true)) + value.SetMapIndex(reflect.ValueOf(elem), emptyStructVal) continue } } // Create new element and deserialize from buffer elem := reflect.New(typeInfo.Type).Elem() - serializer.ReadData(ctx, elem.Type(), elem) + serializer.ReadData(ctx, elem) if ctx.HasError() { return } @@ -327,7 +369,7 @@ func (s setSerializer) readSameType(ctx *ReadContext, buf *ByteBuffer, value ref ctx.RefResolver().SetReadObject(refID, elem) } // Add element to set - value.SetMapIndex(elem, reflect.ValueOf(true)) + value.SetMapIndex(elem, emptyStructVal) } } @@ -353,7 +395,7 @@ func (s setSerializer) readDifferentTypes(ctx *ReadContext, buf *ByteBuffer, val if refID < int32(NotNullValueFlag) { // Use existing reference if available elem := ctx.RefResolver().GetReadObject(refID) - value.SetMapIndex(elem, reflect.ValueOf(true)) + value.SetMapIndex(elem, emptyStructVal) continue } // Read type info (handles namespaced types, meta sharing, etc.) @@ -363,7 +405,7 @@ func (s setSerializer) readDifferentTypes(ctx *ReadContext, buf *ByteBuffer, val } // Create new element and deserialize from buffer elem := reflect.New(typeInfo.Type).Elem() - typeInfo.Serializer.ReadData(ctx, elem.Type(), elem) + typeInfo.Serializer.ReadData(ctx, elem) if ctx.HasError() { return } @@ -382,7 +424,7 @@ func (s setSerializer) readDifferentTypes(ctx *ReadContext, buf *ByteBuffer, val return } elem := reflect.New(typeInfo.Type).Elem() - typeInfo.Serializer.ReadData(ctx, elem.Type(), elem) + typeInfo.Serializer.ReadData(ctx, elem) if ctx.HasError() { return } @@ -394,7 +436,7 @@ func (s setSerializer) readDifferentTypes(ctx *ReadContext, buf *ByteBuffer, val return } elem := reflect.New(typeInfo.Type).Elem() - typeInfo.Serializer.ReadData(ctx, elem.Type(), elem) + typeInfo.Serializer.ReadData(ctx, elem) if ctx.HasError() { return } @@ -409,15 +451,15 @@ func setMapKey(mapValue, key reflect.Value, keyType reflect.Type) { if keyType.Kind() == reflect.Interface { // Check if key is directly assignable to the interface if key.Type().AssignableTo(keyType) { - mapValue.SetMapIndex(key, reflect.ValueOf(true)) + mapValue.SetMapIndex(key, emptyStructVal) } else { // Try pointer - common case where interface has pointer receivers ptr := reflect.New(key.Type()) ptr.Elem().Set(key) - mapValue.SetMapIndex(ptr, reflect.ValueOf(true)) + mapValue.SetMapIndex(ptr, emptyStructVal) } } else { - mapValue.SetMapIndex(key, reflect.ValueOf(true)) + mapValue.SetMapIndex(key, emptyStructVal) } } @@ -431,7 +473,7 @@ func (s setSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, ha return } if refID < int32(NotNullValueFlag) { - // Reference found + // Reference found or null obj := ctx.RefResolver().GetReadObject(refID) if obj.IsValid() { value.Set(obj) @@ -446,7 +488,7 @@ func (s setSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, ha ctx.TypeResolver().readTypeInfoWithTypeID(buf, typeID, ctxErr) } } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s setSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/slice.go b/go/fory/slice.go index 4b40374fd3..8c0dec2ba5 100644 --- a/go/fory/slice.go +++ b/go/fory/slice.go @@ -246,7 +246,7 @@ func (s *sliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s *sliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -254,11 +254,11 @@ func (s *sliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, ty s.Read(ctx, refMode, false, false, value) } -func (s *sliceSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s *sliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := int(buf.ReadVaruint32(ctxErr)) - isArrayType := type_.Kind() == reflect.Array + isArrayType := value.Type().Kind() == reflect.Array if length == 0 { if !isArrayType { @@ -282,7 +282,6 @@ func (s *sliceSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value r trackRefs := (collectFlag & CollectionTrackingRef) != 0 hasNull := (collectFlag & CollectionHasNull) != 0 - elemType := s.type_.Elem() // Handle slice vs array allocation if isArrayType { @@ -324,10 +323,10 @@ func (s *sliceSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value r continue } // refFlag should be NotNullValueFlag, now read the actual data - s.elemSerializer.ReadData(ctx, elemType, elem) + s.elemSerializer.ReadData(ctx, elem) } else { // No ref tracking and no nulls: directly read data - s.elemSerializer.ReadData(ctx, elemType, elem) + s.elemSerializer.ReadData(ctx, elem) } if ctx.HasError() { return diff --git a/go/fory/slice_dyn.go b/go/fory/slice_dyn.go index 69c339d857..8d5f250d13 100644 --- a/go/fory/slice_dyn.go +++ b/go/fory/slice_dyn.go @@ -251,10 +251,10 @@ func (s sliceDynSerializer) Read(ctx *ReadContext, refMode RefMode, readType boo if done || ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } -func (s sliceDynSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s sliceDynSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := int(buf.ReadVaruint32(ctxErr)) @@ -339,10 +339,10 @@ func (s sliceDynSerializer) readSameType(ctx *ReadContext, buf *ByteBuffer, valu // Register reference BEFORE reading data for circular ref support ctx.RefResolver().SetReadObject(refID, elem) // Read into the struct element - serializer.ReadData(ctx, elemType, elem.Elem()) + serializer.ReadData(ctx, elem.Elem()) } else { elem = reflect.New(elemType).Elem() - serializer.ReadData(ctx, elemType, elem) + serializer.ReadData(ctx, elem) ctx.RefResolver().Reference(elem) } value.Index(i).Set(elem) @@ -352,11 +352,11 @@ func (s sliceDynSerializer) readSameType(ctx *ReadContext, buf *ByteBuffer, valu continue } elem := reflect.New(elemType).Elem() - serializer.ReadData(ctx, elemType, elem) + serializer.ReadData(ctx, elem) value.Index(i).Set(elem) } else { elem := reflect.New(elemType).Elem() - serializer.ReadData(ctx, elemType, elem) + serializer.ReadData(ctx, elem) value.Index(i).Set(elem) } if ctx.HasError() { @@ -396,7 +396,7 @@ func (s sliceDynSerializer) readDifferentTypes( } elemType, serializer := s.wrapSerializerIfNeeded(typeInfo.Type, typeInfo.Serializer) elem := reflect.New(elemType).Elem() - serializer.ReadData(ctx, elemType, elem) + serializer.ReadData(ctx, elem) ctx.RefResolver().SetReadObject(refID, elem) value.Index(i).Set(elem) } else { @@ -412,7 +412,7 @@ func (s sliceDynSerializer) readDifferentTypes( } elemType, serializer := s.wrapSerializerIfNeeded(typeInfo.Type, typeInfo.Serializer) elem := reflect.New(elemType).Elem() - serializer.ReadData(ctx, elemType, elem) + serializer.ReadData(ctx, elem) value.Index(i).Set(elem) } if ctx.HasError() { diff --git a/go/fory/slice_primitive.go b/go/fory/slice_primitive.go index f288b43bc8..655cb3149b 100644 --- a/go/fory/slice_primitive.go +++ b/go/fory/slice_primitive.go @@ -57,14 +57,14 @@ func (s byteSliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType bo if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s byteSliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s byteSliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s byteSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := buf.ReadLength(ctxErr) @@ -118,14 +118,14 @@ func (s boolSliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType bo if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s boolSliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s boolSliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s boolSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]bool)(value.Addr().UnsafePointer()) = ReadBoolSlice(ctx.Buffer(), ctx.Err()) } @@ -152,14 +152,14 @@ func (s int8SliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType bo if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s int8SliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s int8SliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s int8SliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]int8)(value.Addr().UnsafePointer()) = ReadInt8Slice(ctx.Buffer(), ctx.Err()) } @@ -186,14 +186,14 @@ func (s int16SliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType b if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s int16SliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s int16SliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s int16SliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]int16)(value.Addr().UnsafePointer()) = ReadInt16Slice(ctx.Buffer(), ctx.Err()) } @@ -220,14 +220,14 @@ func (s int32SliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType b if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s int32SliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s int32SliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s int32SliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]int32)(value.Addr().UnsafePointer()) = ReadInt32Slice(ctx.Buffer(), ctx.Err()) } @@ -254,14 +254,14 @@ func (s int64SliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType b if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s int64SliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s int64SliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s int64SliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]int64)(value.Addr().UnsafePointer()) = ReadInt64Slice(ctx.Buffer(), ctx.Err()) } @@ -288,14 +288,14 @@ func (s float32SliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s float32SliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s float32SliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s float32SliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]float32)(value.Addr().UnsafePointer()) = ReadFloat32Slice(ctx.Buffer(), ctx.Err()) } @@ -322,14 +322,14 @@ func (s float64SliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s float64SliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s float64SliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s float64SliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]float64)(value.Addr().UnsafePointer()) = ReadFloat64Slice(ctx.Buffer(), ctx.Err()) } @@ -360,14 +360,14 @@ func (s intSliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType boo if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s intSliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s intSliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s intSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]int)(value.Addr().UnsafePointer()) = ReadIntSlice(ctx.Buffer(), ctx.Err()) } @@ -400,14 +400,14 @@ func (s uintSliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType bo if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s uintSliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s uintSliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s uintSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { *(*[]uint)(value.Addr().UnsafePointer()) = ReadUintSlice(ctx.Buffer(), ctx.Err()) } @@ -459,14 +459,14 @@ func (s stringSliceSerializer) Read(ctx *ReadContext, refMode RefMode, readType if done || ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s stringSliceSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { s.Read(ctx, refMode, false, false, value) } -func (s stringSliceSerializer) ReadData(ctx *ReadContext, _ reflect.Type, value reflect.Value) { +func (s stringSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := int(buf.ReadVaruint32(ctxErr)) diff --git a/go/fory/string.go b/go/fory/string.go index 001072d815..48b942cffa 100644 --- a/go/fory/string.go +++ b/go/fory/string.go @@ -122,7 +122,7 @@ func (s stringSerializer) Write(ctx *WriteContext, refMode RefMode, writeType bo s.WriteData(ctx, value) } -func (s stringSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s stringSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() str := readString(ctx.buffer, err) if ctx.HasError() { @@ -147,7 +147,7 @@ func (s stringSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s stringSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -189,10 +189,10 @@ func (s ptrToStringSerializer) Read(ctx *ReadContext, refMode RefMode, readType if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } -func (s ptrToStringSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s ptrToStringSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() str := readString(ctx.buffer, err) if ctx.HasError() { diff --git a/go/fory/struct.go b/go/fory/struct.go index e38f51a4ef..a73d363198 100644 --- a/go/fory/struct.go +++ b/go/fory/struct.go @@ -584,6 +584,10 @@ func (s *structSerializer) initFieldsFromTypeDef(typeResolver *TypeResolver) err shouldRead = true fieldType = localType } + } else if defTypeId == SET && isSetReflectType(localType) { + // Both remote and local are Set types, allow reading + shouldRead = true + fieldType = localType } else if !typeLookupFailed && typesCompatible(localType, remoteType) { shouldRead = true fieldType = localType @@ -602,6 +606,10 @@ func (s *structSerializer) initFieldsFromTypeDef(typeResolver *TypeResolver) err fieldSerializer = mustNewSliceDynSerializer(localType.Elem()) } } + // For Set fields (fory.Set[T] = map[T]struct{}), get the setSerializer + if defTypeId == SET && isSetReflectType(localType) && fieldSerializer == nil { + fieldSerializer, _ = typeResolver.getSerializerByType(localType, true) + } // If local type is *T and remote type is T, we need the serializer for *T // This handles Java's Integer/Long (nullable boxed types) mapping to Go's *int32/*int64 if localType.Kind() == reflect.Ptr && localType.Elem() == remoteType { @@ -829,8 +837,8 @@ func (s *structSerializer) computeHash() int32 { } else if field.Meta.Type.Kind() == reflect.Slice { typeId = LIST } else if field.Meta.Type.Kind() == reflect.Map { - // map[T]bool is used to represent a Set in Go - if field.Meta.Type.Elem().Kind() == reflect.Bool { + // fory.Set[T] is defined as map[T]struct{} - check for struct{} elem type + if isSetReflectType(field.Meta.Type) { typeId = SET } else { typeId = MAP @@ -1367,11 +1375,13 @@ func (s *structSerializer) writeRemainingField(ctx *WriteContext, ptr unsafe.Poi ctx.WriteStringFloat64Map(*(*map[string]float64)(fieldPtr), field.RefMode, false) return case StringBoolMapDispatchId: - // NOTE: map[string]bool is used to represent SETs in Go xlang mode. - // We CANNOT use the fast path here because it writes MAP format, - // but the data should be written in SET format. Fall through to slow path - // which uses setSerializer to correctly write the SET format. - break + // map[string]bool is a regular map in Go - use MAP format + // Note: fory.Set[T] uses struct{} values and has its own setSerializer + if field.RefMode == RefModeTracking { + break + } + ctx.WriteStringBoolMap(*(*map[string]bool)(fieldPtr), field.RefMode, false) + return case IntIntMapDispatchId: if field.RefMode == RefModeTracking { break @@ -1723,12 +1733,12 @@ func (s *structSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool typeInfo := ctx.TypeResolver().readTypeInfoWithTypeID(buf, typeID, ctxErr) // Use the serializer from TypeInfo which has the remote field definitions if structSer, ok := typeInfo.Serializer.(*structSerializer); ok && len(structSer.fieldDefs) > 0 { - structSer.ReadData(ctx, value.Type(), value) + structSer.ReadData(ctx, value) return } } } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s *structSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { @@ -1736,7 +1746,7 @@ func (s *structSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, t s.Read(ctx, refMode, false, false, value) } -func (s *structSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s *structSerializer) ReadData(ctx *ReadContext, value reflect.Value) { // Early error check - skip all intermediate checks for normal path performance if ctx.HasError() { return @@ -1753,10 +1763,9 @@ func (s *structSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value buf := ctx.Buffer() if value.Kind() == reflect.Ptr { if value.IsNil() { - value.Set(reflect.New(type_.Elem())) + value.Set(reflect.New(value.Type().Elem())) } value = value.Elem() - type_ = type_.Elem() } // In compatible mode with meta share, struct hash is not written @@ -2148,11 +2157,13 @@ func (s *structSerializer) readRemainingField(ctx *ReadContext, ptr unsafe.Point *(*map[string]float64)(fieldPtr) = ctx.ReadStringFloat64Map(field.RefMode, false) return case StringBoolMapDispatchId: - // NOTE: map[string]bool is used to represent SETs in Go xlang mode. - // We CANNOT use the fast path here because it reads MAP format, - // but the data is actually in SET format. Fall through to slow path - // which uses setSerializer to correctly read the SET format. - break + // map[string]bool is a regular map in Go - use MAP format + // Note: fory.Set[T] uses struct{} values and has its own setSerializer + if field.RefMode == RefModeTracking { + break + } + *(*map[string]bool)(fieldPtr) = ctx.ReadStringBoolMap(field.RefMode, false) + return case IntIntMapDispatchId: if field.RefMode == RefModeTracking { break @@ -2756,9 +2767,9 @@ func readEnumField(ctx *ReadContext, field *FieldInfo, fieldValue reflect.Value) // For pointer enum fields, the serializer is ptrToValueSerializer wrapping enumSerializer. // We need to call the inner enumSerializer directly with the dereferenced value. if ptrSer, ok := field.Serializer.(*ptrToValueSerializer); ok { - ptrSer.valueSerializer.ReadData(ctx, field.Meta.Type.Elem(), targetValue) + ptrSer.valueSerializer.ReadData(ctx, targetValue) } else { - field.Serializer.ReadData(ctx, field.Meta.Type, targetValue) + field.Serializer.ReadData(ctx, targetValue) } } @@ -2776,7 +2787,7 @@ func (s *skipStructSerializer) Write(ctx *WriteContext, refMode RefMode, writeTy ctx.SetError(SerializationError("skipStructSerializer does not support Write - unknown struct type")) } -func (s *skipStructSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s *skipStructSerializer) ReadData(ctx *ReadContext, value reflect.Value) { // Skip all fields based on fieldDefs from remote TypeDef for _, fieldDef := range s.fieldDefs { isStructType := isStructFieldType(fieldDef.fieldType) @@ -2811,7 +2822,7 @@ func (s *skipStructSerializer) Read(ctx *ReadContext, refMode RefMode, readType if ctx.HasError() { return } - s.ReadData(ctx, nil, value) + s.ReadData(ctx, value) } func (s *skipStructSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/struct_test.go b/go/fory/struct_test.go index 5c2fed3719..02253d0959 100644 --- a/go/fory/struct_test.go +++ b/go/fory/struct_test.go @@ -18,9 +18,20 @@ package fory import ( + "reflect" "testing" + + "github.com/stretchr/testify/require" ) +// Test struct for compatible mode tests (must be named struct at package level) +type SetFieldCompatibleTestStruct struct { + SetField Set[string] + NullableSet Set[string] `fory:"nullable"` + MapField map[string]bool + NullableMap map[string]bool `fory:"nullable"` +} + func TestUnsignedTypeSerialization(t *testing.T) { type TestStruct struct { U32Var uint32 `fory:"compress=true"` @@ -31,7 +42,7 @@ func TestUnsignedTypeSerialization(t *testing.T) { } f := New(WithXlang(true), WithCompatible(false)) - f.Register(TestStruct{}, 9999) + f.RegisterStruct(TestStruct{}, 9999) obj := TestStruct{ U32Var: 3000000000, @@ -69,3 +80,144 @@ func TestUnsignedTypeSerialization(t *testing.T) { t.Errorf("U64Tagged mismatch: expected %d, got %d", obj.U64Tagged, resultObj.U64Tagged) } } + +func TestSetField(t *testing.T) { + type TestStruct struct { + F1 Set[int32] + } + + f := New(WithXlang(true), WithCompatible(false)) + require.Nil(t, f.RegisterStruct(TestStruct{}, 1), "register struct error") +} + +func TestSetFieldSerializationSchemaConsistent(t *testing.T) { + f := New(WithXlang(true), WithCompatible(false)) + err := f.RegisterStruct(SetFieldCompatibleTestStruct{}, 1001) + require.NoError(t, err, "register struct error") + + // Create test object with Set and Map fields + obj := SetFieldCompatibleTestStruct{ + SetField: NewSet[string](), + NullableSet: NewSet[string](), + MapField: map[string]bool{"key1": true, "key2": true}, + NullableMap: map[string]bool{"nk1": true}, + } + obj.SetField.Add("x", "y") + obj.NullableSet.Add("m", "n") + + // Serialize + data, err := f.Serialize(obj) + require.NoError(t, err, "Serialize failed") + t.Logf("Serialized %d bytes", len(data)) + + // Deserialize + var result interface{} + err = f.Deserialize(data, &result) + require.NoError(t, err, "Deserialize failed") + + resultObj := result.(*SetFieldCompatibleTestStruct) + + // Verify SetField + require.Equal(t, 2, len(resultObj.SetField), "SetField length mismatch") + require.True(t, resultObj.SetField.Contains("x"), "SetField should contain 'x'") + require.True(t, resultObj.SetField.Contains("y"), "SetField should contain 'y'") + + // Verify NullableSet + require.Equal(t, 2, len(resultObj.NullableSet), "NullableSet length mismatch") + require.True(t, resultObj.NullableSet.Contains("m"), "NullableSet should contain 'm'") + require.True(t, resultObj.NullableSet.Contains("n"), "NullableSet should contain 'n'") + + // Verify MapField + require.Equal(t, 2, len(resultObj.MapField), "MapField length mismatch") + require.True(t, resultObj.MapField["key1"]) + require.True(t, resultObj.MapField["key2"]) + + // Verify NullableMap + require.Equal(t, 1, len(resultObj.NullableMap), "NullableMap length mismatch") + require.True(t, resultObj.NullableMap["nk1"]) +} + +func TestSetFieldSerializationCompatible(t *testing.T) { + f := New(WithXlang(true), WithCompatible(true)) + err := f.RegisterStruct(SetFieldCompatibleTestStruct{}, 1002) + require.NoError(t, err, "register struct error") + + // Create test object with Set and Map fields + obj := SetFieldCompatibleTestStruct{ + SetField: NewSet[string](), + NullableSet: NewSet[string](), + MapField: map[string]bool{"key1": true, "key2": true}, + NullableMap: map[string]bool{"nk1": true}, + } + obj.SetField.Add("x", "y") + obj.NullableSet.Add("m", "n") + + // Serialize + data, err := f.Serialize(obj) + require.NoError(t, err, "Serialize failed") + t.Logf("Serialized %d bytes", len(data)) + + // Deserialize + var result interface{} + err = f.Deserialize(data, &result) + require.NoError(t, err, "Deserialize failed") + + resultObj := result.(*SetFieldCompatibleTestStruct) + + // Verify SetField + require.Equal(t, 2, len(resultObj.SetField), "SetField length mismatch") + require.True(t, resultObj.SetField.Contains("x"), "SetField should contain 'x'") + require.True(t, resultObj.SetField.Contains("y"), "SetField should contain 'y'") + + // Verify NullableSet + require.Equal(t, 2, len(resultObj.NullableSet), "NullableSet length mismatch") + require.True(t, resultObj.NullableSet.Contains("m"), "NullableSet should contain 'm'") + require.True(t, resultObj.NullableSet.Contains("n"), "NullableSet should contain 'n'") + + // Verify MapField + require.Equal(t, 2, len(resultObj.MapField), "MapField length mismatch") + require.True(t, resultObj.MapField["key1"]) + require.True(t, resultObj.MapField["key2"]) + + // Verify NullableMap + require.Equal(t, 1, len(resultObj.NullableMap), "NullableMap length mismatch") + require.True(t, resultObj.NullableMap["nk1"]) +} + +func TestSetFieldTypeId(t *testing.T) { + // Test that Set fields have the correct TypeId in fingerprint + type TestStruct struct { + SetField Set[string] + MapField map[string]bool + } + + f := New(WithXlang(true), WithCompatible(false)) + err := f.RegisterStruct(TestStruct{}, 1003) + require.NoError(t, err, "register struct error") + + // Get the struct serializer to inspect the fields + typeInfo, err := f.typeResolver.getTypeInfo(reflect.ValueOf(TestStruct{}), false) + require.NoError(t, err, "getTypeInfo failed") + require.NotNil(t, typeInfo, "typeInfo is nil") + + structSer, ok := typeInfo.Serializer.(*structSerializer) + require.True(t, ok, "serializer is not structSerializer") + + // Check each field + for _, field := range structSer.fields { + t.Logf("Field: %s, Type: %v, TypeId: %d, Serializer: %T", + field.Meta.Name, field.Meta.Type, field.Meta.TypeId, field.Serializer) + + if field.Meta.Name == "set_field" { + require.Equal(t, SET, field.Meta.TypeId, "SetField should have TypeId=SET(21)") + require.NotNil(t, field.Serializer, "SetField serializer should not be nil") + _, isSetSerializer := field.Serializer.(setSerializer) + require.True(t, isSetSerializer, "SetField serializer should be setSerializer") + } + + if field.Meta.Name == "map_field" { + require.Equal(t, MAP, field.Meta.TypeId, "MapField should have TypeId=MAP(22)") + require.NotNil(t, field.Serializer, "MapField serializer should not be nil") + } + } +} diff --git a/go/fory/tag_test.go b/go/fory/tag_test.go index 83c3670ea0..61cab90e89 100644 --- a/go/fory/tag_test.go +++ b/go/fory/tag_test.go @@ -244,8 +244,8 @@ type PersonWithoutTags struct { } func TestSerializationWithTags(t *testing.T) { - fory := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory.RegisterByName(PersonWithTags{}, "test.PersonWithTags") + fory := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory.RegisterNamedStruct(PersonWithTags{}, "test.PersonWithTags") require.NoError(t, err) person := PersonWithTags{ @@ -271,8 +271,8 @@ func TestSerializationWithTags(t *testing.T) { } func TestSerializationWithoutTags(t *testing.T) { - fory := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory.RegisterByName(PersonWithoutTags{}, "test.PersonWithoutTags") + fory := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory.RegisterNamedStruct(PersonWithoutTags{}, "test.PersonWithoutTags") require.NoError(t, err) person := PersonWithoutTags{ @@ -296,12 +296,12 @@ func TestSerializationWithoutTags(t *testing.T) { } func TestTagIDReducesPayloadSize(t *testing.T) { - fory1 := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory1.RegisterByName(PersonWithTags{}, "test.PersonWithTags") + fory1 := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory1.RegisterNamedStruct(PersonWithTags{}, "test.PersonWithTags") require.NoError(t, err) - fory2 := NewFory(WithRefTracking(false), WithCompatible(true)) - err = fory2.RegisterByName(PersonWithoutTags{}, "test.PersonWithoutTags") + fory2 := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err = fory2.RegisterNamedStruct(PersonWithoutTags{}, "test.PersonWithoutTags") require.NoError(t, err) // Create comparable data @@ -348,12 +348,12 @@ type NumericStructWithoutTags struct { } func TestNumericStructTagIDReducesSize(t *testing.T) { - fory1 := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory1.RegisterByName(NumericStructWithTags{}, "test.NumericStructWithTags") + fory1 := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory1.RegisterNamedStruct(NumericStructWithTags{}, "test.NumericStructWithTags") require.NoError(t, err) - fory2 := NewFory(WithRefTracking(false), WithCompatible(true)) - err = fory2.RegisterByName(NumericStructWithoutTags{}, "test.NumericStructWithoutTags") + fory2 := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err = fory2.RegisterNamedStruct(NumericStructWithoutTags{}, "test.NumericStructWithoutTags") require.NoError(t, err) // Create small int32 values @@ -427,12 +427,12 @@ func TestNullableRefFlagsRespected(t *testing.T) { field.Name, tag.ID, tag.Nullable, tag.NullableSet, tag.Ref, tag.RefSet) } - fory1 := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory1.RegisterByName(TestStructNoNull{}, "test.TestStructNoNull") + fory1 := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory1.RegisterNamedStruct(TestStructNoNull{}, "test.TestStructNoNull") require.NoError(t, err) - fory2 := NewFory(WithRefTracking(false), WithCompatible(true)) - err = fory2.RegisterByName(TestStructDefalt{}, "test.TestStructDefalt") + fory2 := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err = fory2.RegisterNamedStruct(TestStructDefalt{}, "test.TestStructDefalt") require.NoError(t, err) v1, v2, v3, v4, v5 := int32(1), int32(2), int32(3), int32(4), int32(5) @@ -465,12 +465,12 @@ func TestNullableRefFlagsRespected(t *testing.T) { } func TestTypeDefEncodingSizeWithTagIDs(t *testing.T) { - fory1 := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory1.RegisterByName(NumericStructWithTags{}, "test.NumericStructWithTags") + fory1 := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory1.RegisterNamedStruct(NumericStructWithTags{}, "test.NumericStructWithTags") require.NoError(t, err) - fory2 := NewFory(WithRefTracking(false), WithCompatible(true)) - err = fory2.RegisterByName(NumericStructWithoutTags{}, "test.NumericStructWithoutTags") + fory2 := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err = fory2.RegisterNamedStruct(NumericStructWithoutTags{}, "test.NumericStructWithoutTags") require.NoError(t, err) // Build TypeDef for struct with tags @@ -511,8 +511,8 @@ type StructWithLargeTagIDs struct { } func TestLargeTagIDs(t *testing.T) { - fory := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory.RegisterByName(StructWithLargeTagIDs{}, "test.StructWithLargeTagIDs") + fory := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory.RegisterNamedStruct(StructWithLargeTagIDs{}, "test.StructWithLargeTagIDs") require.NoError(t, err) obj := StructWithLargeTagIDs{ @@ -545,8 +545,8 @@ type MixedTagStruct struct { } func TestMixedTagFields(t *testing.T) { - fory := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory.RegisterByName(MixedTagStruct{}, "test.MixedTagStruct") + fory := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory.RegisterNamedStruct(MixedTagStruct{}, "test.MixedTagStruct") require.NoError(t, err) obj := MixedTagStruct{ @@ -582,10 +582,10 @@ type OuterWithTags struct { } func TestNestedStructWithTags(t *testing.T) { - fory := NewFory(WithRefTracking(false), WithCompatible(true)) - err := fory.RegisterByName(InnerWithTags{}, "test.InnerWithTags") + fory := NewFory(WithXlang(true), WithRefTracking(false), WithCompatible(true)) + err := fory.RegisterNamedStruct(InnerWithTags{}, "test.InnerWithTags") require.NoError(t, err) - err = fory.RegisterByName(OuterWithTags{}, "test.OuterWithTags") + err = fory.RegisterNamedStruct(OuterWithTags{}, "test.OuterWithTags") require.NoError(t, err) obj := OuterWithTags{ diff --git a/go/fory/tests/generator_xlang_test.go b/go/fory/tests/generator_xlang_test.go index 94444ab7ea..9adf3e6447 100644 --- a/go/fory/tests/generator_xlang_test.go +++ b/go/fory/tests/generator_xlang_test.go @@ -66,7 +66,7 @@ func TestValidationDemoXlang(t *testing.T) { // Reflect mode (register with full name) foryForReflect := forygo.NewFory(forygo.WithRefTracking(true)) - err := foryForReflect.RegisterByName(ReflectStruct{}, expectedTypeTag) + err := foryForReflect.RegisterNamedStruct(ReflectStruct{}, expectedTypeTag) require.NoError(t, err, "Should be able to register ReflectStruct with full name") // Serialization test @@ -131,7 +131,7 @@ func TestSliceDemoXlang(t *testing.T) { // Reflect mode - enable reference tracking foryForReflect := forygo.NewFory(forygo.WithRefTracking(true)) - err := foryForReflect.RegisterByName(ReflectSliceStruct{}, expectedTypeTag) + err := foryForReflect.RegisterNamedStruct(ReflectSliceStruct{}, expectedTypeTag) require.NoError(t, err, "Should be able to register ReflectSliceStruct with full name") // Serialization test @@ -205,7 +205,7 @@ func TestDynamicSliceDemoXlang(t *testing.T) { // Reflect mode - enable reference tracking foryForReflect := forygo.NewFory(forygo.WithRefTracking(true)) - err := foryForReflect.RegisterByName(ReflectDynamicStruct{}, expectedTypeTag) + err := foryForReflect.RegisterNamedStruct(ReflectDynamicStruct{}, expectedTypeTag) require.NoError(t, err, "Should be able to register ReflectDynamicStruct with full name") // Serialization test diff --git a/go/fory/tests/structs_fory_gen.go b/go/fory/tests/structs_fory_gen.go index 8a85508e1a..154623c655 100644 --- a/go/fory/tests/structs_fory_gen.go +++ b/go/fory/tests/structs_fory_gen.go @@ -157,7 +157,7 @@ func (g *DynamicSliceDemo_ForyGenSerializer) Read(ctx *fory.ReadContext, refMode if readType { ctx.TypeResolver().ReadTypeInfo(ctx.Buffer(), err) } - g.ReadData(ctx, value.Type(), value) + g.ReadData(ctx, value) } // ReadTyped provides strongly-typed deserialization with no reflection overhead @@ -224,14 +224,14 @@ func (g *DynamicSliceDemo_ForyGenSerializer) ReadTyped(ctx *fory.ReadContext, v } // ReadData provides reflect.Value interface compatibility (implements fory.Serializer) -func (g *DynamicSliceDemo_ForyGenSerializer) ReadData(ctx *fory.ReadContext, type_ reflect.Type, value reflect.Value) { +func (g *DynamicSliceDemo_ForyGenSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { g.initHash(ctx.TypeResolver()) // Convert reflect.Value to concrete type and delegate to typed method var v *DynamicSliceDemo if value.Kind() == reflect.Ptr { if value.IsNil() { // For pointer types, allocate using type_.Elem() - value.Set(reflect.New(type_.Elem())) + value.Set(reflect.New(value.Type().Elem())) } v = value.Interface().(*DynamicSliceDemo) } else { @@ -611,7 +611,7 @@ func (g *MapDemo_ForyGenSerializer) Read(ctx *fory.ReadContext, refMode fory.Ref if readType { ctx.TypeResolver().ReadTypeInfo(ctx.Buffer(), err) } - g.ReadData(ctx, value.Type(), value) + g.ReadData(ctx, value) } // ReadTyped provides strongly-typed deserialization with no reflection overhead @@ -847,14 +847,14 @@ func (g *MapDemo_ForyGenSerializer) ReadTyped(ctx *fory.ReadContext, v *MapDemo) } // ReadData provides reflect.Value interface compatibility (implements fory.Serializer) -func (g *MapDemo_ForyGenSerializer) ReadData(ctx *fory.ReadContext, type_ reflect.Type, value reflect.Value) { +func (g *MapDemo_ForyGenSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { g.initHash(ctx.TypeResolver()) // Convert reflect.Value to concrete type and delegate to typed method var v *MapDemo if value.Kind() == reflect.Ptr { if value.IsNil() { // For pointer types, allocate using type_.Elem() - value.Set(reflect.New(type_.Elem())) + value.Set(reflect.New(value.Type().Elem())) } v = value.Interface().(*MapDemo) } else { @@ -1068,7 +1068,7 @@ func (g *SliceDemo_ForyGenSerializer) Read(ctx *fory.ReadContext, refMode fory.R if readType { ctx.TypeResolver().ReadTypeInfo(ctx.Buffer(), err) } - g.ReadData(ctx, value.Type(), value) + g.ReadData(ctx, value) } // ReadTyped provides strongly-typed deserialization with no reflection overhead @@ -1220,14 +1220,14 @@ func (g *SliceDemo_ForyGenSerializer) ReadTyped(ctx *fory.ReadContext, v *SliceD } // ReadData provides reflect.Value interface compatibility (implements fory.Serializer) -func (g *SliceDemo_ForyGenSerializer) ReadData(ctx *fory.ReadContext, type_ reflect.Type, value reflect.Value) { +func (g *SliceDemo_ForyGenSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { g.initHash(ctx.TypeResolver()) // Convert reflect.Value to concrete type and delegate to typed method var v *SliceDemo if value.Kind() == reflect.Ptr { if value.IsNil() { // For pointer types, allocate using type_.Elem() - value.Set(reflect.New(type_.Elem())) + value.Set(reflect.New(value.Type().Elem())) } v = value.Interface().(*SliceDemo) } else { @@ -1356,7 +1356,7 @@ func (g *ValidationDemo_ForyGenSerializer) Read(ctx *fory.ReadContext, refMode f if readType { ctx.TypeResolver().ReadTypeInfo(ctx.Buffer(), err) } - g.ReadData(ctx, value.Type(), value) + g.ReadData(ctx, value) } // ReadTyped provides strongly-typed deserialization with no reflection overhead @@ -1392,14 +1392,14 @@ func (g *ValidationDemo_ForyGenSerializer) ReadTyped(ctx *fory.ReadContext, v *V } // ReadData provides reflect.Value interface compatibility (implements fory.Serializer) -func (g *ValidationDemo_ForyGenSerializer) ReadData(ctx *fory.ReadContext, type_ reflect.Type, value reflect.Value) { +func (g *ValidationDemo_ForyGenSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { g.initHash(ctx.TypeResolver()) // Convert reflect.Value to concrete type and delegate to typed method var v *ValidationDemo if value.Kind() == reflect.Ptr { if value.IsNil() { // For pointer types, allocate using type_.Elem() - value.Set(reflect.New(type_.Elem())) + value.Set(reflect.New(value.Type().Elem())) } v = value.Interface().(*ValidationDemo) } else { diff --git a/go/fory/tests/xlang/xlang_test_main.go b/go/fory/tests/xlang/xlang_test_main.go index 1b95295e0a..6cd1650df6 100644 --- a/go/fory/tests/xlang/xlang_test_main.go +++ b/go/fory/tests/xlang/xlang_test_main.go @@ -21,6 +21,7 @@ import ( "flag" "fmt" "os" + "reflect" "runtime" "github.com/apache/fory/go/fory" @@ -348,7 +349,7 @@ type NullableComprehensiveSchemaConsistent struct { // Base non-nullable reference fields StringField string ListField []string - SetField map[string]bool + SetField fory.Set[string] MapField map[string]string // Nullable fields - first half (boxed types in Java) @@ -361,7 +362,7 @@ type NullableComprehensiveSchemaConsistent struct { NullableBool *bool `fory:"nullable"` NullableString *string `fory:"nullable"` NullableList []string `fory:"nullable"` - NullableSet map[string]bool `fory:"nullable"` + NullableSet fory.Set[string] `fory:"nullable"` NullableMap map[string]string `fory:"nullable"` } @@ -391,7 +392,7 @@ type NullableComprehensiveCompatible struct { // Reference fields - also nullable in Go StringField *string `fory:"nullable"` ListField []string `fory:"nullable"` - SetField map[string]bool `fory:"nullable"` + SetField fory.Set[string] `fory:"nullable"` MapField map[string]string `fory:"nullable"` // Group 2: Non-nullable in Go, Nullable in Java (@ForyField(nullable=true)) @@ -405,7 +406,7 @@ type NullableComprehensiveCompatible struct { // Reference types NullableString2 string NullableList2 []string - NullableSet2 map[string]bool + NullableSet2 fory.Set[string] NullableMap2 map[string]string } @@ -415,21 +416,16 @@ type NullableComprehensiveCompatible struct { type MyExtSerializer struct{} -func (s *MyExtSerializer) Write(buf *fory.ByteBuffer, value interface{}) error { - myExt := value.(MyExt) +func (s *MyExtSerializer) WriteData(ctx *fory.WriteContext, value reflect.Value) { + myExt := value.Interface().(MyExt) // WriteVarint32 uses zigzag encoding (compatible with Java's writeVarint32) - buf.WriteVarint32(myExt.Id) - return nil + ctx.Buffer().WriteVarint32(myExt.Id) } -func (s *MyExtSerializer) Read(buf *fory.ByteBuffer) (interface{}, error) { - var bufErr fory.Error +func (s *MyExtSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { // ReadVarint32 uses zigzag decoding (compatible with Java's readVarint32) - id := buf.ReadVarint32(&bufErr) - if bufErr.HasError() { - return nil, bufErr.CheckError() - } - return MyExt{Id: id}, nil + id := ctx.Buffer().ReadVarint32(ctx.Err()) + value.Set(reflect.ValueOf(MyExt{Id: id})) } // ============================================================================ @@ -667,8 +663,8 @@ func testSimpleStruct() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric IDs to match Java's fory.register(Color.class, 101), etc. f.RegisterEnum(Color(0), 101) - f.Register(Item{}, 102) - f.Register(SimpleStruct{}, 103) + f.RegisterStruct(Item{}, 102) + f.RegisterStruct(SimpleStruct{}, 103) var obj SimpleStruct if err := f.Deserialize(data, &obj); err != nil { @@ -688,9 +684,9 @@ func testNamedSimpleStruct() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use namespace "demo" to match Java's fory.register(Color.class, "demo", "color"), etc. - f.RegisterEnumByName(Color(0), "demo.color") - f.RegisterByName(Item{}, "demo.item") - f.RegisterByName(SimpleStruct{}, "demo.simple_struct") + f.RegisterNamedEnum(Color(0), "demo.color") + f.RegisterNamedStruct(Item{}, "demo.item") + f.RegisterNamedStruct(SimpleStruct{}, "demo.simple_struct") var obj SimpleStruct if err := f.Deserialize(data, &obj); err != nil { @@ -710,7 +706,7 @@ func testList() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric ID 102 to match Java's fory.register(Item.class, 102) - f.Register(Item{}, 102) + f.RegisterStruct(Item{}, 102) buf := fory.NewByteBuffer(data) lists := make([]interface{}, 4) @@ -742,7 +738,7 @@ func testMap() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric ID 102 to match Java's fory.register(Item.class, 102) - f.Register(Item{}, 102) + f.RegisterStruct(Item{}, 102) buf := fory.NewByteBuffer(data) maps := make([]interface{}, 2) @@ -774,7 +770,7 @@ func testInteger() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric ID 101 to match Java's fory.register(Item1.class, 101) - f.Register(Item1{}, 101) + f.RegisterStruct(Item1{}, 101) buf := fory.NewByteBuffer(data) @@ -847,7 +843,7 @@ func testItem() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric ID 102 to match Java's fory.register(Item.class, 102) - f.Register(Item{}, 102) + f.RegisterStruct(Item{}, 102) buf := fory.NewByteBuffer(data) items := make([]interface{}, 3) @@ -911,7 +907,7 @@ func testStructWithList() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric ID 201 to match Java's fory.register(StructWithList.class, 201) - f.Register(StructWithList{}, 201) + f.RegisterStruct(StructWithList{}, 201) // Java serializes two objects to the same buffer, so we need to deserialize twice readBuf := fory.NewByteBuffer(data) @@ -948,7 +944,7 @@ func testStructWithMap() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric ID 202 to match Java's fory.register(StructWithMap.class, 202) - f.Register(StructWithMap{}, 202) + f.RegisterStruct(StructWithMap{}, 202) // Java serializes two objects to the same buffer, so we need to deserialize twice readBuf := fory.NewByteBuffer(data) @@ -987,8 +983,8 @@ func testSkipIdCustom() { // Use numeric IDs to match Java's registration: // fory2.register(MyExt.class, 103) // fory2.register(EmptyWrapper.class, 104) - f.RegisterExtensionType(MyExt{}, 103, &MyExtSerializer{}) - f.Register(EmptyWrapper{}, 104) + f.RegisterExtension(MyExt{}, 103, &MyExtSerializer{}) + f.RegisterStruct(EmptyWrapper{}, 104) var obj EmptyWrapper if err := f.Deserialize(data, &obj); err != nil { @@ -1008,8 +1004,8 @@ func testSkipNameCustom() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) - f.RegisterExtensionTypeByName(MyExt{}, "my_ext", &MyExtSerializer{}) - f.RegisterByName(EmptyWrapper{}, "my_wrapper") + f.RegisterNamedExtension(MyExt{}, "my_ext", &MyExtSerializer{}) + f.RegisterNamedStruct(EmptyWrapper{}, "my_wrapper") var obj EmptyWrapper if err := f.Deserialize(data, &obj); err != nil { @@ -1031,10 +1027,10 @@ func testConsistentNamed() { // Java uses SCHEMA_CONSISTENT mode which doesn't enable metaShare // So Go should NOT expect meta offset field f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) - f.RegisterEnumByName(Color(0), "color") - f.RegisterByName(MyStruct{}, "my_struct") + f.RegisterNamedEnum(Color(0), "color") + f.RegisterNamedStruct(MyStruct{}, "my_struct") // MyExt uses an extension serializer in Java (MyExtSerializer), so register as extension type - f.RegisterExtensionTypeByName(MyExt{}, "my_ext", &MyExtSerializer{}) + f.RegisterNamedExtension(MyExt{}, "my_ext", &MyExtSerializer{}) buf := fory.NewByteBuffer(data) values := make([]interface{}, 9) @@ -1068,7 +1064,7 @@ func testStructVersionCheck() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) // Use numeric ID 201 to match Java's fory.register(VersionCheckStruct.class, 201) - f.Register(VersionCheckStruct{}, 201) + f.RegisterStruct(VersionCheckStruct{}, 201) var obj VersionCheckStruct if err := f.Deserialize(data, &obj); err != nil { @@ -1089,9 +1085,9 @@ func testPolymorphicList() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric IDs to match Java's registration: Dog=302, Cat=303, AnimalListHolder=304 - f.Register(&Dog{}, 302) - f.Register(&Cat{}, 303) - f.Register(AnimalListHolder{}, 304) + f.RegisterStruct(&Dog{}, 302) + f.RegisterStruct(&Cat{}, 303) + f.RegisterStruct(AnimalListHolder{}, 304) buf := fory.NewByteBuffer(data) values := make([]interface{}, 2) @@ -1124,9 +1120,9 @@ func testPolymorphicMap() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Use numeric IDs to match Java's registration: Dog=302, Cat=303, AnimalMapHolder=305 - f.Register(&Dog{}, 302) - f.Register(&Cat{}, 303) - f.Register(AnimalMapHolder{}, 305) + f.RegisterStruct(&Dog{}, 302) + f.RegisterStruct(&Cat{}, 303) + f.RegisterStruct(AnimalMapHolder{}, 305) buf := fory.NewByteBuffer(data) values := make([]interface{}, 2) @@ -1165,7 +1161,7 @@ func testOneFieldStructCompatible() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) // Register with numeric ID 200 to match Java's fory.register(OneFieldStruct.class, 200) - f.Register(OneFieldStruct{}, 200) + f.RegisterStruct(OneFieldStruct{}, 200) // Parse header and meta offset manually for debugging if len(data) >= 8 { @@ -1219,7 +1215,7 @@ func testOneFieldStructSchema() { fmt.Println() f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) - f.Register(OneFieldStruct{}, 200) + f.RegisterStruct(OneFieldStruct{}, 200) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1251,7 +1247,7 @@ func testOneStringFieldSchemaConsistent() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) - f.Register(OneStringFieldStruct{}, 200) + f.RegisterStruct(OneStringFieldStruct{}, 200) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1278,7 +1274,7 @@ func testOneStringFieldCompatible() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) - f.Register(OneStringFieldStruct{}, 200) + f.RegisterStruct(OneStringFieldStruct{}, 200) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1305,7 +1301,7 @@ func testTwoStringFieldCompatible() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) - f.Register(TwoStringFieldStruct{}, 201) + f.RegisterStruct(TwoStringFieldStruct{}, 201) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1332,7 +1328,7 @@ func testSchemaEvolutionCompatible() { // Read TwoStringFieldStruct data, deserialize as EmptyStruct f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) - f.Register(EmptyStruct{}, 200) + f.RegisterStruct(EmptyStruct{}, 200) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1357,7 +1353,7 @@ func testSchemaEvolutionCompatibleReverse() { // Read OneStringFieldStruct data, deserialize as TwoStringFieldStruct // Missing f2 field will be Go's zero value (empty string) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) - f.Register(TwoStringFieldStruct{}, 200) + f.RegisterStruct(TwoStringFieldStruct{}, 200) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1387,7 +1383,7 @@ func testOneEnumFieldSchemaConsistent() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) f.RegisterEnum(TestEnum(0), 210) - f.Register(OneEnumFieldStruct{}, 211) + f.RegisterStruct(OneEnumFieldStruct{}, 211) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1415,7 +1411,7 @@ func testOneEnumFieldCompatible() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) f.RegisterEnum(TestEnum(0), 210) - f.Register(OneEnumFieldStruct{}, 211) + f.RegisterStruct(OneEnumFieldStruct{}, 211) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1443,7 +1439,7 @@ func testTwoEnumFieldCompatible() { f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) f.RegisterEnum(TestEnum(0), 210) - f.Register(TwoEnumFieldStruct{}, 212) + f.RegisterStruct(TwoEnumFieldStruct{}, 212) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1475,7 +1471,7 @@ func testEnumSchemaEvolutionCompatible() { // Read TwoEnumFieldStruct data, deserialize as EmptyStruct f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) f.RegisterEnum(TestEnum(0), 210) - f.Register(EmptyStruct{}, 211) + f.RegisterStruct(EmptyStruct{}, 211) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1501,7 +1497,7 @@ func testEnumSchemaEvolutionCompatibleReverse() { // Missing f2 field will be Go's zero value (nil for pointer) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) f.RegisterEnum(TestEnum(0), 210) - f.Register(TwoEnumFieldStruct{}, 211) + f.RegisterStruct(TwoEnumFieldStruct{}, 211) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1537,7 +1533,7 @@ func testNullableFieldSchemaConsistentNotNull() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) - f.Register(NullableComprehensiveSchemaConsistent{}, 401) + f.RegisterStruct(NullableComprehensiveSchemaConsistent{}, 401) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1562,7 +1558,7 @@ func testNullableFieldSchemaConsistentNotNull() { if len(result.ListField) != 3 || result.ListField[0] != "a" || result.ListField[1] != "b" || result.ListField[2] != "c" { panic(fmt.Sprintf("ListField mismatch: expected [a, b, c], got %v", result.ListField)) } - if len(result.SetField) != 2 || !result.SetField["x"] || !result.SetField["y"] { + if len(result.SetField) != 2 || !result.SetField.Contains("x") || !result.SetField.Contains("y") { panic(fmt.Sprintf("SetField mismatch: expected {x, y}, got %v", result.SetField)) } if len(result.MapField) != 2 || result.MapField["key1"] != "value1" || result.MapField["key2"] != "value2" { @@ -1595,7 +1591,7 @@ func testNullableFieldSchemaConsistentNotNull() { if len(result.NullableList) != 2 || result.NullableList[0] != "p" || result.NullableList[1] != "q" { panic(fmt.Sprintf("NullableList mismatch: expected [p, q], got %v", result.NullableList)) } - if len(result.NullableSet) != 2 || !result.NullableSet["m"] || !result.NullableSet["n"] { + if len(result.NullableSet) != 2 || !result.NullableSet.Contains("m") || !result.NullableSet.Contains("n") { panic(fmt.Sprintf("NullableSet mismatch: expected {m, n}, got %v", result.NullableSet)) } if len(result.NullableMap) != 1 || result.NullableMap["nk1"] != "nv1" { @@ -1615,7 +1611,7 @@ func testNullableFieldSchemaConsistentNull() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) - f.Register(NullableComprehensiveSchemaConsistent{}, 401) + f.RegisterStruct(NullableComprehensiveSchemaConsistent{}, 401) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1640,7 +1636,7 @@ func testNullableFieldSchemaConsistentNull() { if len(result.ListField) != 3 || result.ListField[0] != "a" || result.ListField[1] != "b" || result.ListField[2] != "c" { panic(fmt.Sprintf("ListField mismatch: expected [a, b, c], got %v", result.ListField)) } - if len(result.SetField) != 2 || !result.SetField["x"] || !result.SetField["y"] { + if len(result.SetField) != 2 || !result.SetField.Contains("x") || !result.SetField.Contains("y") { panic(fmt.Sprintf("SetField mismatch: expected {x, y}, got %v", result.SetField)) } if len(result.MapField) != 2 || result.MapField["key1"] != "value1" || result.MapField["key2"] != "value2" { @@ -1694,7 +1690,7 @@ func testNullableFieldCompatibleNotNull() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) - f.Register(NullableComprehensiveCompatible{}, 402) + f.RegisterStruct(NullableComprehensiveCompatible{}, 402) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1756,7 +1752,7 @@ func testNullableFieldCompatibleNotNull() { if len(result.ListField) != 3 || result.ListField[0] != "a" || result.ListField[1] != "b" || result.ListField[2] != "c" { panic(fmt.Sprintf("ListField mismatch: expected [a, b, c], got %v", result.ListField)) } - if len(result.SetField) != 2 || !result.SetField["x"] || !result.SetField["y"] { + if len(result.SetField) != 2 || !result.SetField.Contains("x") || !result.SetField.Contains("y") { panic(fmt.Sprintf("SetField mismatch: expected {x, y}, got %v", result.SetField)) } if len(result.MapField) != 2 || result.MapField["key1"] != "value1" || result.MapField["key2"] != "value2" { @@ -1774,7 +1770,7 @@ func testNullableFieldCompatibleNotNull() { if len(result.NullableList2) != 2 || result.NullableList2[0] != "p" || result.NullableList2[1] != "q" { panic(fmt.Sprintf("NullableList2 mismatch: expected [p, q], got %v", result.NullableList2)) } - if len(result.NullableSet2) != 2 || !result.NullableSet2["m"] || !result.NullableSet2["n"] { + if len(result.NullableSet2) != 2 || !result.NullableSet2.Contains("m") || !result.NullableSet2.Contains("n") { panic(fmt.Sprintf("NullableSet2 mismatch: expected {m, n}, got %v", result.NullableSet2)) } if len(result.NullableMap2) != 1 || result.NullableMap2["nk1"] != "nv1" { @@ -1800,7 +1796,7 @@ func testNullableFieldCompatibleNull() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) - f.Register(NullableComprehensiveCompatible{}, 402) + f.RegisterStruct(NullableComprehensiveCompatible{}, 402) buf := fory.NewByteBuffer(data) var obj interface{} @@ -1862,7 +1858,7 @@ func testNullableFieldCompatibleNull() { if len(result.ListField) != 3 || result.ListField[0] != "a" || result.ListField[1] != "b" || result.ListField[2] != "c" { panic(fmt.Sprintf("ListField mismatch: expected [a, b, c], got %v", result.ListField)) } - if len(result.SetField) != 2 || !result.SetField["x"] || !result.SetField["y"] { + if len(result.SetField) != 2 || !result.SetField.Contains("x") || !result.SetField.Contains("y") { panic(fmt.Sprintf("SetField mismatch: expected {x, y}, got %v", result.SetField)) } if len(result.MapField) != 2 || result.MapField["key1"] != "value1" || result.MapField["key2"] != "value2" { @@ -1984,8 +1980,8 @@ func testRefSchemaConsistent() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(false), fory.WithRefTracking(true)) - f.Register(RefInnerSchemaConsistent{}, 501) - f.Register(RefOuterSchemaConsistent{}, 502) + f.RegisterStruct(RefInnerSchemaConsistent{}, 501) + f.RegisterStruct(RefOuterSchemaConsistent{}, 502) buf := fory.NewByteBuffer(data) var obj interface{} @@ -2032,8 +2028,8 @@ func testRefCompatible() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true), fory.WithRefTracking(true)) - f.Register(RefInnerCompatible{}, 503) - f.Register(RefOuterCompatible{}, 504) + f.RegisterStruct(RefInnerCompatible{}, 503) + f.RegisterStruct(RefOuterCompatible{}, 504) buf := fory.NewByteBuffer(data) var obj interface{} @@ -2084,7 +2080,7 @@ func testCircularRefSchemaConsistent() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(false), fory.WithRefTracking(true)) - f.Register(CircularRefStruct{}, 601) + f.RegisterStruct(CircularRefStruct{}, 601) buf := fory.NewByteBuffer(data) var obj interface{} @@ -2121,7 +2117,7 @@ func testCircularRefCompatible() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true), fory.WithRefTracking(true)) - f.Register(CircularRefStruct{}, 602) + f.RegisterStruct(CircularRefStruct{}, 602) buf := fory.NewByteBuffer(data) var obj interface{} @@ -2222,7 +2218,7 @@ func testUnsignedSchemaConsistentSimple() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) - f.Register(UnsignedSchemaConsistentSimple{}, 1) + f.RegisterStruct(UnsignedSchemaConsistentSimple{}, 1) var obj interface{} err := f.Deserialize(data, &obj) @@ -2254,7 +2250,7 @@ func testUnsignedSchemaConsistent() { fmt.Printf("Input hex: %x\n", data) f := fory.New(fory.WithXlang(true), fory.WithCompatible(false)) - f.Register(UnsignedSchemaConsistent{}, 501) + f.RegisterStruct(UnsignedSchemaConsistent{}, 501) var obj interface{} err := f.Deserialize(data, &obj) @@ -2312,7 +2308,7 @@ func testUnsignedSchemaCompatible() { data := readFile(dataFile) f := fory.New(fory.WithXlang(true), fory.WithCompatible(true)) - f.Register(UnsignedSchemaCompatible{}, 502) + f.RegisterStruct(UnsignedSchemaCompatible{}, 502) var obj interface{} err := f.Deserialize(data, &obj) diff --git a/go/fory/threadsafe/fory.go b/go/fory/threadsafe/fory.go index fc3c9bf1e8..8ba83cf491 100644 --- a/go/fory/threadsafe/fory.go +++ b/go/fory/threadsafe/fory.go @@ -76,11 +76,11 @@ func (f *Fory) Deserialize(data []byte, v interface{}) error { return inner.Unmarshal(data, v) } -// RegisterNamedType registers a named type for cross-language serialization -func (f *Fory) RegisterNamedType(type_ interface{}, typeName string) error { +// RegisterNamedStruct registers a named struct type for cross-language serialization +func (f *Fory) RegisterNamedStruct(type_ interface{}, typeName string) error { inner := f.acquire() defer f.release(inner) - return inner.RegisterByName(type_, typeName) + return inner.RegisterNamedStruct(type_, typeName) } // ============================================================================ diff --git a/go/fory/time.go b/go/fory/time.go index b923b6bacb..570093bb9b 100644 --- a/go/fory/time.go +++ b/go/fory/time.go @@ -61,10 +61,10 @@ func (s dateSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, h if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } -func (s dateSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s dateSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() diff := time.Duration(ctx.buffer.ReadInt32(err)) * 24 * time.Hour date := time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local).Add(diff) @@ -91,7 +91,7 @@ func (s timeSerializer) Write(ctx *WriteContext, refMode RefMode, writeType bool s.WriteData(ctx, value) } -func (s timeSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) { +func (s timeSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() value.Set(reflect.ValueOf(CreateTimeFromUnixMicro(ctx.buffer.ReadInt64(err)))) } @@ -109,7 +109,7 @@ func (s timeSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, h if ctx.HasError() { return } - s.ReadData(ctx, value.Type(), value) + s.ReadData(ctx, value) } func (s timeSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) { diff --git a/go/fory/type_def.go b/go/fory/type_def.go index 424b796454..d99c084b2b 100644 --- a/go/fory/type_def.go +++ b/go/fory/type_def.go @@ -773,9 +773,9 @@ func (c *CollectionFieldType) getTypeInfo(f *Fory) (TypeInfo, error) { return TypeInfo{}, err } - // For SET type, Go uses map[T]bool to represent sets + // For SET type, fory.Set[T] is defined as map[T]struct{} (empty struct value type) if c.typeId == SET { - setType := reflect.MapOf(elemInfo.Type, reflect.TypeOf(true)) + setType := reflect.MapOf(elemInfo.Type, reflect.TypeOf(struct{}{})) setSerializer, serErr := f.GetTypeResolver().GetSetSerializer(setType) if serErr != nil { return TypeInfo{}, serErr @@ -799,9 +799,9 @@ func (c *CollectionFieldType) getTypeInfoWithResolver(resolver *TypeResolver) (T return TypeInfo{}, err } - // For SET type, Go uses map[T]bool to represent sets + // For SET type, fory.Set[T] is defined as map[T]struct{} (empty struct value type) if c.typeId == SET { - setType := reflect.MapOf(elemInfo.Type, reflect.TypeOf(true)) + setType := reflect.MapOf(elemInfo.Type, reflect.TypeOf(struct{}{})) setSerializer, serErr := resolver.GetSetSerializer(setType) if serErr != nil { return TypeInfo{}, serErr @@ -1009,11 +1009,9 @@ func buildFieldType(fory *Fory, fieldValue reflect.Value) (FieldType, error) { // Handle map types BEFORE getTypeInfo to avoid anonymous type errors if fieldType.Kind() == reflect.Map { keyType := fieldType.Key() - valueType := fieldType.Elem() - // In Go xlang mode, map[T]bool is used to represent a Set - // The key type becomes the element type of the set - if valueType.Kind() == reflect.Bool { + // fory.Set[T] is defined as map[T]struct{} (empty struct value type) + if isSetReflectType(fieldType) { keyValue := reflect.Zero(keyType) keyFieldType, err := buildFieldType(fory, keyValue) if err != nil { @@ -1023,6 +1021,7 @@ func buildFieldType(fory *Fory, fieldValue reflect.Value) (FieldType, error) { } // Regular map type + valueType := fieldType.Elem() keyValue := reflect.Zero(keyType) valueValue := reflect.Zero(valueType) diff --git a/go/fory/type_def_test.go b/go/fory/type_def_test.go index 481fee0a6d..efbfd83113 100644 --- a/go/fory/type_def_test.go +++ b/go/fory/type_def_test.go @@ -108,7 +108,7 @@ func TestTypeDefEncodingDecoding(t *testing.T) { t.Run(tt.name, func(t *testing.T) { fory := NewFory(WithRefTracking(false)) - if err := fory.RegisterByName(tt.testStruct, tt.tagName); err != nil { + if err := fory.RegisterNamedStruct(tt.testStruct, tt.tagName); err != nil { t.Fatalf("Failed to register tag type: %v", err) } @@ -129,7 +129,7 @@ func TestTypeDefEncodingDecoding(t *testing.T) { // basic checks assert.True(t, decodedTypeDef.typeId == originalTypeDef.typeId || decodedTypeDef.typeId == -originalTypeDef.typeId, "TypeId mismatch") - assert.Equal(t, originalTypeDef.registerByName, decodedTypeDef.registerByName, "RegisterByName mismatch") + assert.Equal(t, originalTypeDef.registerByName, decodedTypeDef.registerByName, "RegisterNamedStruct mismatch") assert.Equal(t, originalTypeDef.compressed, decodedTypeDef.compressed, "Compressed flag mismatch") assert.Equal(t, len(originalTypeDef.fieldDefs), len(decodedTypeDef.fieldDefs), "Field count mismatch") @@ -196,7 +196,7 @@ func TestTypeDefNullableFields(t *testing.T) { fory := NewFory(WithRefTracking(false)) // Register the type - if err := fory.RegisterByName(Item1{}, "test.Item1"); err != nil { + if err := fory.RegisterNamedStruct(Item1{}, "test.Item1"); err != nil { t.Fatalf("Failed to register type: %v", err) } diff --git a/go/fory/type_resolver.go b/go/fory/type_resolver.go index 5e2de1bb16..ebef7083bc 100644 --- a/go/fory/type_resolver.go +++ b/go/fory/type_resolver.go @@ -78,6 +78,7 @@ var ( int32Int32MapType = reflect.TypeOf((*map[int32]int32)(nil)).Elem() int64Int64MapType = reflect.TypeOf((*map[int64]int64)(nil)).Elem() intIntMapType = reflect.TypeOf((*map[int]int)(nil)).Elem() + emptyStructType = reflect.TypeOf((*struct{})(nil)).Elem() boolType = reflect.TypeOf((*bool)(nil)).Elem() byteType = reflect.TypeOf((*byte)(nil)).Elem() uint8Type = reflect.TypeOf((*uint8)(nil)).Elem() @@ -93,7 +94,7 @@ var ( float64Type = reflect.TypeOf((*float64)(nil)).Elem() dateType = reflect.TypeOf((*Date)(nil)).Elem() timestampType = reflect.TypeOf((*time.Time)(nil)).Elem() - genericSetType = reflect.TypeOf((*GenericSet)(nil)).Elem() + genericSetType = reflect.TypeOf((*Set[any])(nil)).Elem() ) // Global registry for generated serializer factories @@ -241,7 +242,7 @@ func newTypeResolver(fory *Fory) *TypeResolver { dateType, timestampType, interfaceType, - genericSetType, // FIXME set should be a generic type + genericSetType, } { r.typeInfoToType[t.String()] = t r.typeToTypeInfo[t] = t.String() @@ -338,7 +339,7 @@ func (r *TypeResolver) initialize() { {stringInt64MapType, MAP, stringInt64MapSerializer{}}, {stringIntMapType, MAP, stringIntMapSerializer{}}, {stringFloat64MapType, MAP, stringFloat64MapSerializer{}}, - {stringBoolMapType, SET, setSerializer{}}, // map[T]bool represents a Set in Go + {stringBoolMapType, MAP, stringBoolMapSerializer{}}, // map[string]bool is a regular map {int32Int32MapType, MAP, int32Int32MapSerializer{}}, {int64Int64MapType, MAP, int64Int64MapSerializer{}}, {intIntMapType, MAP, intIntMapSerializer{}}, @@ -411,9 +412,9 @@ func (r *TypeResolver) registerSerializer(type_ reflect.Type, typeId TypeId, s S return nil } -// RegisterByID registers a type with a numeric type ID for cross-language serialization. +// RegisterStruct registers a type with a numeric type ID for cross-language serialization. // This is used when the full type ID (user_id << 8 | internal_id) is already calculated. -func (r *TypeResolver) RegisterByID(type_ reflect.Type, fullTypeID uint32) error { +func (r *TypeResolver) RegisterStruct(type_ reflect.Type, fullTypeID uint32) error { // Check if already registered if info, ok := r.typeIDToTypeInfo[fullTypeID]; ok { return fmt.Errorf("type %s with id %d has been registered", info.Type, fullTypeID) @@ -456,14 +457,14 @@ func (r *TypeResolver) RegisterByID(type_ reflect.Type, fullTypeID uint32) error } default: - return fmt.Errorf("unsupported type for ID registration: %v (use RegisterEnumByID for enum types)", type_.Kind()) + return fmt.Errorf("unsupported type for ID registration: %v (use RegisterEnum for enum types)", type_.Kind()) } return nil } -// RegisterEnumByID registers an enum type (numeric type in Go) with a full type ID -func (r *TypeResolver) RegisterEnumByID(type_ reflect.Type, fullTypeID uint32) error { +// RegisterEnum registers an enum type (numeric type in Go) with a full type ID +func (r *TypeResolver) RegisterEnum(type_ reflect.Type, fullTypeID uint32) error { // Check if already registered if info, ok := r.typeIDToTypeInfo[fullTypeID]; ok { return fmt.Errorf("type %s with id %d has been registered", info.Type, fullTypeID) @@ -475,7 +476,7 @@ func (r *TypeResolver) RegisterEnumByID(type_ reflect.Type, fullTypeID uint32) e reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: // OK default: - return fmt.Errorf("RegisterEnumByID only supports numeric types; got: %v", type_.Kind()) + return fmt.Errorf("RegisterEnum only supports numeric types; got: %v", type_.Kind()) } // Create enum serializer @@ -501,8 +502,8 @@ func (r *TypeResolver) RegisterEnumByID(type_ reflect.Type, fullTypeID uint32) e return nil } -// RegisterEnumByName registers an enum type (numeric type in Go) with a namespace and type name -func (r *TypeResolver) RegisterEnumByName(type_ reflect.Type, namespace, typeName string) error { +// RegisterNamedEnum registers an enum type (numeric type in Go) with a namespace and type name +func (r *TypeResolver) RegisterNamedEnum(type_ reflect.Type, namespace, typeName string) error { // Check if already registered if prev, ok := r.typeToSerializers[type_]; ok { return fmt.Errorf("type %s already has a serializer %s registered", type_, prev) @@ -514,7 +515,7 @@ func (r *TypeResolver) RegisterEnumByName(type_ reflect.Type, namespace, typeNam reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: // OK default: - return fmt.Errorf("RegisterEnumByName only supports numeric types; got: %v", type_.Kind()) + return fmt.Errorf("RegisterNamedEnum only supports numeric types; got: %v", type_.Kind()) } // Parse namespace from typeName if not provided @@ -551,7 +552,7 @@ func (r *TypeResolver) RegisterEnumByName(type_ reflect.Type, namespace, typeNam return nil } -func (r *TypeResolver) RegisterNamedType( +func (r *TypeResolver) RegisterNamedStruct( type_ reflect.Type, typeId uint32, namespace string, @@ -629,10 +630,10 @@ func (r *TypeResolver) RegisterExt(extId int16, type_ reflect.Type) error { panic("not supported") } -// RegisterExtensionType registers a type as an extension type (NAMED_EXT). +// RegisterNamedExtension registers a type as an extension type (NAMED_EXT). // Extension types use a user-provided serializer for custom serialization logic. // This is used for types with custom serializers in cross-language serialization. -func (r *TypeResolver) RegisterExtensionType( +func (r *TypeResolver) RegisterNamedExtension( type_ reflect.Type, namespace string, typeName string, @@ -689,8 +690,8 @@ func (r *TypeResolver) RegisterExtensionType( return nil } -// RegisterExtensionTypeByID registers a type as an extension type with a numeric ID. -func (r *TypeResolver) RegisterExtensionTypeByID( +// RegisterExtension registers a type as an extension type with a numeric ID. +func (r *TypeResolver) RegisterExtension( type_ reflect.Type, userTypeID uint32, userSerializer ExtensionSerializer, @@ -879,7 +880,7 @@ func (r *TypeResolver) getTypeInfo(value reflect.Value, create bool) (*TypeInfo, // First register the value type elemPkgPath := elemType.PkgPath() elemTypeName := elemType.Name() - if err := r.RegisterNamedType(elemType, 0, elemPkgPath, elemTypeName); err != nil { + if err := r.RegisterNamedStruct(elemType, 0, elemPkgPath, elemTypeName); err != nil { // Might already be registered, that's okay _ = err } @@ -926,7 +927,7 @@ func (r *TypeResolver) getTypeInfo(value reflect.Value, create bool) (*TypeInfo, // Auto-assign IDs typeID = 0 default: - fmt.Errorf("type %v must be registered explicitly", type_) + panic(fmt.Errorf("type %v must be registered explicitly", type_)) } /* @@ -1422,6 +1423,11 @@ func (r *TypeResolver) createSerializer(type_ reflect.Type, mapInStruct bool) (s }, nil } case reflect.Map: + // Check if this is a Set type (map[T]struct{} where value is empty struct) + // This includes both fory.Set[T] and raw map[T]struct{} + if isSetReflectType(type_) { + return setSerializer{}, nil + } hasKeySerializer, hasValueSerializer := !isDynamicType(type_.Key()), !isDynamicType(type_.Elem()) if hasKeySerializer || hasValueSerializer { var keySerializer, valueSerializer Serializer @@ -1468,7 +1474,7 @@ func (r *TypeResolver) createSerializer(type_ reflect.Type, mapInStruct bool) (s return nil, fmt.Errorf("cannot auto-register anonymous struct type %s", type_.String()) } // For auto-registered types, use package path as namespace and type name - if err := r.RegisterNamedType(type_, 0, pkgPath, typeName); err != nil { + if err := r.RegisterNamedStruct(type_, 0, pkgPath, typeName); err != nil { return nil, fmt.Errorf("failed to auto-register struct %s: %w", type_.String(), err) } serializer = r.typeToSerializers[type_] @@ -1522,13 +1528,11 @@ func (r *TypeResolver) GetSliceSerializer(sliceType reflect.Type) (Serializer, e return newSliceSerializer(sliceType, elemSerializer, r.isXlang) } -// GetSetSerializer returns the setSerializer for a map[T]bool type (used to represent sets in Go). +// GetSetSerializer returns the setSerializer for a Set[T] type. +// Accepts both fory.Set[T] and anonymous map[T]struct{} types. func (r *TypeResolver) GetSetSerializer(setType reflect.Type) (Serializer, error) { - if setType.Kind() != reflect.Map { - return nil, fmt.Errorf("expected map type but got %s", setType.Kind()) - } - if setType.Elem().Kind() != reflect.Bool { - return nil, fmt.Errorf("expected map[T]bool for set but got map[%s]%s", setType.Key(), setType.Elem()) + if !isSetReflectType(setType) { + return nil, fmt.Errorf("expected Set type (map[T]struct{}) but got %s", setType) } return setSerializer{}, nil } diff --git a/go/fory/type_test.go b/go/fory/type_test.go index 88f3a611d5..01378b4003 100644 --- a/go/fory/type_test.go +++ b/go/fory/type_test.go @@ -29,8 +29,8 @@ func TestTypeResolver(t *testing.T) { type A struct { F1 string } - require.Nil(t, typeResolver.RegisterNamedType(reflect.TypeOf(A{}), 0, "", "example.A")) - require.Error(t, typeResolver.RegisterNamedType(reflect.TypeOf(A{}), 0, "", "example.A")) + require.Nil(t, typeResolver.RegisterNamedStruct(reflect.TypeOf(A{}), 0, "", "example.A")) + require.Error(t, typeResolver.RegisterNamedStruct(reflect.TypeOf(A{}), 0, "", "example.A")) var tests = []struct { type_ reflect.Type