How to Use Protocol Buffers in Go

A practical guide to working with Protocol Buffers in Go applications

Published: January 2025 • 9 min read

Go and Protocol Buffers are a perfect match. Go's simplicity and performance, combined with Protobuf's efficiency, make them ideal for building microservices and high-performance systems. Many companies use this combination with gRPC for their service communication.

This guide walks you through everything you need to use Protocol Buffers in Go, from installation to building working examples.

What You'll Need

  • Go: Version 1.16 or higher (check with go version)
  • Protocol Buffer Compiler: We'll install this next
  • Basic Go knowledge: Understanding of Go modules and packages

Step 1: Install Required Tools

First, install the Protocol Buffer compiler:

# On macOS
brew install protobuf

# On Linux
sudo apt install protobuf-compiler

# On Windows (using chocolatey)
choco install protoc

Verify installation:

protoc --version
# Should show: libprotoc 3.x.x or higher

Install the Go protobuf plugin:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

Make sure your $GOPATH/bin is in your PATH:

export PATH="$PATH:$(go env GOPATH)/bin"

Step 2: Create a Go Project

mkdir protobuf-go-example
cd protobuf-go-example
go mod init example.com/protobuf-demo

mkdir proto
mkdir main

Your project structure should look like:

protobuf-go-example/
├── go.mod
├── proto/
│   └── person.proto
└── main/
    └── main.go

Step 3: Create Your .proto File

Create proto/person.proto:

syntax = "proto3";

package person;

option go_package = "example.com/protobuf-demo/person";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
  repeated string phone_numbers = 4;
  
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
  
  repeated PhoneNumber phones = 5;
  bool is_active = 6;
}

message AddressBook {
  repeated Person people = 1;
}

Important:

The go_package option is required. It tells the compiler where to place generated Go code.

Step 4: Generate Go Code

Run the protoc compiler:

protoc --go_out=. --go_opt=paths=source_relative proto/person.proto

This generates proto/person.pb.go containing all the Go types and methods.

Install the protobuf runtime library:

go get google.golang.org/protobuf/proto

Step 5: Use Protocol Buffers in Go

Create main/main.go:

package main

import (
    "fmt"
    "log"
    "os"
    
    "google.golang.org/protobuf/proto"
    pb "example.com/protobuf-demo/proto"
)

func main() {
    // Create a Person
    person := &pb.Person{
        Name:  "John Doe",
        Id:    1234,
        Email: "john.doe@example.com",
        PhoneNumbers: []string{"555-1234", "555-5678"},
        Phones: []*pb.Person_PhoneNumber{
            {
                Number: "555-9999",
                Type:   pb.Person_MOBILE,
            },
        },
        IsActive: true,
    }

    fmt.Println("Created Person:")
    fmt.Printf("Name: %s\n", person.Name)
    fmt.Printf("ID: %d\n", person.Id)
    fmt.Printf("Email: %s\n", person.Email)

    // Serialize to bytes
    data, err := proto.Marshal(person)
    if err != nil {
        log.Fatal("marshaling error: ", err)
    }
    fmt.Printf("\nSerialized to %d bytes\n", len(data))

    // Deserialize from bytes
    person2 := &pb.Person{}
    err = proto.Unmarshal(data, person2)
    if err != nil {
        log.Fatal("unmarshaling error: ", err)
    }
    fmt.Println("\nDeserialized Person:")
    fmt.Printf("Name: %s\n", person2.Name)
    fmt.Printf("Active: %t\n", person2.IsActive)

    // Save to file
    err = os.WriteFile("person.bin", data, 0644)
    if err != nil {
        log.Fatal("write error: ", err)
    }
    fmt.Println("\nSaved to person.bin")

    // Read from file
    data, err = os.ReadFile("person.bin")
    if err != nil {
        log.Fatal("read error: ", err)
    }
    person3 := &pb.Person{}
    err = proto.Unmarshal(data, person3)
    if err != nil {
        log.Fatal("unmarshal error: ", err)
    }
    fmt.Printf("Read from file: %s\n", person3.Name)

    // Create AddressBook with multiple people
    addressBook := &pb.AddressBook{
        People: []*pb.Person{
            person,
            {
                Name:  "Jane Smith",
                Id:    5678,
                Email: "jane@example.com",
            },
        },
    }
    fmt.Printf("\nAddress book has %d people\n", len(addressBook.People))
}

Step 6: Run Your Application

go run main/main.go

Expected output:

Created Person:
Name: John Doe
ID: 1234
Email: john.doe@example.com

Serialized to 78 bytes

Deserialized Person:
Name: John Doe
Active: true

Saved to person.bin
Read from file: John Doe

Address book has 2 people

Common Operations in Go

Check Field Presence

// For pointer fields (optional)
if person.Email != nil {
    fmt.Println("Email:", *person.Email)
}

// For primitive fields, check against zero value
if person.Name != "" {
    fmt.Println("Name:", person.Name)
}

Clone a Message

person2 := proto.Clone(person).(*pb.Person)

Merge Messages

// Merge person2 into person1
proto.Merge(person1, person2)

Reset a Message

proto.Reset(person)

Convert to JSON

import "google.golang.org/protobuf/encoding/protojson"

jsonData, err := protojson.Marshal(person)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(jsonData))

Parse from JSON

jsonStr := `{"name": "Bob", "id": 9999}`
person := &pb.Person{}
err := protojson.Unmarshal([]byte(jsonStr), person)
if err != nil {
    log.Fatal(err)
}

Best Practices for Go

Always Check Errors

Go's error handling is explicit. Always check errors returned by Marshal andUnmarshal.

Use Pointers for Messages

Always work with pointers to protobuf messages (*pb.Person) rather than values to avoid copying large structures.

Don't Modify Generated Code

Never edit .pb.go files. They're regenerated when you recompile your .proto files.

Use go_package Option

Always specify go_package in your .proto files to control where generated code is placed.

Using with gRPC

Protocol Buffers are commonly used with gRPC in Go. To add gRPC support:

# Install gRPC plugin
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# Generate with gRPC support
protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    proto/person.proto

Common Issues

Issue: protoc-gen-go not found

Solution: Make sure $GOPATH/bin is in your PATH. Run which protoc-gen-go to verify.

Issue: Import errors after generation

Solution: Run go mod tidy to download required dependencies.

Issue: go_package option required

Solution: Add option go_package = "your/package/path"; to your .proto file.

Related Tools

Conclusion

Protocol Buffers fit naturally into Go's ecosystem. The generated code is idiomatic Go, and the tooling integrates seamlessly with go modules. Combined with gRPC, this becomes a powerful stack for building distributed systems.

Start with these simple examples, then explore more advanced features like gRPC services, custom options, and performance optimization as your needs grow.