Usage Example: Haberdasher
Let's make the canonical Twirp service: a Haberdasher
.
The Haberdasher
service makes hats. It has only one RPC method, MakeHat
,
which makes a new hat of a particular size.
Make sure to Install Protobuf and Twirp before starting.
By the end of this, we'll run a Haberdasher service with a strongly typed client.
There are 5 steps here:
- Write a Protobuf service definition
- Generate code
- Implement the server
- Mount and run the server
- Use the client
Write a Protobuf Service Definition
Start with the proto definition file, placed in rpc/haberdasher/service.proto
:
syntax = "proto3";
package twirp.example.haberdasher;
option go_package = "github.com/example/rpc/haberdasher";
// Haberdasher service makes hats for clients.
service Haberdasher {
// MakeHat produces a hat of mysterious, randomly-selected color!
rpc MakeHat(Size) returns (Hat);
}
// Size of a Hat, in inches.
message Size {
int32 inches = 1; // must be > 0
}
// A Hat is a piece of headwear made by a Haberdasher.
message Hat {
int32 inches = 1;
string color = 2; // anything but "invisible"
string name = 3; // i.e. "bowler"
}
It's a good idea to add comments on your Protobuf file. These files can work as the primary documentation of your API. The comments also show up in the generated Go types.
Generate code
To generate code run the protoc
compiler pointed at your service's .proto
files:
$ protoc --twirp_out=. --go_out=paths=. rpc/haberdasher/service.proto
See Generator Command Line Arguments for details about running the generator.
The code should be generated in the same directory as the .proto
files.
/rpc
/haberdasher
service.proto
service.pb.go # generated by protoc-gen-go
service.twirp.go # generated by protoc-gen-twirp
If you open the generated .twirp.go
file, you should see a Go interface like this:
// A Haberdasher makes hats for clients.
type Haberdasher interface {
// MakeHat produces a hat of mysterious, randomly-selected color!
MakeHat(context.Context, *Size) (*Hat, error)
}
along with code to instantiate clients and servers.
Implement the Server
Now, our job is to write code that fulfills the Haberdasher
interface. This
will be the "backend" logic to handle the requests.
The implementation could go in internal/haberdasherserver/server.go
:
package haberdasherserver
import (
"context"
"math/rand"
"github.com/twitchtv/twirp"
pb "github.com/example/rpc/haberdasher"
)
// Server implements the Haberdasher service
type Server struct {}
func (s *Server) MakeHat(ctx context.Context, size *pb.Size) (hat *pb.Hat, err error) {
if size.Inches <= 0 {
return nil, twirp.InvalidArgumentError("inches", "I can't make a hat that small!")
}
return &pb.Hat{
Inches: size.Inches,
Color: []string{"white", "black", "brown", "red", "blue"}[rand.Intn(5)],
Name: []string{"bowler", "baseball cap", "top hat", "derby"}[rand.Intn(4)],
}, nil
}
Mount and run the server
To serve our Haberdasher over HTTP, use the generated server constructor
New{{Service}}Server
. For Haberdasher, it is:
func NewHaberdasherServer(svc Haberdasher, opts ...interface{}) TwirpServer
This constructor wraps your interface implementation as an TwirpServer
, which
is a http.Handler
with a few extra bells and whistles.
The http.Handler
can be mounted like any other HTTP handler. For example,
using the standard library http.ListenAndServe
method.
In cmd/server/main.go
:
package main
import (
"net/http"
"github.com/example/internal/haberdasherserver"
"github.com/example/rpc/haberdasher"
)
func main() {
server := &haberdasherserver.Server{} // implements Haberdasher interface
twirpHandler := haberdasher.NewHaberdasherServer(server)
http.ListenAndServe(":8080", twirpHandler)
}
If you go run ./cmd/server/main.go
, you'll be running your server at
localhost:8080
. All that's left is to create a client!
Use the Client
Client stubs are automatically generated, hooray!
For each service, there are 2 client constructors:
New{{Service}}ProtobufClient
for Protobuf requests.New{{Service}}JSONClient
for JSON requests.
You should use the ProtobufClient. See Protobuf vs JSON for comparison.
Clients in other languages can also be generated by using the respective
protoc
plugins defined by their languages, for example --twirp_ruby_out.
To use the Haberdasher
service from another Go project, import the
auto-generated client.
For example, in cmd/client/main.go
:
package main
import (
"context"
"net/http"
"os"
"fmt"
"github.com/example/rpc/haberdasher"
)
func main() {
client := haberdasher.NewHaberdasherProtobufClient("http://localhost:8080", &http.Client{})
hat, err := client.MakeHat(context.Background(), &haberdasher.Size{Inches: 12})
if err != nil {
fmt.Printf("oh no: %v", err)
os.Exit(1)
}
fmt.Printf("I have a nice new hat: %+v", hat)
}
If you have the server running in another terminal, try running this client with
go run ./cmd/client/main.go
. Enjoy the new hat!