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.