Module 5 · Lesson 2 · ~25 min read

Defining a Service and Generating Go

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.

A tiny but complete proto file

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 {}

Anatomy of a proto file

Installing the toolchain

# 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.

Generating Go code

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:

Or use buf, which is nicer

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.

What the generated Go looks like

// 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:

Server implementation skeleton

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)
}

Client skeleton

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,
})

For Canton: the practical workflow

  1. Clone the daml repo at the version matching your Canton.
  2. Copy the relevant .proto files (Ledger API ones at minimum) into your Go module.
  3. Set up a buf.yaml + buf.gen.yaml in your module root.
  4. buf generate.
  5. Import the generated packages and call the Ledger API.

Module 7 (capstone) walks through this end-to-end against a live Canton sandbox.

Takeaways