Go and gRPC: How to Create and Use gRPC APIs from Scratch
Hey everyone!
gRPC is one of the most powerful technologies for building modern APIs. Excellent performance. Strong typing. Native streaming. Used by Google, Netflix, Uber, and other giants.
But where to start? How to create a gRPC API from scratch in Go?
This post is a practical guide to get you started with gRPC. From Protocol Buffers to working APIs in production.
Want to learn even more? On my YouTube channel there’s a complete playlist teaching everything about gRPC:
What you’ll find here
This guide covers everything you need to get started with gRPC in Go:
- What is gRPC and why use it: fundamental concepts
- Protocol Buffers: defining your API
- Creating a gRPC server: implementation in Go
- Creating a gRPC client: consuming the API
- Streaming: real-time communication
- Errors and status codes: error handling
Each section has practical examples and concepts you need to understand.
1. What is gRPC and why use it
What is gRPC
gRPC (gRPC Remote Procedure Calls) is a framework for service-to-service communication. Unlike REST, gRPC uses:
- Protocol Buffers for serialization (more efficient than JSON)
- HTTP/2 for transport (multiplexing, streaming)
- Strong typing (defined contracts)
- Automatic code generation
Why use gRPC
Performance: Protocol Buffers is faster and smaller than JSON
- Fewer bytes transferred
- Faster serialization/deserialization
- Ideal for high-traffic microservices
Strong typing: Well-defined contracts
- Compile-time errors
- Automatic documentation
- Controlled versioning
Native streaming: Bidirectional communication
- Real-time chat
- Push notifications
- Stream processing
Multi-language: Same contract, multiple languages
- Go, Python, Java, Node.js, etc.
- Single contract (
.proto)
When to use gRPC
Use when:
- Communication between internal microservices
- Performance is critical
- Need streaming
- Control both sides (client and server)
Don’t use when:
- Public APIs for browsers (don’t natively support gRPC)
- Integration with legacy systems
- Simple APIs that REST solves
2. Protocol Buffers: defining your API
What are Protocol Buffers
Protocol Buffers (protobuf) is a language for defining API contracts. You write a .proto file that defines:
- Messages (data structures)
- Services (methods/endpoints)
- Types and fields
Basic example
Let’s create a simple user API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";
package user;
option go_package = "github.com/your-username/proto/user";
// Request message
message GetUserRequest {
string user_id = 1;
}
// Response message
message User {
string id = 1;
string name = 2;
string email = 3;
}
// Service (API)
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
Important concepts
syntax = “proto3”: Protocol Buffers version (proto3 is the latest)
package: Namespace to avoid conflicts
message: Data structure (like structs in Go)
service: API interface (like methods)
rpc: Remote method (like REST endpoints)
Field numbers: Each field has a unique number (1, 2, 3…). Never change existing field numbers!
Installing tools
To generate Go code from .proto:
1
2
3
4
5
6
7
# Install protoc (compiler)
# Linux/Mac
brew install protobuf # or apt-get install protobuf-compiler
# Install Go plugin
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Generating Go code
With the .proto ready, generate the code:
1
protoc --go_out=. --go-grpc_out=. user.proto
This generates:
user.pb.go: message codeuser_grpc.pb.go: server and client code
3. Creating a gRPC server
Basic structure
A gRPC server in Go needs:
- Implement the generated interface
- Create the gRPC server
- Register the service
- Listen on a port
Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/your-username/proto/user"
)
// Server that implements UserService
type server struct {
pb.UnimplementedUserServiceServer
}
// Implements GetUser method
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
// Here you fetch the user (DB, cache, etc)
user := &pb.User{
Id: req.UserId,
Name: "John Doe",
Email: "john@example.com",
}
return user, nil
}
func main() {
// Create listener on port 50051
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Create gRPC server
s := grpc.NewServer()
// Register the service
pb.RegisterUserServiceServer(s, &server{})
log.Println("Server running on port 50051")
// Start the server
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Important concepts
UnimplementedUserServiceServer: Always include this for future compatibility
Context: Always receive context.Context as first parameter
Errors: Return status.Error for appropriate gRPC errors
4. Creating a gRPC client
Connecting to the server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "github.com/your-username/proto/user"
)
func main() {
// Connect to server
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
// Create client
client := pb.NewUserServiceClient(conn)
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Call the method
user, err := client.GetUser(ctx, &pb.GetUserRequest{
UserId: "123",
})
if err != nil {
log.Fatalf("error fetching user: %v", err)
}
log.Printf("User: %+v", user)
}
Important concepts
grpc.Dial: Creates connection to server
NewUserServiceClient: Automatically generated client
Context with timeout: Always use timeout to avoid hangs
insecure.NewCredentials: For development. In production, use TLS!
5. Streaming: real-time communication
Types of streaming
gRPC supports three types of streaming:
Server Streaming: Server sends multiple responses
1
rpc ListUsers(ListUsersRequest) returns (stream User);
Client Streaming: Client sends multiple requests
1
rpc CreateUsers(stream User) returns (CreateUsersResponse);
Bidirectional Streaming: Bidirectional communication
1
rpc Chat(stream Message) returns (stream Message);
Example: Server Streaming
Protocol Buffers:
1
2
3
service UserService {
rpc ListUsers(ListUsersRequest) returns (stream User);
}
Server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
users := []*pb.User{
{Id: "1", Name: "John", Email: "john@example.com"},
{Id: "2", Name: "Jane", Email: "jane@example.com"},
}
for _, user := range users {
if err := stream.Send(user); err != nil {
return err
}
}
return nil
}
Client:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{})
if err != nil {
log.Fatalf("error: %v", err)
}
for {
user, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("error: %v", err)
}
log.Printf("User: %+v", user)
}
When to use streaming
- Real-time notifications: Server streaming
- Large file uploads: Client streaming
- Chat/messages: Bidirectional streaming
- Large data processing: Server streaming
6. Errors and status codes
gRPC status codes
gRPC uses specific status codes:
OK: SuccessINVALID_ARGUMENT: Invalid parametersNOT_FOUND: Resource not foundALREADY_EXISTS: Resource already existsPERMISSION_DENIED: No permissionUNAUTHENTICATED: Not authenticatedRESOURCE_EXHAUSTED: Rate limitINTERNAL: Internal errorUNAVAILABLE: Service unavailableDEADLINE_EXCEEDED: Timeout
Returning errors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "google.golang.org/grpc/status"
import "google.golang.org/grpc/codes"
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.UserId == "" {
return nil, status.Error(codes.InvalidArgument, "user_id is required")
}
user, err := s.db.GetUser(req.UserId)
if err == ErrNotFound {
return nil, status.Error(codes.NotFound, "user not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "error fetching user")
}
return user, nil
}
Checking errors on client
1
2
3
4
5
6
7
8
9
10
11
12
13
user, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "123"})
if err != nil {
st := status.Convert(err)
switch st.Code() {
case codes.NotFound:
log.Println("User not found")
case codes.InvalidArgument:
log.Println("Invalid parameter")
default:
log.Printf("Error: %v", err)
}
return
}
Conclusion
gRPC is a powerful technology for building modern APIs. With Go, you have everything you need to get started.
The process is simple:
- Define your contract in
.proto - Generate Go code
- Implement the server
- Create clients
Start simple. Add complexity as needed. And always test in production carefully.
It’s worth the effort.
References and sources
Official documentation
- gRPC Documentation - Complete documentation
- Protocol Buffers - Protocol Buffers guide
- gRPC Go Examples - Official examples
Articles and guides
- gRPC vs REST - Detailed comparison
- gRPC Best Practices - Official best practices
Tools
- buf - Modern tool for Protocol Buffers
- grpc-gateway - REST gateway for gRPC
- protoc-gen-validate - Message validation
