Hi everyone, I started out learning gRPC and created a very basic gRPC server. Although it doesn't do much, it still has a lot of technical stuff going on. So here is a simple breakdown of my attempt at making a gRPC server. I will try to explain everything in simple words and make everything crystal clear.
First of all, let's address the question "What the hell is gRPC ???"
In simple words gRPC is a opensource framework for remote procedure calls (RPC) developed by google. It allows a client to call a function on a server as if it is a local function making it ideal for distributed systems and microservices. It uses protocol buffers which can be serialized into smaller binary format, making it much more efficient and faster than JSON format.It also used HTTP/2 as the transport protocol, enabling faster, persistent connections and features essential for streaming.
Before we move further, there are some prerequisites that you need to be familiar with.
- basics of golang
 - pointers, structs, struct embedding, interfaces
 - context
 - stubs
 - protocol buffers
 
If you don't know these, don't worry; I'll give a small explanation of all these concepts when used.
Let's first start with our file system. We will create a folder named "grpcdemo". Inside this directory,o pen up your terminal and run the following command:
go mod init grpcdemo
It creates a plain text file named "go.mod" in the current directory. This file is the root of your module and contains all the metadata Go needs to manage your project's dependencies.
Here is my folder structure. If you are following this aritcle then make sure to keep the folder structure consistent.
└── 📁grpcdemo
    └── 📁client
        ├── client.go
    └── 📁proto
        ├── greet.proto
    └── 📁server
        ├── server.go
    └── go.mod
We'll go over each one of them one by one starting with the greet.proto.
syntax="proto3";
package pb;
option go_package = "grpcdemo/pb";
service GreetService{
    rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest{
    string name = 1;
}
message HelloResponse{
    string message = 1;
}
Let's first discuss what a proto file is. A .proto file is a simple text file that defines the structure of data you want to serialize and transfer, commonly used in conjunction with Protocol Buffers (protobuf, in short). Protobuf is nothing but a data serialization mechanism. It's language agnostic (independent of the language we are writing our code in). It was developed to serialize data more efficiently across various internal services. Normally we use REST APIs which is fine and relatively easier to implement, but protobufs are preffered in more high performant services. 
First line declares the syntax for proto file since there are versions available.
The second line specifies the package name for the Protocol Buffer definitions contained within the file.
Third line is a Go-specific instruction for the protoc compiler. Its primary purpose is to define the Go package name and import path for the generated files (greet.pb.go and greet_grpc.pb.go).
Protoc is a compiler which compiles the .proto files and generates functional source code from the schema defined in the proto file. It uses plugins for generating the executable code. 
After installing protoc use the following command to generate executable go files from out greet.proto file. Make sure you are in the root directory.
protoc --go_out=../ --go-grpc_out=../ proto/greet.proto
It executes two separate code generation passes, dictated by the two output flags:
- --go_out=../,
 
plugin: protoc-gen-go,
description: Generates files like greet.pb.go.,"Contains the Go                structs (e.g., HelloRequest, HelloResponse) for your data messages, along with serialization and deserialization methods." ../ is the path we pass relative to which our go files will be generated. In this case files will be generated inside pb directory.  
- --go-grpc_out=../,
 
plugin: protoc-gen-go-grpc,
description: Generates files like greet_grpc.pb.go (name may vary).,Contains the Go interfaces (GreetServiceServer) for implementing the service and the client stubs for calling the service.
After running the command, file system should look something like this:
└── 📁grpcdemo
    └── 📁client
        ├── client.go
    └── 📁pb
        ├── greet_grpc.pb.go
        ├── greet.pb.go
    └── 📁proto
        ├── greet.proto
    └── 📁server
        ├── server.go
    ├── go.mod
    └── go.sum
You can see that it generated a new directory pb along with two go files inside. We will use these go files in our server.go and client.go. Now let's discuss about the services and messages in our proto file. 
messages & services are the most important parts of a .proto file since they defined the schema of the data and function we need to serialize.
In messages we define the structure of the data/payload we need to pass between server and client.
Notice the numbers assigned to each field: name = 1; and message = 1;. In Protocol Buffers, these numbers (not the field names) are used to identify the field in the serialized binary data. They are known as field tags. These tags must be unique within the message and must never be changed once your service is in production, as changing them will break backward compatibility with older clients or servers.
In services we define the rpc function signature. This is the function that can be called by the client, in our case it's SayHello.
Now the greet_grpc.pb.go and greet.pb.go files need not to be meddled with since they are automatically generated. If you want to change anything, make the changes in .proto file. 
It's time for server.go file.
package main
import (
    "context"
    "grpcdemo/pb"
    "log"
    "net"
    "google.golang.org/grpc"
    "google.golang.org/protobuf/types/known/emptypb"
)
type Server struct {
    pb.UnimplementedGreetServiceServer
}
func (s *Server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    name := req.GetName()
    result := "Hello " + name + ", Welcome to grpc tutorial !!!"
    res := &pb.HelloResponse{Message: result}
    return res, nil
}
func main() {
    lis, err := net.Listen("tcp", ":12345")
    log.Print("gRPC server is running on port 12345")
    if err != nil {
        log.Fatalf("failed to listen : %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreetServiceServer(s, &Server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("Failed to server: %v", err)
    }
}
There are a bunch of things happening in this code.
type Server struct {
    pb.UnimplementedGreetServiceServer
}
The protocol buffer compiler (protoc) generated an Interface called GreetServiceServer. This interface lists every RPC method defined in your .proto file (currently, just SayHello). To build a valid gRPC server, your Server struct must satisfy this entire interface.
By embedding the UnimplementedGreetServiceServer struct (which provides a default, error-returning implementation for all methods), we accomplish two things:
Initial Satisfaction: You instantly satisfy the entire GreetServiceServer interface without explicitly writing SayHello implementation yet. Doing this would ensure forward compatibility. It's like a default implementation for
GreetServiceServerinterface insidegreet_grpc.pb.gofileThe Override: Your manually written SayHello method overrides the placeholder version, making your implementation the one that actually runs.
(Do check out the greet_grpc.pb.go file to find the GreetServiceServer interface)
func (s *Server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    name := req.GetName()
    result := "Hello " + name + ", Welcome to grpc tutorial !!!"
    res := &pb.HelloResponse{Message: result}
    return res, nil
}
This is the implementation of the SayHello method we defined in the .proto file.
func main() {
    lis, err := net.Listen("tcp", ":12345")
    log.Print("gRPC server is running on port 12345")
    if err != nil {
        log.Fatalf("failed to listen : %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreetServiceServer(s, &Server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("Failed to server: %v", err)
    }
}
Here comes the fun part. The net.Listen function creates a TCP listener (lis) on the specified port (:12345). This step effectively tells the operating system, "I want to reserve this port and listen for incoming connections." If the port is already in use or access is denied, the program exits with an error.
s := grpc.NewServer() line creates an instance of gRPC server which handles all the complex internal tasks of grpc. 
pb.RegisterGreetServiceServer(s, &Server{}) line links/registers our Server implementation to grpc server instance. In other words it tells the grpc server that if any request comes targetting the "GreetService" then redirect that request to our defined Server struct. 
Finally s.Serve(lis) launches our server. It takes the network listener (lis) and begins an infinite loop, continuously accepting new client connections.
Now let's move on to the client implementation. We will make a function call to the SayHello as if it was a local function.
package main
import (
    "context"
    "grpcdemo/pb"
    "log"
    "time"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)
func main() {
    conn, err := grpc.NewClient(
        "localhost:12345",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    defer conn.Close()
    greetClient := pb.NewGreetServiceClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    res, err := greetClient.SayHello(ctx, &pb.HelloRequest{Name: "Billy"})
    if err != nil {
        log.Fatalf("Failed to greet: %v", err)
    }
    log.Printf("Greeting: %s", res.GetMessage())
}
Great !!!, now let's first test the code. Open two terminals. In first one run the server.go file using go run main.go and similarly run client.go file as well. You should see the following output on client terminal.
2025/11/03 22:36:20 Greeting: Hello Billy, Welcome to grpc tutorial !!!
Great Work !!!
Here is a breakdown of the client.go file.  
grpc.NewClient("localhost:12345",grpc.WithTransportCredentials(insecure.NewCredentials()),)
This line creates a persistent connection to the open server running on port ":12345". Since this is a simple implementation, I haven't passed any proper credentials. 
greetClient := pb.NewGreetServiceClient(conn) 
The function pb.NewGreetServiceClient() is generated by the Protocol Buffer compiler. It takes the established network connection (conn) and returns an object (greetClient). All methods called on this stub (like SayHello) are automatically handled by gRPC, taking care of all the complicated underlying networking stuff.
res, err := greetClient.SayHello(ctx, &pb.HelloRequest{Name: "Billy"})
Finally, greetClient is used to call the SayHello method on the client stub. This is what we call an RPC. Print out the response variable to check if everything worked out fine or not.
That's the end of this short project. I definitely got to learn a lot about protocol buffers and how they work with gRPC. The biggest takeaways were realizing how Protocol Buffers define the contract and how the client stub hides the network overhead. Understanding the need for struct embedding to achieve forward compatibility was the critical Go-specific lesson. Please let me know if I missed out something or went wrong somewhere.
Here's the source code for this project. You can play around using this as a base. Try to create more services, more methods inside the GreetService only and interact with the gRPC server.
    
Top comments (0)