Implementing a Goa Service


Overview

Once the service design is complete, it is time to run the goa tool to generate the code:

goa gen <Go import path of design package>

The goa tool creates a gen directory containing all the generated code and documentation. The generated code follows the clean architecture pattern, with each service in its own package. Additionally the gen directory contains subdirectories for each transport (http and/or grpc):

gen
├── service1
│   ├── client.go       # Service client struct
│   ├── endpoints.go    # Transport agnostic service endpoints
│   └── service.go      # Service interface
├── service2
│   ├── client.go
│   ├── endpoints.go
│   └── service.go
├── ...
├── grpc
│   ├── service1
│   │   ├── client      # gRPC client code
│   │   ├── pb          # Generated gRPC protobuf files
│   │   └── server      # gRPC server code
│   ├── service2
    │   └── ...
│   ├── ...
│   └── cli
└── http
    ├── service1
    │   ├── client      # HTTP client code
    │   └── server      # HTTP server code
    ├── service2
    │   └── ...
    ├── ...
    ├── cli
    │   └── calc
    │       └── cli.go
    ├── openapi.json    # OpenAPI 2 (a.k.a. swagger) specification
    ├── openapi.yaml
    ├── openapi3.json   # OpenAPI 3 specification
    └── openapi3.yaml

Clean Architecture Layers

Before we can dive into the details of how to implement a service it is helpful to understand the layers involved in the clean architecture pattern. For each service Goa generates a transport, an endpoint and a service layers.

Transport Layer

The transport layer takes care of encoding and decoding the requests and responses and validates their content. In the case of HTTP the code generated by Goa leverages encoders and decoders that are provided at runtime making it possible to use different encoders and decoders for different services or even methods. See HTTP Encoding for more information. This layer is implemented by the packages under the http and grpc directories.

Endpoint Layer

The endpoint layer is the glue between the transport and the service layer. It represents each service method using a common Go function signature which makes it possible to implement orthogonal behavior that apply to all methods (a.k.a. transport agnostic middleware). The signature for endpoint methods is:

func (s *Service) Method(ctx context.Context, payload interface{}) (response interface{}, err error)

The endpoint layer is implemented by the endpoints.go file under each service directory.

Service Layer

Finally, the service layer is where the business logic lives. Goa generates the interface for each service and users provide their implementation.

The service layer is implemented by the service.go file under each service.

Putting It All Together

Requests that are received by the underlying HTTP or gRPC server are decoded by the transport layer and passed to the endpoint layer. The endpoint layer then calls the service layer to execute the business logic. The service layer then returns the response to the endpoint layer which is then encoded by the transport layer and sent back to the client.

             TRANSPORT             ENDPOINT            SERVICE

           +-----------+       +--------------+
  Request  | Decoding  |       |  Middleware  |
---------->|    &      +------>|      &       +----------+
           | Validation|       | Type casting |          v
           +-----------+       +--------------+     +----------+
                                                    | Business |
                                                    |  logic   |
           +-----------+       +--------------+     +----+-----+
  Response |           |       |  Middleware  |          |
<----------+ Encoding  |<------+      &       |<---------+
           |           |       | Type casting |
           +-----------+       +--------------+

Note: the transport layers may also add middlewares to the request flow that is not represented in the diagram above.

Implementing Services

Implementing a service consists of implementing the corresponding interface generated by Goa in the file service.go. Given the following service design:

package design

import . "goa.design/goa/v3/dsl"

var _ = Service("calc", func() {
    Method("Add", func() {
        Payload(func() {
            Attribute("a", Int, "First operand")
            Attribute("b", Int, "Second operand")
        })
        Result(Int)
        HTTP(func() {
            GET("/add/{a}/{b}")
        })
        GRPC(func() {})
    })
})

And the following setup:

mkdir calc; cd calc
go mod init calc
mkdir design
# create design/design.go with the content above
goa gen calc/design

Goa generates the following interface in gen/calc/service.go:

type Service interface {
    Add(context.Context, *AddPayload) (res int, err error)
}

A possible implementation could be:

type svc struct {}

func (s *svc) Add(ctx context.Context, p *calcsvc.AddPayload) (int, error) {
	return p.A + p.B, nil
}

The struct implementing the interface can then be used to instantiate the service endpoints using the function NewEndpoints generated in endpoints.go:

func NewEndpoints(s Service) *Endpoints {
	return &Endpoints{
		Add: NewAddEndpoint(s),
	}
}

This function simply wraps the service interface with endpoint methods that can then be provided to the generated transport layer to expose the endpoints to the given transport.

s := &svc{}
endpoints := calc.NewEndpoints(s)

Creating HTTP Servers

For HTTP this is done using the New function generated in the file http/<service>/server/server.go:

func New(
	e *calc.Endpoints,
	mux goahttp.Muxer,
	decoder func(*http.Request) goahttp.Decoder,
	encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
	errhandler func(context.Context, http.ResponseWriter, error),
	formatter func(err error) goahttp.Statuser,
) *Server

Creating a HTTP server requires the service endpoints as well as the HTTP router, decoder and encoder. errhandler is the function called by the generated code when encoding or decoding fails. formatter makes it possible to override how Goa formats errors returned by the service methods prior to encoding. Both can be nil in which case the generated code panics in case of encoding errors (cannot happen with the default Goa encoders) and other errors are formatted using the ServiceError struct.

The Goa http package comes with default implementations for the router, decoder and encoder making it convenient to create HTTP servers.

mux := goahttp.NewMuxer()
dec := goahttp.RequestDecoder
enc := goahttp.ResponseEncoder
svr := calcsvr.New(endpoints, mux, dec, enc, nil, nil)

The final step necessary to configure the HTTP server consists of calling the generated Mount function.

func Mount(mux goahttp.Muxer, h *Server) {
	MountAddHandler(mux, h.Add)
}

The mux object is a standard Go HTTP handler which can be used to serve HTTP requests:

calcsvr.Mount(mux, svr)
s := &http.Server{Handler: mux}
s.ListenAndServe()

The complete code to create the example HTTP service is thus:

package main

import (
	"context"
	"net/http"

	goahttp "goa.design/goa/v3/http"

	"calc/gen/calc"
	"calc/gen/http/calc/server"
)

type svc struct{}

func (s *svc) Add(ctx context.Context, p *calc.AddPayload) (int, error) {
	return p.A + p.B, nil
}

func main() {
	s := &svc{}                                               # Create Service
	endpoints := calc.NewEndpoints(s)                         # Create endpoints
	mux := goahttp.NewMuxer()                                 # Create HTTP muxer
	dec := goahttp.RequestDecoder                             # Set HTTP request decoder           
	enc := goahttp.ResponseEncoder                            # Set HTTP response encoder
	svr := server.New(endpoints, mux, dec, enc, nil, nil)     # Create Goa HTTP server
	server.Mount(mux, svr)                                    # Mount Goa server on mux
	httpsvr := &http.Server{                                  # Create Go HTTP server
        Addr: "localhost:8081",                               # Configure server address
        Handler: mux,                                         # Set request handler
    }
	if err := httpsvr.ListenAndServe(); err != nil {          # Start HTTP server
		panic(err)
	}
}

Note: the code above is meant to help understand how to interface with the generated code and is not intended to be used as is. In particular real code would probably move the business logic to its own package and implement proper error handling.

Creating gRPC Servers

Creating gRPC servers follows a similar pattern to the HTTP servers. The New function generated in gen/grpc/<service>/server/server.go creates the server:

func New(e *calc.Endpoints, uh goagrpc.UnaryHandler) *Server {
	return &Server{
		AddH: NewAddHandler(e.Add, uh),
	}
}

The function accepts the endpoints and an optional gRPC handler that makes it possible to configure gRPC. The default implementation uses the default gRPC options:

func NewAddHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler {
	if h == nil {
		h = goagrpc.NewUnaryHandler(endpoint, DecodeAddRequest, EncodeAddResponse)
	}
	return h
}

Once created the gRPC Goa server is registered against a standard gRPC server using the generated Register<Service>Server function:

svr := server.New(endpoints, nil)
grpcsrv := grpc.NewServer()
calcpb.RegisterCalcServer(grpcsrv, svr)

Starting the gRPC server is done the usual way, for example:

lis, err := net.Listen("tcp", "localhost:8082")
if err != nil {
    panic(err)
}
if err :=  srv.Serve(lis); err != nil {
    panic(err)
}

The complete code to create the example gRPC service is thus:

package main

import (
	"context"
	"net"

	"google.golang.org/grpc"

	"calc/gen/calc"
	calcpb "ca/gen/grpc/calc/pb"
	"calc/gen/grpc/calc/server"
)

type svc struct{}

func (s *svc) Add(ctx context.Context, p *calc.AddPayload) (int, error) {
	return p.A + p.B, nil
}

func main() {
	s := &svc{}
	endpoints := calc.NewEndpoints(s)
	svr := server.New(endpoints, nil)
	grpcsrv := grpc.NewServer()
	calcpb.RegisterCalcServer(grpcsrv, svr)
	lis, err := net.Listen("tcp", "localhost:8082")
	if err != nil {
		panic(err)
	}
	if err := grpcsrv.Serve(lis); err != nil {
		panic(err)
	}
}

A single Goa service may expose both HTTP and gRPC endpoints at the same time. In this case the endpoints struct is created once and shared between the HTTP and gRPC servers.

Overriding Defaults

One of the most important things to understand about the generated code is that it is designed to be completely overridable. The generated service package provides functions that make it possible to create individual endpoints, for example:

gen/calc/endpoints.go

func NewAddEndpoint(s Service) goa.Endpoint {
	return func(ctx context.Context, req interface{}) (interface{}, error) {
		p := req.(*AddPayload)
		return s.Add(ctx, p)
	}
}

The generated NewEndpoints function simply calls each of the individual endpoint creation functions and returns a struct that holds each endpoint reference. User code may override any of the endpoint as the struct fields are public:

type Endpoints struct {
	Add goa.Endpoint
}

Similarly the HTTP and gRPC Goa server objects expose public fields that hold each of the HTTP and gRPC handlers:

HTTP (gen/http/calc/server/server.go):

type Server struct {
	Add http.Handler
    // ...
}

gRPC (gen/grpc/calc/server/server.go):

type Server struct {
	AddH goagrpc.UnaryHandler
	// ...
}

User code may thus override any of the handlers as the fields are public. The New functions that create the servers simply delegate to endpoint specific functions which are public and can be called individually:

HTTP (gen/http/calc/server/server.go):

func NewAddHandler(
	endpoint goa.Endpoint,
	mux goahttp.Muxer,
	decoder func(*http.Request) goahttp.Decoder,
	encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
	errhandler func(context.Context, http.ResponseWriter, error),
	formatter func(err error) goahttp.Statuser,
) http.Handler

gRPC (gen/grpc/calc/server/server.go):

func NewAddHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler

Also with HTTP one can override the HTTP encoder, decoder or even the muxer by providing their own implementation rather than the defaults provided by the Goa HTTP package.