Post

Write A Simple Grpc Server Go

Write A Simple Grpc Server Go

Write a simple gRPC server in go in 5 minutes

How to spin up a basic gRPC server in Go: Define your service in a .proto file, let protoc do the heavy lifting, implement the server, and hit run. Perfect for beginners—because everyone loves boilerplate, right? 🚀

Here are the steps..

Create your directory

1
mkdir myserver

Initialize your module

1
cd myservergo mod init github.com/user/myserver. 

Create basic structure

1
mkdir internalmkdir cmdtouch cmd/main.go

Add main boilerlplate

package mainfunc main(){}

Cool, now let’s get to the fun part…

Say I’m making a calculator gRPC server with only ints…

1
cd internalmkdir calctouch calc.proto

Write your calc.proto` file like so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syntax = "proto3";

option go_package = "github.com/user/myserver/internal/calc";

package calc;

service CalcService {
  rpc Add(Input) returns (Result);
  rpc Subtract(Input) returns (Result);
  rpc Multiply(Input) returns (Result);
  rpc Divide(Input) returns (Result);
}

message Input {
  int32 a = 1;
  int32 b = 2;
}

message Result {
  int32 c = 1;
}

Now let’s install protoc compiler

Visit https://github.com/protocolbuffers/protobuf/releases in your browser and download the zip file that corresponds to your OS and computer architecture.

Next, unzip the file under $HOME/.local by running the following command, where protoc-24.3-osx-universal_binary.zip is the zip file that corresponds to your OS and computer architecture:

1
unzip protoc-24.3-osx-universal_binary.zip -d $HOME/.local

Now update your environment’s PATH variable to include the path to the protoc executable by adding the following code to your .bash_profile or .zshrc file:

1
export PATH="$PATH:$HOME/.local/bin"

Note: If your .bash_profile or .zshrc file already contains an export path, you can simply append :$HOME/.local/bin.

Now we’re ready for protoc to do it’s magic.

Run the following command from the internal directory

1
protoc --go_out=calc --go_opt=paths=source_relative \    --go-grpc_out=calc --go-grpc_opt=paths=source_relative \    calc.proto

This will generate the pb serializer and stub code under intenal/calc

1
2
3
4
├── calc
│   ├── calc.pb.go
│   └── calc_grpc.pb.go
└── calc.proto

Run go mod tidy for necessary packages to be downloaded

``bash internal ❯ go mod tidy go: finding module for package google.golang.org/grpc go: finding module for package google.golang.org/protobuf/reflect/protoreflect go: finding module for package google.golang.org/grpc/codes go: finding module for package google.golang.org/grpc/status go: finding module for package google.golang.org/protobuf/runtime/protoimpl go: found google.golang.org/grpc in google.golang.org/grpc v1.67.1 go: found google.golang.org/grpc/codes in google.golang.org/grpc v1.67.1 go: found google.golang.org/grpc/status in google.golang.org/grpc v1.67.1 go: found google.golang.org/protobuf/reflect/protoreflect in google.golang.org/protobuf v1.35.1 go: found google.golang.org/protobuf/runtime/protoimpl in google.golang.org/protobuf v1.35.1

1
2
3
4
5
6
7
Now let’s add the `server.go`\`, under `internal`

Import pb

```go
import ( pb "github.com/user/myserver/internal/calc")

Define server type

1
type server struct{  pb.UnimplementedCalcServiceServer}

Implement rpc methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (s *server) Add(ctx context.Context, inp *pb.Input) (*pb.Result, error) {
    return &pb.Result{C: inp.A + inp.B}, nil
}

func (s *server) Subtract(ctx context.Context, inp *pb.Input) (*pb.Result, error) {
    return &pb.Result{C: inp.A - inp.B}, nil
}

func (s *server) Multiply(ctx context.Context, inp *pb.Input) (*pb.Result, error) {
    return &pb.Result{C: inp.A * inp.B}, nil
}

func (s *server) Divide(ctx context.Context, inp *pb.Input) (*pb.Result, error) {
    return &pb.Result{C: inp.A / inp.B}, nil
}

Add server code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func StartServer() {
    ln, err := net.Listen("tcp", ":3000")
    if err != nil {
        log.Fatalf("error listening at port 3000: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterCalcServiceServer(s, &server{})

    log.Printf("gRPC server listening at %v", ln.Addr())

    if err := s.Serve(ln); err != nil {
        log.Fatalf("failed to start gRPC server: %v", err)
    }
}

Here’s it all together: server.go

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
package internal

import (
    "context"
    "log"
    "net"
    pb "github.com/user/myserver/internal/calc"
    "google.golang.org/grpc"
)

type server struct {
    pb.UnimplementedCalcServiceServer
}

func (s *server) Add(ctx context.Context, inp *pb.Input) (*pb.Result, error) {
    return &pb.Result{C: inp.A + inp.B}, nil
}

func (s *server) Subtract(ctx context.Context, inp *pb.Input) (*pb.Result, error) {
    return &pb.Result{C: inp.A - inp.B}, nil
}

func (s *server) Multiply(ctx context.Context, inp *pb.Input) (*pb.Result, error) {
    return &pb.Result{C: inp.A * inp.B}, nil
}

func (s *server) Divide(ctx context.Context, inp *pb.Input) (*pb.Result, error) {
    return &pb.Result{C: inp.A / inp.B}, nil
}

func StartServer() {
    ln, err := net.Listen("tcp", ":3000")
    if err != nil {
        log.Fatalf("error listening at port 3000: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterCalcServiceServer(s, &server{})

    log.Printf("gRPC server listening at %v", ln.Addr())

    if err := s.Serve(ln); err != nil {
        log.Fatalf("failed to start gRPC server: %v", err)
    }
}

Now add this to cmd/main.go

1
2
3
4
5
6
7
package main

import "github.com/user/myserver/internal"

func main() {
    internal.StartServer()
}

That’s it, server is ready

1
2
myserver ❯ go run cmd/main.go
2024/11/02 10:23:36 gRPC server listening at [::]:3000

Now let’s add the client code, internal/client.go like so...

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
49
50
51
52
53
54
55
56
57
package internal

import (
    "context"
    "time"
    pb "github.com/user/myserver/internal/calc"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func Connect() (pb.CalcServiceClient, error) {
    conn, err := grpc.Dial("localhost:3000", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        return nil, err
    }
    return pb.NewCalcServiceClient(conn), nil
}

func Add(client pb.CalcServiceClient, a int32, b int32) (int32, error) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
    defer cancel()
    r, err := client.Add(ctx, &pb.Input{A: a, B: b})
    if err != nil {
        return 0, err
    }
    return r.C, nil
}

func Subtract(client pb.CalcServiceClient, a int32, b int32) (int32, error) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
    defer cancel()
    r, err := client.Subtract(ctx, &pb.Input{A: a, B: b})
    if err != nil {
        return 0, err
    }
    return r.C, nil
}

func Multiply(client pb.CalcServiceClient, a int32, b int32) (int32, error) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
    defer cancel()
    r, err := client.Multiply(ctx, &pb.Input{A: a, B: b})
    if err != nil {
        return 0, err
    }
    return r.C, nil
}

func Divide(client pb.CalcServiceClient, a int32, b int32) (int32, error) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
    defer cancel()
    r, err := client.Divide(ctx, &pb.Input{A: a, B: b})
    if err != nil {
        return 0, err
    }
    return r.C, nil
}

There’s a Connect function and wrappers for each rpc method

Let’s now modify main to use the client functions as well

First, move the server to a seperate goroutine

1
2
3
4
func main() {
    go internal.StartServer()
    select {}
}

Now call the client wrappers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "fmt"
    "log"
    "github.com/user/myserver/internal"
)

func main() {
    go internal.StartServer()

    client, err := internal.Connect()
    if err != nil {
        log.Fatalf("error connecting to server: %v", err)
    }

    fmt.Println(internal.Add(client, 2, 2))
    fmt.Println(internal.Subtract(client, 2, 2))
    fmt.Println(internal.Multiply(client, 2, 2))
    fmt.Println(internal.Divide(client, 2, 2))

    select {}
}

Let’s run it :)

1
2
3
4
5
6
go run cmd/main.go
2024/11/02 10:35:24 gRPC server listening at [::]:3000
4 <nil>
0 <nil>
4 <nil>
1 <nil>

That’s it 😄
You know how to write gRPC servers/clients in go now.

note: this was only unary… you can look at the docs for insights on client/server side streaming… It’s pretty simple with channels.

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