id | title | sidebar_label |
---|---|---|
example |
Usage Example: Haberdasher |
Usage Example |
Let's make the canonical Twirp example service: a Haberdasher
.
The Haberdasher
service makes hats. It has only one RPC method, MakeHat
,
which makes a new hat of a particular size.
By the end of this, we'll run a Haberdasher server and have a client that can send it requests.
There are 5 steps here:
- Write a Protobuf service definition
- Generate code
- Implement the server
- Mount and run the server
- Use the client
Start with the Protobuf
definition file, placed in rpc/haberdasher/service.proto
:
syntax = "proto3";
package twirp.example.haberdasher;
option go_package = "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. They'll also show up in the generated Go code, so they'll show up in Godoc.
To generate code run the protoc
compiler pointed at your service's .proto
files:
$ protoc --proto_path=$GOPATH/src:. --twirp_out=. --go_out=. ./rpc/haberdasher/service.proto
The code is generated in the same directory as the .proto
files.
You should see the generated files next to the service.proto
file:
/rpc
/haberdasher
service.pb.go # auto-generated by protoc-gen-go (for protobuf serialization)
service.proto # original protobuf definition
service.twirp.go # auto-generated by protoc-gen-twirp (servers, clients and interfaces)
If you open the generated .twirp.go
file, you will 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.
Now, our job is to write code that fulfills the Haberdasher
interface. This
will be the "backend" full of logic that we'll be serving.
For example, the implementation could go in
internal/haberdasherserver/server.go
:
package haberdasherserver
import (
"context"
"math/rand"
"github.com/twitchtv/twirp"
pb "github.com/twitchtv/twirpexample/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(4)],
Name: []string{"bowler", "baseball cap", "top hat", "derby"}[rand.Intn(3)],
}, nil
}
This meets the Haberdasher
interface because it implements the MakeHat
method, so we're ready to serve this over Twirp!
To serve our Haberdasher over HTTP, use the auto-generated server constructor
New{{Service}}Server
. For Haberdasher, this is:
func NewHaberdasherServer(svc Haberdasher, hooks *twirp.ServerHooks) TwirpServer
This constructor wraps your interface implementation as an TwirpServer
, which
is a http.Handler
with a few extra bells and whistles.
It's easy to serve a http.Handler
straight away with the standard library.
Just call http.ListenAndServe
. For example, you might write the following in
cmd/server/main.go
:
package main
import (
"net/http"
"github.com/twitchtv/twirpexample/internal/haberdasherserver"
"github.com/twitchtv/twirpexample/rpc/haberdasher"
)
func main() {
server := &haberdasherserver.Server{} // implements Haberdasher interface
twirpHandler := haberdasher.NewHaberdasherServer(server, nil)
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!
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.
The JSONClient is included as reference, to show that a Twirp server can handle JSON requests, but you should really use ProtobufClient (see Protobuf vs JSON).
To use the Haberdasher
service from another Go project, just import the
auto-generated client and then use it. You might do this, in cmd/client/main.go
:
package main
import (
"context"
"net/http"
"os"
"fmt"
"github.com/twitchtv/twirpexample/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!