GOB: Serialize a cyclic structure with interfaces

Hello All,

I am attempting to serialize what for me is a fairly complicated structure.
I have used gob in the past but for fairly simple things. I’ve identified a number of sticking points where I’m not sure how to proceed.

  1. (non) exported fields
  2. pointers - nil and cycles
  3. interfaces and generics

My domain is fairly complicated so I created a toy example that maintains the important parts of the code. I am trying to add serialization to an existing library so while I likely could change the code I’d like to see how far I can go with this.
I ultimately want a more dense graph, but for learning purposes I’ve reduced it so that the friends can only have one friend but ultimately would be a map

package friends

import (
	"bytes"
	"encoding/gob"
	"log"
	"strconv"
)

type Events = []float32

type Friend interface {
	ID() string
	Events() Events
}

type BasicFriend struct {
	point float32
}

func (n BasicFriend) ID() string {
	return strconv.FormatFloat(float64(n.point), 'f', -1, 32)
}

func (n BasicFriend) Events() []float32 {
	return []float32{float32(n.point)}
}

// what I want eventually
type socialNetwork[T Friend] struct {
	user    Friend
	friends map[string]*socialNetwork[T]
}

type lonelyNetwork[T Friend] struct {
	user    Friend
	friends *lonelyNetwork[T]
}

func interfaceEncode(enc *gob.Encoder, p Friend) {
	err := enc.Encode(&p)
	if err != nil {
		log.Fatal("encode:", err)
	}
}

func interfaceDecode(dec *gob.Decoder) Friend {
	var p Friend
	err := dec.Decode(&p)
	if err != nil {
		log.Fatal("decode:", err)
	}
	return p
}

func (p *lonelyNetwork[T]) MarshalBinary() (_ []byte, err error) {
	var buf bytes.Buffer
	enc := gob.NewEncoder(&buf)
	// enc.Encode(p.user)
	interfaceEncode(enc, p.user)
	if p.friends == nil {
		return buf.Bytes(), nil
	}
	isCyclic := p.friends != nil && p.friends.friends == p
	enc.Encode(isCyclic)
	if isCyclic {
		p.friends.friends = nil
		err = enc.Encode(p.friends)
		p.friends.friends = p
	} else {
		err = enc.Encode(p.friends)
	}
	return buf.Bytes(), err
}

func (p *lonelyNetwork[T]) UnmarshalBinary(data []byte) (err error) {
	dec := gob.NewDecoder(bytes.NewReader(data))
	if err = dec.Decode(&p.user); err != nil {
		return
	}
	var isCyclic bool
	if err = dec.Decode(&isCyclic); err != nil {
		return
	}
	// err = dec.Decode(&p.friends)
	interfaceDecode(dec)
	if isCyclic {
		p.friends.friends = p
	}
	return
}

// These are required to encode BasicFriend as it exports no fields
func (d *BasicFriend) GobEncode() ([]byte, error) {
	var buf bytes.Buffer
	encoder := gob.NewEncoder(&buf)
	if err := encoder.Encode(d.point); err != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}

func (d *BasicFriend) GobDecode(b []byte) error {
	buf := bytes.NewBuffer(b)
	decoder := gob.NewDecoder(buf)
	if err := decoder.Decode(&d.point); err != nil {
		return err
	}
	return nil
}


I’m currently stuck at the encoding of the Friend interface. The code in it’s current from returns the following error

 encode:gob: unaddressable value of type *hnsw.BasicFriend

If I do not pass to the interfaceEncode/Decode then I’ll get

gob: local interface type *hnsw.Friend can only be decoded from remote interface type; received concrete type bool

I’m not sure if it’s picking up the isCyclic flag or something else going on.

If it’s helpful I have some testcode

package friends

import (
	"bytes"
	"encoding/gob"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestBasicFriend(t *testing.T) {
	var disk bytes.Buffer
	enc := gob.NewEncoder(&disk)
	friend := BasicFriend{1.1}
	err := enc.Encode(&friend)
	require.NoError(t, err)
	dec := gob.NewDecoder(&disk)
	var decodedFriend BasicFriend
	err = dec.Decode(&decodedFriend)
	require.NoError(t, err)
	require.Equal(t, friend.point, decodedFriend.point)
}

func TestLonelyNetwork(t *testing.T) {
	gob.Register(BasicFriend{})
	var disk bytes.Buffer
	enc := gob.NewEncoder(&disk)
	friend := BasicFriend{1.1}
	bestFriend := BasicFriend{2.1}
	bestFriendNetwork := lonelyNetwork[BasicFriend]{user: bestFriend}
	network := lonelyNetwork[BasicFriend]{user: friend, friends: &bestFriendNetwork}
	err := enc.Encode(&network)
	require.NoError(t, err)
	dec := gob.NewDecoder(&disk)
	var decodedNetwork lonelyNetwork[BasicFriend]
	err = dec.Decode(&decodedNetwork)
	require.NoError(t, err)
}