04. go/packages + go/types ingest
This note details the ingest pipeline: how the bridge turns a Go module .zip in the content-addressed cache into a structured ApiSurface JSON document the typemap can consume.
The two-layer Go stdlib API
The Go stdlib (and its supplement golang.org/x/tools) ships two complementary packages the bridge uses:
-
go/types(in-stdlib since Go 1.5, 2015): the type representation. Every Go type has atypes.Typevalue. The discriminator tree:*types.Basic(int, int32, int64, float64, string, bool, ...)*types.Pointer(*T)*types.Slice([]T)*types.Array([N]T)*types.Map(map[K]V)*types.Chan(chan T, <-chan T, chan<- T)*types.Struct(struct { ... }; query fields viaNumFields()andField(i))*types.Interface(interface { ... }; query method set viaNumMethods()andMethod(i))*types.Signature(func types; query params and results viaParams()andResults())*types.Named(named types viatypedeclarations; the wrapper around any of the above)*types.TypeParam(Go 1.18+ generics)*types.Union(interface constraint unions in generics)*types.Tuple(multi-return signatures)
-
golang.org/x/tools/go/packages(separate Go module, stable since 2018): loads a Go program into a typed form.packages.Load(cfg, patterns...)returns[]*packages.Packagewhere each entry carries:Name(package name, e.g.,cobra)PkgPath(canonical-import-path, e.g.,github.com/spf13/cobra)GoFiles(list of source files)Imports(transitive dep map)Types(a*types.Packagewith the full export scope)TypesInfo(a*types.Infowith token-to-type mappings)
The combination is exactly what the bridge needs: packages.Load gives the dep graph, types.Package gives the surface, and TypesInfo gives the source-token to type mapping for diagnostics.
Why no nightly toolchain
Unlike MEP-73's rustdoc-JSON ingest (which requires cargo +nightly rustdoc --output-format=json -Z unstable-options), the Go ingest path runs on stable Go from day one:
go/typeshas been stable in the Go stdlib since Go 1.5 (August 2015).golang.org/x/tools/go/packageshas documented backwards-compatibility since 2018.- The Go team's stability commitment for these APIs is as strong as for
net/httporencoding/json.
The bridge's helper binary builds against stable Go and can be cross-compiled to every host the Mochi toolchain runs on.
This is the single largest risk-reduction relative to MEP-73. The Rust bridge has to ship a nightly rustup-toolchain-install command in its onboarding; the Go bridge does not.
Helper binary shape
The bridge ships a Go-toolchain-resident helper at package3/go/cmd/go-ingest (~300 LOC):
package main
import (
"encoding/json"
"go/types"
"os"
"golang.org/x/tools/go/packages"
)
type ApiSurface struct {
Module string `json:"module"`
Version string `json:"version"`
GoVersion string `json:"go_version"`
Items []ApiItem `json:"items"`
Skipped []SkipReport `json:"skipped"`
}
type ApiItem struct {
Kind string `json:"kind"` // "func" | "type" | "const" | "var" | "method"
Name string `json:"name"`
Type ApiTypeRef `json:"type"`
Doc string `json:"doc,omitempty"`
Methods []ApiItem `json:"methods,omitempty"` // for type items
TypeParams []ApiTypeParam `json:"type_params,omitempty"` // for generic items
}
type ApiTypeRef struct {
Kind string `json:"kind"` // "basic" | "pointer" | "slice" | "map" | "chan" | "struct" | "interface" | "func" | "named" | "type_param" | "error"
Basic string `json:"basic,omitempty"` // "int64", "float64", "string", ...
Elem *ApiTypeRef `json:"elem,omitempty"` // slice / pointer / chan element
Key *ApiTypeRef `json:"key,omitempty"` // map key
Value *ApiTypeRef `json:"value,omitempty"` // map value
Fields []ApiField `json:"fields,omitempty"` // struct fields
Methods []ApiItem `json:"methods,omitempty"` // interface methods
Params []ApiTypeRef `json:"params,omitempty"` // func params
Results []ApiTypeRef `json:"results,omitempty"` // func results
Variadic bool `json:"variadic,omitempty"` // func variadic
Named string `json:"named,omitempty"` // canonical-path "."-joined name
Dir string `json:"dir,omitempty"` // chan direction "send", "recv", "both"
TypeParam string `json:"type_param,omitempty"` // type-param name
}
type ApiField struct {
Name string `json:"name"`
Type ApiTypeRef `json:"type"`
Tag string `json:"tag,omitempty"`
Anonymous bool `json:"anonymous,omitempty"`
}
type ApiTypeParam struct {
Name string `json:"name"`
Constraint ApiTypeRef `json:"constraint"`
}
type SkipReport struct {
Item string `json:"item"`
Reason string `json:"reason"`
}
func main() {
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedTypes | packages.NeedDeps |
packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedFiles,
Dir: os.Args[1],
}
pkgs, err := packages.Load(cfg, "./...")
if err != nil { panic(err) }
surface := walk(pkgs)
json.NewEncoder(os.Stdout).Encode(surface)
}
(Full implementation lives in package3/go/cmd/go-ingest/main.go; the snippet above shows the data shape.)
ApiSurface JSON schema
The emitted JSON document is the contract between the helper binary and the bridge's main Go binary. The schema is the closed shape above. A schema version (api_surface_schema_version) field at the top level records the schema epoch; bridge upgrades that change the schema bump the version.
The Type.Kind discriminator is the closed set the typemap pass consumes. Items with Kind outside the set are not emitted; they appear in the Skipped array with a reason string.
Walk order and visibility rules
The helper walks every *packages.Package and emits an ApiItem for every exported identifier in the package's Scope() (computed via pkg.Types.Scope() and iterated with Names()). Visibility rules:
- An identifier is exported iff its first rune is uppercase (Go's universal rule).
- An identifier inside an
internal/directory subtree is invisible to consumers outside the module tree. The helper checks the import path againstgolang.org/x/mod/module.MatchPath(<...>, "internal")and silently skips. - Identifiers with a
// Deprecated:comment in the godoc are still emitted, but the bridge marks them with adeprecated = truefield that the typemap honours (it emits a warning in theextern fndeclaration). - Identifiers behind build tags are emitted if the build-tag set the manifest declares includes the tag. The helper is invoked with the user's tag set via
cfg.BuildFlags.
Generic items
Go 1.18+ generic items (functions and types with [T constraint] type parameters) are emitted with the type_params array populated. The typemap pass refuses generic items by default; the user opts in via [go.monomorphise]. When an entry like { item = "encoding/json.Unmarshal", T = "MyStruct" } is declared, the bridge synthesises a non-generic wrapper that instantiates the source at the declared T.
Method sets and receiver kinds
A Go type can have value-receiver methods and pointer-receiver methods. The combined method set:
- For value type
T: methods with receiverT(not methods with receiver*T; this matters for interface satisfaction). - For pointer type
*T: methods with receiverTAND methods with receiver*T.
The helper emits both receiver kinds with a receiver_kind: "value" or receiver_kind: "pointer" field on each method item. The wrapper synthesiser uses this to pick the right call shape on the cgo side (passing by value vs. by pointer).
Embedded fields and promoted methods
A Go struct can embed another struct or interface (type Foo struct { Bar; ... }), which promotes the embedded type's methods and exported fields to the embedding type's surface. The helper resolves promotions and emits the promoted items in the embedding type's methods array, with a promoted_from = "<embedded-type-path>" field for traceability.
Test files
*_test.go files are NOT included in the ApiSurface. Test files are conditionally compiled and would inflate the surface with helper utilities. The helper invokes packages.Load with the default Tests = false to exclude them.
Cgo files
A source file declaring import "C" is a cgo file. The helper emits cgo-file items but marks them with a requires_cgo = true field. The bridge's [go.capabilities] cgo check refuses the import unless the user opts in.
Performance
The helper takes ~50-500ms per module on warm filesystem cache (measured against the 24-module fixture corpus on darwin-arm64 with Go 1.23, May 2026). For a Mochi program with 30 imported modules, the total ingest time is ~5-15 seconds at mochi pkg lock. The result is cached in ~/.cache/mochi/go-deps/api-surface/<api-surface-sha256>/; subsequent locks against the same module versions skip the ingest step.
Why not consume the go doc -json output
Go 1.22 added a go doc -json flag that emits a JSON-formatted godoc summary. The bridge does not use it because:
go docoperates on the rendered documentation, not the full type information.go doc -jsondoes not exposetypes.Typediscriminators; the bridge would have to parse natural-language type strings.go doc -json's schema is not as stable asgo/packages(added 2024 vs. 2018).
The go/packages path is the right level of abstraction.
Cross-references
- 02-design-philosophy §2 for why
go/packagesover alternatives. - 05-type-mapping for what the typemap pass does with the ApiSurface.
- 10-interface-and-method-set for the interface and method-set encoding details.
- MEP-74 §1 for where ingest sits in the pipeline.