RPC in Go using Twitch's Twirp

Author

Gurleen Sethi on 10 November 2022

RPC in Go using Twitch's Twirp
Code Repository
Code realted to this article is stored in this repository.

Getting Started #

Twirp is an RPC framework from Twitch which just like gRPC uses Protobufs and is much easier to use. In this article I am going to give you a taste of Twirp. You can find the complete code for this article in this GitHub repository.

๐Ÿ™‹โ€โ™‚๏ธ This article presumes that you are familiar with Protobufs and the concept of RPC.

Setup Project #

Project creation and installation instructions for protoc tool and twirp/proto.

  1. Create a new Go project.
$ mkdir go-twirp-rpc-example
$ cd go-twirp-rpc-example
$ go mod init github.com/gurleensethi/go-twirp-rpc-example

Create three folders server, client and rpc in the project so that the folder structure looks like this.

go-twirp-rpc-example/
โ”œโ”€ client/
โ”œโ”€ server/
โ”œโ”€ rpc/
   โ”œโ”€ notes/
  1. Install the protoc and twirp-protoc generators (these are cli tools).
$ go install github.com/twitchtv/twirp/[email protected]
$ go install google.golang.org/protobuf/cmd/[email protected]
  1. Install protobuf and twirp modules in project.
$ go get github.com/twitchtv/twirp
$ go get google.golang.org/protobuf

Writing Proto file #

Since Twirp uses Protobufs to generate clients, we need to write a .proto file.

You will be building a simple note taking application that only has two features:

1. Get all notes.
2. Create a new note.

Here is the proto definition model (store it in rpc/notes/service.proto).

syntax = "proto3";

package gotwirprpcexample.rpc.notes;
option go_package = "rpc/notes";

message Note {
    int32 id = 1;
    string text = 2;
    int64 created_at = 3;
}

message CreateNoteParams {
    string text = 1;
}

message GetAllNotesParams {
}

message GetAllNotesResult {
    repeated Note notes = 1;
}

service NotesService {    
    rpc CreateNote(CreateNoteParams) returns (Note);
    rpc GetAllNotes(GetAllNotesParams) returns (GetAllNotesResult);
}
rpc/notes/service.proto

If you don't understand the above code you need to learn about Protobufs here. In hindsight we have defined a Note type and defined two RPC calls CreateNote (to create a new note) GetAllNotes (to get all notes).

Generating Go code #

Now that proto file is in place, let's generate the go client using the protoc generator.

From the root of your project run the protoc command.

$ protoc --twirp_out=. --go_out=. rpc/notes/service.proto

This will generate two files service.pb.go and service.twirp.go in the rpc/notes directory. While not required, it is a good idea to go through the generated code to get a better understanding of what is generated.

Writing Server Code #

Server side of things involves implementing the two functions CreateNote and GetAllNotes.

If you open the generated twirp code rpc/notes/service.twirp.go, you will find an interface NotesService, this is the interface that we will be implementing.

// ======================
// NotesService Interface
// ======================

type NotesService interface {
	CreateNote(context.Context, *CreateNoteParams) (*Note, error)

	GetAllNotes(context.Context, *GetAllNotesParams) (*GetAllNotesResult, error)
}
rpc/notes/service.twirp.go

Implementing functionality #

  1. Define a notesService type that holds a list of notes.
package main

import (
	"github.com/gurleensethi/go-twirp-rpc-example/rpc/notes"
)

type notesService struct {
	Notes     []notes.Note
	CurrentId int32
}

func main() {
}
server/main.go
  1. Implement the CreateNote function on notesService.
package main

import (
	"context"
	"net/http"
	"time"

	"github.com/gurleensethi/go-twirp-rpc-example/rpc/notes"
	"github.com/twitchtv/twirp"
)

...

func (s *notesService) CreateNote(ctx context.Context, params *notes.CreateNoteParams) (*notes.Note, error) {
	if len(params.Text) < 4 {
		return nil, twirp.InvalidArgument.Error("Text should be min 4 characters.")
	}

	note := notes.Note{
		Id:        s.CurrentId,
		Text:      params.Text,
		CreatedAt: time.Now().UnixMilli(),
	}

	s.Notes = append(s.Notes, note)

	s.CurrentId++

	return &note, nil
}

...
server/main.go

Pretty straightforward stuff. One thing to notice is we are returning twirp.InvalidArgument in case the length of text is less than 4. Twirp errors let you easily communicate different states of errors to the client, if you return a regular error Twirp will wrap it in a twirp.InternalError.

I encourage you to read the documentation on Twirp Errors here.

  1. Implement the GetAllNotes function on notesService.
func (s *notesService) GetAllNotes(ctx context.Context, params *notes.GetAllNotesParams) (*notes.GetAllNotesResult, error) {
	allNotes := make([]*notes.Note, 0)

	for _, note := range s.Notes {
		n := note
		allNotes = append(allNotes, &n)
	}

	return &notes.GetAllNotesResult{
		Notes: allNotes,
	}, nil
}
server/main.go

Serving over HTTP #

Running a Twirp server is as easy as serving it using the deafult net/http package ๐Ÿ˜€.

func main() {
	notesServer := notes.NewNotesServiceServer(&notesService{})
    
	mux := http.NewServeMux()
	mux.Handle(notesServer.PathPrefix(), notesServer)

	err := http.ListenAndServe(":8000", notesServer)
	if err != nil {
		panic(err)
	}
}
server/main.go

Run the server using go run server/main.go, its as easy as that. Glance at the complete server code here.

Writing Client Code #

On the client side, let's create a new note and subsequently fetch all the notes.

  1. Prepare the notes service client.
package main

import (
	"net/http"

	"github.com/gurleensethi/go-twirp-rpc-example/rpc/notes"
)

func main() {
	client := notes.NewNotesServiceProtobufClient("http://localhost:8000", &http.Client{})
}
client/main.go
  1. Call the CreateNote function.
package main

import (
	"context"
	"log"
	"net/http"

	"github.com/gurleensethi/go-twirp-rpc-example/rpc/notes"
)

func main() {
	client := notes.NewNotesServiceProtobufClient("http://localhost:8000", &http.Client{})

	ctx := context.Background()

	_, err := client.CreateNote(ctx, &notes.CreateNoteParams{Text: "Hello World"})
	if err != nil {
		log.Fatal(err)
	}
}
client/main.go
  1. List all the notes using GetAllNotes.
func main() {
	client := notes.NewNotesServiceProtobufClient("http://localhost:8000", &http.Client{})

	ctx := context.Background()

	_, err := client.CreateNote(ctx, &notes.CreateNoteParams{Text: "Hello World"})
	if err != nil {
		log.Fatal(err)
	}

	allNotes, err := client.GetAllNotes(ctx, &notes.GetAllNotesParams{})
	if err != nil {
		log.Fatal(err)
	}

	for _, note := range allNotes.Notes {
		log.Println(note)
	}
}
client/main.go

Call RPC via HTTP #

A very lovely thing about Twirp is you can call the functions through simple HTTP calls. Everything underneath Twirp is a POST request.

Request in Insomnia

Go Twirp POST HTTP

cURL Request

โฏ curl --request POST \
  --url http://localhost:8000/twirp/gotwirprpcexample.rpc.notes.NotesService/GetAllNotes \
  --header 'Content-Type: application/json' \
  --data '{}'

{"notes":[{"id":0,"text":"Hello World","created_at":"1668035588211"}]}

Read more about using cURL with Twirp here.

Ending #

That was a quick introduction to Twirp, as next steps take a look at Errors and HTTP Headers.

Twip Documentation.

Table of Contents
Code Repository
Code realted to this article is stored in this repository.
Subscribe to Newsletter

Get notified once/twice per month when new articles are published.

TheDeveloperCafe
Copyright ยฉ 2022 TheDeveloperCafe.
The Go gopher was designed by Renee French.