Last active
April 28, 2022 10:00
-
-
Save arl/3951041e403d0050d5b316f9da24d47d to your computer and use it in GitHub Desktop.
Gob encoding decoding with multiple types
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module github.com/arl/gobtest | |
go 1.18 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/gob" | |
"fmt" | |
"io" | |
"log" | |
"time" | |
) | |
type Measure struct { | |
Timestamp time.Time | |
Name string | |
Type string | |
DisplayName string | |
Unit string | |
Data string | |
} | |
func NewMeasure() *Measure { | |
return &Measure{ | |
Timestamp: time.Now(), | |
Name: "some name", | |
Type: "some type", | |
DisplayName: "some display name", | |
Unit: "some unit", | |
Data: "some data", | |
} | |
} | |
type Alert struct { | |
Timestamp time.Time | |
Name string | |
State string | |
Message string | |
} | |
func NewAlert() *Alert { | |
return &Alert{ | |
Timestamp: time.Now(), | |
Name: "some name", | |
State: "some state", | |
Message: "some message", | |
} | |
} | |
func concreteHandler(r io.Reader) { | |
dec := gob.NewDecoder(r) | |
measure, err := gobDecodeConcrete[Measure](dec) | |
if err != nil { | |
log.Fatal(err) | |
} | |
fmt.Printf("%+v\n", measure) | |
alert, err := gobDecodeConcrete[Alert](dec) | |
if err != nil { | |
log.Fatal(err) | |
} | |
fmt.Printf("%+v\n", alert) | |
} | |
func dynamicHandler(r io.Reader) { | |
dec := gob.NewDecoder(r) | |
msg1, err := gobDecode(dec) | |
if err != nil { | |
log.Fatalf("msg1: %v", err) | |
} | |
msg2, err := gobDecode(dec) | |
if err != nil { | |
log.Fatalf("msg2: %v", err) | |
} | |
doSomethingDynamic := func(imsg any) { | |
switch msg := imsg.(type) { | |
case Measure: | |
fmt.Printf("%+v\n", msg) | |
case Alert: | |
fmt.Printf("%+v\n", msg) | |
default: | |
panic("") | |
} | |
} | |
doSomethingDynamic(msg1) | |
doSomethingDynamic(msg2) | |
} | |
func reader() io.Reader { | |
wire := bytes.Buffer{} | |
enc := gob.NewEncoder(&wire) | |
if err := gobEncode(enc, NewMeasure()); err != nil { | |
log.Fatal(err) | |
} | |
if err := gobEncode(enc, NewAlert()); err != nil { | |
log.Fatal(err) | |
} | |
return &wire | |
} | |
func main() { | |
// Register concrete types we want 'gob' to handle. | |
gob.RegisterName("measurement", Measure{}) | |
gob.RegisterName("alert", Alert{}) | |
// Handler acting on a single type (and 'sure' to receive that one) | |
concreteHandler(reader()) | |
// Handler having dynamic behaviour based on the type of the message it receives. | |
dynamicHandler(reader()) | |
} | |
/* 3 helper functions */ | |
// gobEncode encodes msg into the gob encoder. Underlying type of 'msg' must | |
// have previously been registered via gob.Register or gob.RegisterName. | |
// | |
// NOTE: | |
// | |
// This thin wrapper is only required if we want to support handlers to which | |
// values we can sent values of different types (i.e types are not known at | |
// compile-time). If we don't need to support that specific use case, then just | |
// using plain gob.Encoder/Decoder is enough. | |
// | |
// However in case we want to support dynamicity, we need to encode messages as | |
// interface{} rather than directly with their concrete type. Doing so means we | |
// can decode messages into interface{}, and use type assertions to perform | |
// dynamic behaviour based on the type. | |
func gobEncode(enc *gob.Encoder, msg any) error { | |
err := enc.Encode(&msg) | |
if err != nil { | |
return fmt.Errorf("gobEncode: error encoding value of type %T: %v", msg, err) | |
} | |
return nil | |
} | |
// gobDecodeConcrete is also only useful when we want to support single handlers | |
// receiving mulitple types. thanks to generic, the returned T has the final, | |
// concrete type. | |
func gobDecodeConcrete[T any](dec *gob.Decoder) (*T, error) { | |
var t any | |
err := dec.Decode(&t) | |
if err != nil { | |
return nil, fmt.Errorf("gobDecodeConcrete: error decoding payload into %T: %v", t, err) | |
} | |
conc, ok := t.(T) | |
if !ok { | |
var actual T | |
return nil, fmt.Errorf("gobDecodeConcrete: type assertion failed, got %T want %T", t, actual) | |
} | |
return &conc, nil | |
} | |
// This does nothing more than what gob.Decoder does, but it's provided in order | |
// to make the API symmetric. It's less confusing to do gobEncode and gobDecode | |
// than gobEncode and gob.Encoder.Decode imho. | |
func gobDecode(dec *gob.Decoder) (any, error) { | |
var msg any | |
if err := dec.Decode(&msg); err != nil { | |
return nil, fmt.Errorf("gobDecode: error decoding: %v", err) | |
} | |
return msg, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment