* Auto-formatting: ran `mdformat --extensions frontmatter --number .` * manual tweaks * manual tweaks
7.6 KiB
Go SDK Data Handling
Overview
The Go SDK uses the converter.DataConverter interface to serialize/deserialize workflow inputs, outputs, and activity parameters. The default converter converts values to JSON.
Default Data Converter
The default CompositeDataConverter applies converters in order until one returns a non-nil Payload:
converter.NewNilPayloadConverter()-- nil valuesconverter.NewByteSlicePayloadConverter()--[]byteconverter.NewProtoJSONPayloadConverter()-- Protobuf messages as JSONconverter.NewProtoPayloadConverter()-- Protobuf messages as binaryconverter.NewJSONPayloadConverter()-- anything JSON-serializable
Structs must have exported fields to be serialized.
Custom Data Converter
In most cases you don't implement the full DataConverter interface directly. Instead, implement a PayloadConverter for your specific type and insert it into a CompositeDataConverter. The PayloadConverter interface has four methods:
type PayloadConverter interface {
ToPayload(value interface{}) (*commonpb.Payload, error) // return nil if this type isn't handled
FromPayload(payload *commonpb.Payload, valuePtr interface{}) error
ToString(payload *commonpb.Payload) string
Encoding() string // e.g. "json/msgpack"
}
Example — custom msgpack PayloadConverter:
import (
"encoding/json"
"fmt"
commonpb "go.temporal.io/api/common/v1"
"go.temporal.io/sdk/converter"
"github.com/vmihailenco/msgpack/v5"
)
const encodingMsgpack = "binary/msgpack"
type MsgpackPayloadConverter struct{}
func (c *MsgpackPayloadConverter) Encoding() string {
return encodingMsgpack
}
func (c *MsgpackPayloadConverter) ToPayload(value interface{}) (*commonpb.Payload, error) {
if value == nil {
return nil, nil
}
data, err := msgpack.Marshal(value)
if err != nil {
return nil, fmt.Errorf("msgpack marshal: %w", err)
}
return &commonpb.Payload{
Metadata: map[string][]byte{
converter.MetadataEncoding: []byte(encodingMsgpack),
},
Data: data,
}, nil
}
func (c *MsgpackPayloadConverter) FromPayload(payload *commonpb.Payload, valuePtr interface{}) error {
if string(payload.GetMetadata()[converter.MetadataEncoding]) != encodingMsgpack {
return fmt.Errorf("unsupported encoding")
}
return msgpack.Unmarshal(payload.Data, valuePtr)
}
func (c *MsgpackPayloadConverter) ToString(payload *commonpb.Payload) string {
// Decode to a map for human-readable display
var v interface{}
if err := msgpack.Unmarshal(payload.Data, &v); err != nil {
return fmt.Sprintf("<msgpack: %v>", err)
}
b, _ := json.Marshal(v)
return string(b)
}
Register in a CompositeDataConverter and pass to the client:
dataConverter := converter.NewCompositeDataConverter(
converter.NewNilPayloadConverter(),
converter.NewByteSlicePayloadConverter(),
&MsgpackPayloadConverter{}, // handles your type; falls through to JSON for everything else
converter.NewJSONPayloadConverter(),
)
c, err := client.Dial(client.Options{
DataConverter: dataConverter,
})
Per-activity/child-workflow override — use a different converter for specific calls:
actCtx := workflow.WithDataConverter(ctx, mySpecialConverter)
workflow.ExecuteActivity(actCtx, SensitiveActivity, input)
Note: If your converter makes remote calls (e.g., to a KMS for encryption), wrap it with workflow.DataConverterWithoutDeadlockDetection to avoid deadlock detection timeouts in workflow code.
Composition of Payload Converters
Use converter.NewCompositeDataConverter to chain type-specific converters. The first converter that can handle the type wins.
dataConverter := converter.NewCompositeDataConverter(
converter.NewNilPayloadConverter(),
converter.NewByteSlicePayloadConverter(),
converter.NewProtoJSONPayloadConverter(),
converter.NewProtoPayloadConverter(),
YourCustomPayloadConverter(),
converter.NewJSONPayloadConverter(),
)
Protobuf Support
Binary protobuf:
converter.NewProtoPayloadConverter()
JSON protobuf:
converter.NewProtoJSONPayloadConverter()
Both are included in the default data converter. SDK v1.26.0 (March 2024) migrated from gogo/protobuf to google/protobuf. If you need backward compatibility with older payloads encoded with gogo, use the LegacyTemporalProtoCompat option.
Payload Encryption
Implement the converter.PayloadCodec interface (Encode and Decode) and wrap the default data converter:
// Codec implements converter.PayloadCodec for encryption.
type Codec struct{}
func (Codec) Encode(payloads []*commonpb.Payload) ([]*commonpb.Payload, error) {
result := make([]*commonpb.Payload, len(payloads))
for i, p := range payloads {
origBytes, err := p.Marshal()
if err != nil {
return payloads, err
}
encrypted := encrypt(origBytes) // your encryption logic
result[i] = &commonpb.Payload{
Metadata: map[string][]byte{converter.MetadataEncoding: []byte("binary/encrypted")},
Data: encrypted,
}
}
return result, nil
}
func (Codec) Decode(payloads []*commonpb.Payload) ([]*commonpb.Payload, error) {
result := make([]*commonpb.Payload, len(payloads))
for i, p := range payloads {
if string(p.Metadata[converter.MetadataEncoding]) != "binary/encrypted" {
result[i] = p
continue
}
decrypted := decrypt(p.Data) // your decryption logic
result[i] = &commonpb.Payload{}
err := result[i].Unmarshal(decrypted)
if err != nil {
return payloads, err
}
}
return result, nil
}
Wrap with CodecDataConverter and pass to client:
var DataConverter = converter.NewCodecDataConverter(
converter.GetDefaultDataConverter(),
&Codec{},
)
c, err := client.Dial(client.Options{
DataConverter: DataConverter,
})
Search Attributes
Set at workflow start:
handle, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: "order-123",
TaskQueue: "orders",
SearchAttributes: map[string]interface{}{
"OrderStatus": "pending",
"CustomerId": "cust-456",
},
}, OrderWorkflow, input)
Upsert from within a workflow:
err := workflow.UpsertSearchAttributes(ctx, map[string]interface{}{
"OrderStatus": "completed",
})
Typed search attributes (v1.26.0+, preferred):
var OrderStatusKey = temporal.NewSearchAttributeKeyKeyword("OrderStatus")
err := workflow.UpsertTypedSearchAttributes(ctx, OrderStatusKey.ValueSet("completed"))
Query workflows by search attributes:
resp, err := c.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
Query: `OrderStatus = "pending" AND CustomerId = "cust-456"`,
})
Workflow Memo
Set in start options:
handle, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: "order-123",
TaskQueue: "orders",
Memo: map[string]interface{}{
"customerName": "Alice",
"notes": "Priority customer",
},
}, OrderWorkflow, input)
Read memo from workflow info. Upsert memo (Go SDK only):
err := workflow.UpsertMemo(ctx, map[string]interface{}{
"notes": "Updated notes",
})
Best Practices
- Use structs with exported fields for inputs and outputs
- Prefer JSON for readability during development, protobuf for performance in production
- Keep payloads small -- see
references/core/gotchas.mdfor limits - Use
PayloadCodecfor encryption; never store sensitive data unencrypted - Configure the same data converter on both client and worker