10 The Automatically Generated Code Structure Produced by `protoc`
Imagine you are building a microservices system that needs to exchange data between various services. The data protocol you use must be efficient, easy to extend, and support multiple programming languages. This is exactly where Protocol Buffers (protobuf) becomes a highly relevant solution.
By relying on the protoc tool, .proto definition files can be compiled into code in various languages automatically. But what does protoc actually generate? What does that automatically generated code structure look like? And how can developers make the most of it?
This article takes a deep dive into the automatically generated code structure produced by protoc, complete with implementation examples, usage simulations, and a process flow diagram.
Getting to Know protoc and the Code Generation Process
protoc is the official Protocol Buffer Compiler from Google. It “consumes” a .proto file and produces source code (classes, enums, service stubs) that is ready to use in your application project.
The process can be illustrated with the following diagram:
flowchart LR
A[Definisi .proto] --> B[protoc]
B --> C{Bahasa Target}
C -->|Go| D[File .pb.go]
C -->|Java| E[File .java]
C -->|Python| F[File _pb2.py]
C -->|C++| G[File .pb.h & .pb.cc]
Simulation: A Simple .proto File
Let’s take the following .proto file as an example (user.proto):
1syntax = "proto3";
2
3package user;
4
5message User {
6 int32 id = 1;
7 string name = 2;
8 string email = 3;
9}Running protoc
We run the command:
1protoc --go_out=. user.protoThe result is a user.pb.go file that is ready for you to use in your Go application.
The Automatically Generated Code Structure: Anatomy of the *_pb.go File (Go)
Let’s dissect its contents and structure.
1. Header, Licensing & Imports
1// Code generated by protoc-gen-go. DO NOT EDIT.
2// versions:
3// protoc-gen-go v1.25.0
4// protoc v3.14.0
5package user
6
7import (
8 proto "github.com/golang/protobuf/proto"
9 reflect "reflect"
10 sync "sync"
11)The opening section is typically always a “DO NOT EDIT” comment along with version information. This is followed by the import statements for the protobuf runtime library.
2. Struct Declaration
Each message in the .proto file becomes a struct in Go:
1type User struct {
2 Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
3 Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
4 Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
5}These fields come with meta tags for protobuf serialization and JSON serialization.
Field Mapping Table
.proto Syntax | Auto-generated Go Struct |
|---|---|
int32 id = 1; | Id int32 |
string name = 2; | Name string |
string email = 3; | Email string |
3. Serialization Methods and Utilities
Structs generated by protoc always include methods for (de)serialization:
1func (x *User) Reset() { ... }
2func (x *User) String() string { ... }
3func (*User) ProtoMessage() {}
4func (x *User) ProtoReflect() protoreflect.Message {
5 ...
6}These methods are the interface between the Protobuf runtime and your application’s data objects.
4. Descriptor and Registration
The metadata section of the .proto file looks like this:
1var File_user_proto protoreflect.FileDescriptor
2var file_user_proto_rawDesc = [...]byte{...}
3func init() {
4 ... // Registrasi file descriptor
5}This section is important for reflection and schema evolution.
Exploring the protoc Generation Output in Other Languages
Each language has its own conventions for the generator’s output. Here is a summary in table form:
| Language | Main output | Message Type | Serialization | File name |
|---|---|---|---|---|
| Go | Struct & Type Method | struct | Marshal/X | .pb.go |
| Java | Class & builder | class + Builder | parseFrom() | .java |
| Python | Class | class (CamelCase) | SerializeToString() | _pb2.py |
| C++ | Class | class | SerializeToString() | .pb.h/.pb.cc |
Case Study: Using the Generated Go Struct
Simulation Code
1package main
2
3import (
4 "fmt"
5 "log"
6 "user" // dari hasil compilasi user.pb.go
7
8 "google.golang.org/protobuf/proto"
9)
10
11func main() {
12 // Membuat instance User
13 u := &user.User{
14 Id: 123,
15 Name: "Bagus Prakoso",
16 Email: "bagus@company.com",
17 }
18
19 // Serialisasi ke byte array
20 raw, err := proto.Marshal(u)
21 if err != nil {
22 log.Fatal("Serialize error:", err)
23 }
24
25 fmt.Println("Protobuf bytes:", raw)
26
27 // Deserialisasi ke objek User
28 baru := &user.User{}
29 if err := proto.Unmarshal(raw, baru); err != nil {
30 log.Fatal("Deserialize error:", err)
31 }
32
33 fmt.Printf("Hasil decode: id=%v, name=%v, email=%v\n",
34 baru.Id, baru.Name, baru.Email)
35}Program output:
1Protobuf bytes: [8 123 18 14 66 97 103 117 115 32 80 114 97 107 111 115 111 26 19 98 97 103 117 115 64 99 111 109 112 97 110 121 46 99 111 109]
2Hasil decode: id=123, name=Bagus Prakoso, email=bagus@company.comProtobuf Struct Serialization-Deserialization Flow Diagram
The process above can be illustrated as follows:
flowchart LR
A[Struct User di Go] --> B["Marshal (Serialisasi)"]
B --> C["[]byte"]
C --> D["Unmarshal (Deserialisasi)"]
D --> E[Struct User]
What Else Is in the Auto-generated Code File?
If your .proto file grows more complex—for example, including an enum, oneof, nested messages, and service definitions—the code generator will automatically add:
- Enum code (enum/constant)
- Nested messages & oneof as structs and interfaces
- Client/Server interfaces for RPC (if there is a service)
- Additional validation and reflection functions
Example of an enum declaration:
1enum Status {
2 UNKNOWN = 0;
3 ACTIVE = 1;
4 INACTIVE = 2;
5}
6message Account {
7 Status status = 1;
8}The resulting Go snippet (simplified):
1type Status int32
2
3const (
4 Status_UNKNOWN Status = 0
5 Status_ACTIVE Status = 1
6 Status_INACTIVE Status = 2
7)Conclusion
The automatically generated code structure produced by protoc is the backbone for integrating strongly typed data across languages and platforms. With a detailed understanding of what gets generated—whether in the form of structs/messages, utility methods, or registration—engineers can:
- Speed up the development of distributed applications without writing serialization boilerplate
- Ensure compatibility between services and application versions
- Easily track dependencies and the evolution of the data schema
If you want to extend the capabilities—for example, adding a cross-language gRPC service stub—simply extend the .proto file and run protoc again.
Tips for engineers: Always review the auto-generated file, understand the contract between modules, and make sure you utilize every method available from the generated output.
Additional references:
Happy experimenting, and explore the automatically generated code structure from protoc even more deeply! 🚀