Module 5 · Lesson 2 · ~25 min read
Walk through the full toolchain: write a .proto file, run the code generator, get usable Go types and stubs out the other side. The pattern you'll repeat for every Canton client.
syntax = "proto3";
package example.v1;
option go_package = "example.com/cantonctl/proto/example/v1;examplev1";
import "google/protobuf/timestamp.proto";
service Submitter {
rpc Submit(SubmitRequest) returns (SubmitResponse);
rpc StreamUpdates(StreamRequest) returns (stream Update);
}
message SubmitRequest {
string command_id = 1;
string party = 2;
bytes payload = 3;
google.protobuf.Timestamp deadline = 4;
}
message SubmitResponse {
string submission_id = 1;
enum Status {
UNKNOWN = 0;
ACCEPTED = 1;
REJECTED = 2;
}
Status status = 2;
string error_message = 3;
}
message StreamRequest {
string party = 1;
int64 offset = 2;
}
message Update {
int64 offset = 1;
string contract_id = 2;
oneof kind {
CreatedEvent created = 3;
ArchivedEvent archived = 4;
}
}
message CreatedEvent { bytes payload = 1; }
message ArchivedEvent {}
syntax = "proto3" — proto3 is the modern dialect. Canton uses proto3.package — wire-level namespace for the messages. Combines with the message name to form full names like example.v1.SubmitRequest.option go_package — tells the Go generator where the output lives and what package name to use. Format: full/import/path;short_pkg_name.import — pulls in another proto file. google/protobuf/timestamp.proto is the standard timestamp message.service — a group of RPC methods.message — a record type. Fields have a type, name, and number.enum — named integer constants. Always include a 0-valued UNSPECIFIED or UNKNOWN default. Required by proto3 conventions.oneof — sum type / discriminated union. At most one of the fields inside is set. The generated Go uses an interface + struct wrapper.# protoc itself
brew install protobuf # macOS
# or download from https://github.com/protocolbuffers/protobuf/releases
# Go plugins (puts binaries in $GOBIN, usually ~/go/bin)
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Make sure ~/go/bin is on your PATH so protoc can find the plugins.
protoc \
--proto_path=proto \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/example/v1/submitter.proto
This produces two files next to your proto:
submitter.pb.go — message types (SubmitRequest, Update, etc.) with Marshal/Unmarshal methods.submitter_grpc.pb.go — service stubs (SubmitterClient, SubmitterServer interfaces, RegisterSubmitterServer function).Buf wraps protoc with a saner CLI, dependency management, lint, and breaking-change detection.
# Install
brew install bufbuild/buf/buf
# In your project, create buf.yaml + buf.gen.yaml
buf mod init
buf generate
For Canton work, buf is the right choice — it'll lint your protos to the same standard as Canton's, and detect breaking schema changes before you ship them.
// In submitter.pb.go (excerpt)
type SubmitRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
CommandId string `protobuf:"bytes,1,opt,name=command_id,..."`
Party string `protobuf:"bytes,2,opt,name=party,..."`
Payload []byte `protobuf:"bytes,3,opt,name=payload,..."`
Deadline *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=deadline,..."`
}
func (m *SubmitRequest) Reset() { ... }
func (m *SubmitRequest) String() string { ... }
func (m *SubmitRequest) GetCommandId() string { ... }
// ... etc
// In submitter_grpc.pb.go (excerpt)
type SubmitterClient interface {
Submit(ctx context.Context, in *SubmitRequest, opts ...grpc.CallOption) (*SubmitResponse, error)
StreamUpdates(ctx context.Context, in *StreamRequest, opts ...grpc.CallOption) (Submitter_StreamUpdatesClient, error)
}
type SubmitterServer interface {
Submit(context.Context, *SubmitRequest) (*SubmitResponse, error)
StreamUpdates(*StreamRequest, Submitter_StreamUpdatesServer) error
mustEmbedUnimplementedSubmitterServer()
}
Notes on what the generator gives you:
m.GetField() instead of m.Field when there's any chance the message could be nil — getters return zero values safely.Register...Server function for wiring servers.mustEmbedUnimplementedSubmitterServer — forces server implementations to embed an unimplemented base type, which makes adding new RPCs in the future a non-breaking change. Embed the unimplemented type in your server struct.type submitterServer struct {
examplev1.UnimplementedSubmitterServer // embed for forward-compat
}
func (s *submitterServer) Submit(ctx context.Context, req *examplev1.SubmitRequest) (*examplev1.SubmitResponse, error) {
// ... implement
return &examplev1.SubmitResponse{
SubmissionId: uuid(),
Status: examplev1.SubmitResponse_ACCEPTED,
}, nil
}
func main() {
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
examplev1.RegisterSubmitterServer(s, &submitterServer{})
s.Serve(lis)
}
conn, _ := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
client := examplev1.NewSubmitterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.Submit(ctx, &examplev1.SubmitRequest{
CommandId: "abc123",
Party: "Alice",
Payload: payloadBytes,
})
.proto files (Ledger API ones at minimum) into your Go module.buf.yaml + buf.gen.yaml in your module root.buf generate.Module 7 (capstone) walks through this end-to-end against a live Canton sandbox.
.proto, generator emits Go: protoc-gen-go for messages, protoc-gen-go-grpc for service stubs.buf is the production-quality wrapper — use it.GetField() for nil-safe access.UnimplementedFooServer for forward compatibility.oneof generates an interface + struct-wrapper Go shape.