You don’t actually generate Go structs with protoc. You generate Go interfaces and concrete implementations that conform to those interfaces, and that’s a crucial distinction.

Let’s say you have this minimal .proto file, user.proto:

syntax = "proto3";

package myapp;

message User {
  string name = 1;
  int32 age = 2;
}

The command to generate Go code is:

protoc --go_out=. --go_opt=paths=source_relative user.proto

This creates a file named user.pb.go (or similar, depending on your paths option). Inside, you won’t find a type User struct {...}. Instead, you’ll see code that looks roughly like this (simplified):

// User is the proto message
type User struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Age  int32  `protobuf:"varint,7,opt,name=age,proto3" json:"age,omitempty"`
}

// Reset clears the message.
func (x *User) Reset() { ... }

// String returns the message as a string.
func (x *User) String() { ... }

// ProtoMessage is the interface implementation
func (x *User) ProtoMessage() { ... }

// Reset is the interface implementation
func (x *User) Reset() { ... }

// Size is the interface implementation
func (x *User) Size() int { ... }

// GetName returns the Name field value.
func (x *User) GetName() string { ... }

// GetAge returns the Age field value.
func (x *User) GetAge() int32 { ... }

// ... other methods for marshaling, unmarshaling, etc.

Notice the type User struct. This is the concrete Go struct that represents your Protobuf message. What protoc generates are not just plain Go structs, but a whole set of methods that implement the proto.Message interface (and others). This includes Reset, String, ProtoMessage, Size, and crucially, methods for marshaling (encoding) and unmarshaling (decoding) the message into its binary Protobuf format.

The magic isn’t just in the struct definition, but in the generated methods that allow you to:

  1. Create and populate messages:

    u := &myapp.User{
        Name: "Alice",
        Age:  30,
    }
    
  2. Marshal them into bytes:

    data, err := proto.Marshal(u)
    if err != nil {
        log.Fatalf("Failed to marshal: %v", err)
    }
    fmt.Printf("Marshaled data: %x\n", data) // e.g., 0a05416c696365181e
    
  3. Unmarshal bytes back into messages:

    newUser := &myapp.User{}
    err = proto.Unmarshal(data, newUser)
    if err != nil {
        log.Fatalf("Failed to unmarshal: %v", err)
    }
    fmt.Printf("Unmarshaled user: %+v\n", newUser) // e.g., &{Name:Alice Age:30}
    

The generated code handles all the low-level details of Protobuf encoding (varints, length-delimited fields, etc.) for you. The Go struct fields (Name string, Age int32) directly map to the Protobuf fields defined in your .proto file, including their field numbers (1 for name, 2 for age in your .proto file, but the generated Go code might show different internal numbers like varint,7,opt,name=age,proto3 due to internal compiler optimizations or field number remapping in some versions).

The go_opt=paths=source_relative flag is important because it tells protoc to place the generated Go file in the same directory as the .proto file, and to use relative import paths. Without it, the generated code might use absolute import paths that could be difficult to manage.

The most surprising true thing about Protobuf Go code generation is that it’s not just about mapping .proto fields to Go struct fields; it’s about generating a full, self-contained serialization and deserialization engine for your specific message types, complete with idiomatic Go methods and interface implementations. This allows your Go applications to seamlessly exchange data with other systems using the Protobuf format, regardless of the programming language used on the other end.

The next concept you’ll likely run into is handling nested messages and oneof fields, which introduce more complex generation patterns and require careful consideration of how to access and manipulate their data in Go.

Want structured learning?

Take the full Protobuf course →