
Building a gRPC Server in Go
Intro
In this article, we will build a gRPC server in Go from a Protocol Buffers contract, generate the Go bindings, implement a unary RPC, and inspect the running service with grpcurl.
The example stays deliberately small, but the generated contract, server registration, reflection, and transport choices are the same pieces you will meet in a larger gRPC service.
The plan
So this is what we are trying to achieve.
- Generate the
.protoIDL stubs. - Write the business logic for our service methods.
- Spin up a gRPC server on a given port.
In a nutshell, we will be covering the following items on our initial diagram.

💡 As always, all the code samples documentation can be found at: https://github.com/sahansera/go-grpc
Prerequisites
This guide targets Go 1.25 or later and assumes the Go toolchain is installed locally. You also need the Protocol Buffers compiler, protoc. On macOS with Homebrew:
To install Protobuf compiler:
brew install protobufInstall the Go plugins used by protoc, then make sure $(go env GOPATH)/bin is on your PATH:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin"The repository also wraps these plugin installs in make install.
Project structure
There is no universally agreed-upon project structure per se. We will use Go modules and start by initializing a new project. So our business problem is this - we have a bookstore and we want to expose its inventory via an RPC function.
Since this talks about the creation of the server, we will call it bookshop/server
We can create a new folder called server for the server and initialize it by so:
go mod init bookshop/serverIn a later post, we will also work on the client-side of this app and call it bookshop/client
This is what it’s going to look like at the end of this post.

Creating the service definitions with .proto files
In my previous post, we discussed what are Protobufs and how to write one. We will be using the same example which is shown below.
A common pattern to note here is to keep your .proto files in their separate folder, so that use them to generate the server and client stubs.
syntax = "proto3";
option go_package = "pb/inventory";
message Book {
string title = 1;
string author = 2;
int32 page_count = 3;
optional string language = 4;
}
message GetBookListRequest {}
message GetBookListResponse { repeated Book books = 1; }
service Inventory {
rpc GetBookList(GetBookListRequest) returns (GetBookListResponse) {}
}The go_package option tells the Go generator which import path to use for the generated package. Keep it stable once other code imports it, because changing it moves the generated Go API even when the wire contract is unchanged.
💡 You can find a full list of allowed values at google/protobuf/descriptor.proto
Other than that, we have 3 messages to represent a Book entity, a request and a response, respectively. Finally we have a service defined called Inventory which has a RPC named GetBookList which can be called by the clients.
If you need to understand how this is structured, please refer to my previous post 🙏
Generating the stubs
Now that we have the IDL created we can generate the Go stubs for our server. It is a good practice to put it under the make gen command so that we can easily generate them with a single command in the future.
protoc --proto_path=proto proto/*.proto --go_out=. --go-grpc_out=.Once this is done, you will see the generated files under the server/pb folder.

Awesome! 🎉 now we can use these subs in our server to respond to any incoming requests.
Creating the gRPC server
Now, we will create the main.go file to create the server.
...
type server struct {
pb.UnimplementedInventoryServer
}
func (s *server) GetBookList(ctx context.Context, in *pb.GetBookListRequest) (*pb.GetBookListResponse, error) {
return &pb.GetBookListResponse{
Books: getSampleBooks(),
}, nil
}
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
s := grpc.NewServer()
pb.RegisterInventoryServer(s, &server{})
if err := s.Serve(listener); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
...- We first define a struct to represent our server. The reason why we need to embed
pb.UnimplementedInventoryServeris to maintain future compatibility when generating gRPC bindings for Go. You can read more on this in this initial proposal and on README. - As we discussed,
GetBookListcan be called by the clients, and this is where that request will be handled. We have access to context (such as auth tokens, headers etc.) and the request object we defined. - In the
mainmethod, we are creating a listening on TCP port 8080 with thenet.Listenmethod, initialize a new gRPC server instance, register our Inventory service and then start responding to incoming requests.
Interacting with the Server
Usually, when interacting with the HTTP/1.1-like server, we can use cURL to make requests and inspect the responses. However, with gRPC, we can’t do that. (you can make requests to HTTP/2 services, but those won’t be readable). We will be using gRPCurl for that.
Once you have it up and running, you can now interact with the server we just built.
grpcurl -plaintext localhost:8080 Inventory.GetBookList💡 Note: This sample uses plaintext transport only for local development.
-plaintextcontrols transport security; it does not make the binary gRPC response human-readable.grpcurldecodes the response through the service schema.

How do we figure out the endpoints of the service? There are two ways to do this. One is by providing a path to the proto files, while the other option enables reflection through the code.
Enabling reflection
This is a pretty cool feature, where it will give you introspection capabilities to your API.
reflection.Register(s)Reflection is useful for local debugging and internal tooling. For an internet-facing production service, decide deliberately whether callers should be able to enumerate the complete API surface.
Following screenshot displays some of the commands you can use to introspect the API.

grpcurl -plaintext localhost:8080 list
grpcurl -plaintext localhost:8080 describe GetBookListResponseUsing proto files
If you don’t want to to enable it by code we can use the Protobuf files to let gRPCurl know which methods are available. Normally, when a team makes a gRPC service they will make the protobuf files available if you are integrating with them. So, without having to ask them or doing trial-and-error you can use these proto files to introspect what kind of endpoints are available for consumption.
grpcurl -import-path proto -proto bookshop.proto \
-plaintext localhost:8080 listgRPCurl is great if you want to debug your RPC calls if you don’t have your client built yet.
Conclusion
In this article we looked at how a .proto contract becomes generated Go interfaces, how the server registers an implementation, and how grpcurl can inspect and call it. The matching Go gRPC client reuses the same contract and calls this server with a bounded deadline.
Feel free to let me know any feedback or questions. Thanks for reading ✌️