Post
PT-BR

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:

  1. What is gRPC and why use it: fundamental concepts
  2. Protocol Buffers: defining your API
  3. Creating a gRPC server: implementation in Go
  4. Creating a gRPC client: consuming the API
  5. Streaming: real-time communication
  6. 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 code
  • user_grpc.pb.go: server and client code

3. Creating a gRPC server

Basic structure

A gRPC server in Go needs:

  1. Implement the generated interface
  2. Create the gRPC server
  3. Register the service
  4. 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: Success
  • INVALID_ARGUMENT: Invalid parameters
  • NOT_FOUND: Resource not found
  • ALREADY_EXISTS: Resource already exists
  • PERMISSION_DENIED: No permission
  • UNAUTHENTICATED: Not authenticated
  • RESOURCE_EXHAUSTED: Rate limit
  • INTERNAL: Internal error
  • UNAVAILABLE: Service unavailable
  • DEADLINE_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:

  1. Define your contract in .proto
  2. Generate Go code
  3. Implement the server
  4. Create clients

Start simple. Add complexity as needed. And always test in production carefully.

It’s worth the effort.

References and sources

Official documentation

Articles and guides

Tools

This post is licensed under CC BY 4.0 by the author.