10. Interfaces and method sets
This note covers the trickiest part of the type-mapping: Go interfaces and their interaction with method sets. Go's interface semantics are structural (any type with the right method set satisfies the interface) and the satisfaction rules are subtle (value vs pointer receivers). The bridge has to encode all of this through the cgo boundary.
Go interfaces are structural
A Go interface is a method set. A type satisfies the interface iff its method set includes every method the interface lists. There is no explicit implements declaration; satisfaction is checked structurally by the compiler:
type Reader interface {
Read(p []byte) (n int, err error)
}
type FileReader struct { f *os.File }
func (r *FileReader) Read(p []byte) (n int, err error) {
return r.f.Read(p)
}
// FileReader satisfies Reader implicitly; no `type FileReader implements Reader` declaration.
The bridge's strategy: an interface type becomes a Mochi extern type with per-method extern fn declarations. The Mochi side holds an opaque handle; method calls dispatch through cgo to the underlying Go value:
extern type Reader
extern fn (r: Reader) read(p: bytes): Result<int> from go "io.Reader.Read"
The Mochi user cannot construct a Reader value directly; they obtain it from a Go function that returns Reader (e.g., os.Open returns *os.File which the wrapper auto-promotes to a Reader handle when assigned to a Reader-typed variable).
Method sets: value vs pointer receivers
A Go method can have a value receiver (func (r T) M()) or a pointer receiver (func (r *T) M()). The method-set rules:
- Method set of type
T: methods with receiverT(only value receivers). - Method set of type
*T: methods with receiverTOR receiver*T(both).
This matters for interface satisfaction: a value of type T satisfies an interface only if every interface method has a value-receiver implementation on T. A value of type *T always satisfies.
The bridge encodes the receiver kind in the emitted extern fn:
extern type Buffer // wraps Go *bytes.Buffer
extern fn (b: Buffer) write_string(s: string): Result<int> from go "bytes.Buffer.WriteString" receiver "pointer"
extern fn (b: Buffer) string(): string from go "bytes.Buffer.String" receiver "value"
The receiver "pointer" / receiver "value" clause is honoured by the wrapper synthesiser when it picks the call shape:
- For value-receiver methods, the wrapper takes the handle by value (copy the Go value out of the handle, call the method, copy back if mutated... actually Go's value semantics means the wrapper just dereferences the handle, copies the value, calls the method, ignores the copy because value receivers can't mutate).
- For pointer-receiver methods, the wrapper takes the handle, dereferences to get the pointer, calls the method.
The bridge always exposes the more-complete *T method set; Mochi values are always opaque handles to *T (pointer to the underlying Go value). This sidesteps the receiver-kind subtlety on the consumer side: every Mochi call through the bridge sees the pointer's method set, which is the superset.