soapui-5-banner

How to Build a Web Service in 5 Minutes with Go

timer

There are lots of very good Web frameworks for the Go language. In this article, you learn how to use one to create a fully-working Web service, faster than you might have imagined.

The Go Programming Language (Go) has many interesting features, with plentyof third-party packages that make use of them. Today I take you through the journey of implementing a web service for an existing API in Go using the power of one of those libraries… in five minutes.

I got your attention, right?Good.

First, let’s get a few definitions out of the way. A Web service is nothing more than a type of Web server that receives requests, processes them, and return responses. At a high level, everything works exactly the way it does when the same server is serving a webpage; but the client request is usually more complex and the response is not meant to be directly rendered in the browser (both request and response data are usually encoded in some agreed-upon format, such as JSON). This graph shows how it works.

 WebServicesArchitecture

Web Services Architecture (source: Wikipedia)

Simple, right? Another thing that becomes glaringly obvious from this explanation is that you have to learn a lot of acronyms when dealing with Web services.

This article assumes the reader has a passing familiarity with Go, so I do not give many details about the language itself. If you do not know Go at all but are interested, A Tour of Go is a good place to start. You also might like my earlier article, An Introduction to the Go Language: Boldly Going Where No Man Has Ever Gone Before.

Go has an extensive standard library that supports virtually everything one would expect from a systems programming language. The way it is laid out makes it easier for developers to extend it. One example of such package is net/http and it is the basis of what we do today.

To create a Web service, you can simply use the net/http package, which works fine. However, this package was created to generally support HTTP (both servers and clients); HTTP is used mainly as the Web services transport so the higher-level abstraction needs to be implemented by hand.

But there is no need to do that. You can simply use one of the very good Web-related Go third-party packages. Here is a non-extensive list:

  • Gorilla: A Web toolkit that extends the net/http package.
  • Goweb: A framework for Web services, with several advanced  features.
  • Martini: A very nice framework for Web services that makes use of reflection and dependency injection to simplify several aspects of developing them. Martini is a promising newcomer to the Go universe.

For our example, we use Martini to do the heavy lifting. It may not be the most established option but it has the most elegant API from my point of view (and it plays nicely with the net/http package). Martini does most of its stuff under the hood, so you only see hints of its workings in most of the relevant code provided in this article.

With all that out of the way, let’s start.

Imagine you have an existing API without Web support. For our purposes, we take a simple non-Web Guest Book API and make it work as a Web service. We implement our guest book in memory to simplify the example, but even if it was backed by a full fledged database, the amount of work to transform it to a Web service would be the same.

Here is what the API looks like:

// GuestBookEntry represents a single entry in a Guest Book. It contains the
// usual fields.
type GuestBookEntry struct {
        Id      int
        Email   string
        Title   string
        Content string
}
 
// GuestBook represents a Guest Book instance. It holds the associated
// GuestBookEntries.
type GuestBook struct {
        guestBookData []*GuestBookEntry
}
 
// NewGuestBook returns a new empty GuestBook instance.
func NewGuestBook() *GuestBook
 
// AddEntry adds a new GuestBookEntry with the provided data.
func (g *GuestBook) AddEntry(email, title, content string) int
 
// RemoveEntry removes the entry with the given id. Return nil in case of
// success or a specific error in case of failure.
func (g *GuestBook) RemoveEntry(id int) error
 
// GetEntry returns the entry identified by the given id or an error if it can
// not find it.
func (g *GuestBook) GetEntry(id int) (*GuestBookEntry, error)
 
// GetAllEntries returns all non-nil entries in the Guest Book.
func (g *GuestBook) GetAllEntries() []*GuestBookEntry
 
// RemoveAllEntries removes all entries from the Guest Book.
func (g *GuestBook) RemoveAllEntries()
 

We have a struct that represents a single guest book entry, the container for those entries, and methods that act on this container (you can see the complete source code here). As can be seen, there is nothing Web-specific in this API.

Go incorporates the concepts of duck-typing and interfaces. This means that a method can take an interface (which boils down to a list of methods) as a parameter and any types that implement the interface (i.e. has the required methods) can be passed to it. This is how we define our Web service:

// WebService is the interface that should be implemented by types that want to
// provide web services.
type WebService interface {
        // GetPath returns the path to be associated with the service.
        GetPath() string
 
         // WebDelete wraps a DELETE method request. The given params might be
        // empty, in case it was applied to the collection itself (i.e. all
        // entries instead of a single one) or will have a “id” key that will
        // point to the id of the entry being deleted.
        WebDelete(params martini.Params) (int, string)
 
        // WebGet is Just as above, but for the GET method. If params is empty,
        // it returns all the entries in the collection. Otherwise it returns
        // the entry with the id as per the “id” key in params.
        WebGet(params martini.Params) (int, string)
 
        // WebPost wraps the POST method. Again an empty params means that the
        // request should be applied to the collection. A non-empty param will
        // have an “id” key that refers to the entry that should be processed
        // (note this specific case is usually not supported unless each entry
        // is also a collection).
        WebPost(params martini.Params, req *http.Request) (int, string)
}
 

The interface above contains the methods that map to the HTTP methods that are used for Web services. Here we only add support to the two most common methods (GET and POST) and one extra method (DELETE) so we can fully exercise the existing API. Adding support for other methods (PUT, PATCH) is trivial.

We also have a function that registers Web services that looks like this:

// RegisterWebService adds Martini routes to the relevant webservice methods
// based on the path returned by GetPath. Each method is registered once for
// the collection and once for each id in the collection.
func RegisterWebService(webService WebService, classicMartini *martini.ClassicMartini)
 

Any type that implements the WebService interface can be passed to this method (full source code here). The method also takes a ClassicMartini instance pointer as it does most of the work under the hood.

Our Guest Book API does not implement any of these methods so we can not use it as-is, but it is trivial to do so. Here starts our five minutes. (What? You expected the clock to be ticking already?)Any type that implements the WebService interface can be passed to this method (full source code here). The method also takes a ClassicMartini instance pointer as it does most of the work under the hood.

To implement the new methods, we do not even need to change the actual file that implements our API. Instead we simply create a new file in the same package that only contains the new methods being implemented for our GuestBook type. For example, here is what the WebPost method would look like:

// WebPost implements webservice.WebPost.
func (g *GuestBook) WebPost(params martini.Params,
        req *http.Request) (int, string) {
        // Make sure Body is closed when we are done.
        defer req.Body.Close()
 
        // Read request body.
        requestBody, err := ioutil.ReadAll(req.Body)
        if err != nil {
                return http.StatusInternalServerError, “internal error”
        }
 
        if len(params) != 0 {
                // No keys in params. This is not supported.
                return http.StatusMethodNotAllowed, “method not allowed”
        }
 
        // Unmarshal entry sent by the user.
        var guestBookEntry GuestBookEntry
        err = json.Unmarshal(requestBody, &guestBookEntry)
        if err != nil {
                // Could not unmarshal entry.
                return http.StatusBadRequest, “invalid JSON data”
        }
 
        // Add entry provided by the user.
        g.AddEntry(guestBookEntry.Email, guestBookEntry.Title,
                guestBookEntry.Content)
 
        // Everything is fine.
        return http.StatusOK, “new entry created”
}
 

Both parameters our method receives, params and req (which, by the way, is an http.Request pointer from the net/http package), are automatically injected by Martini. Martini is also responsible for calling the correct methods of our Web API based on the method used for the request and the URL used.

Another point worth mentioning is that the encoding/json package in the standard library makes it a breeze to convert JSON to Go structs and vice-versa, removing another item from the list of things you have to worry about when creating a Web service.

That is it! With that (and the other methods; full source here) our service is ready to be used. All that is left to do is write the code to put it all together and actually run the service. In our example, it would be as simple as this:

func main() {
        martiniClassic := martini.Classic()
        guestBook := guestbook.NewGuestBook()
        webservice.RegisterWebService(guestBook, martiniClassic)
        martiniClassic.Run()
}
 

We create a new Martini instance, create a new guest book, register it as a Web service, and start our server to handle requests. To see it working (assuming you have Go installed; if not, you can see how to install it here) just do this at the command line:

go get github.com/brunoga/go-webservice-sample
 

Switch to the directory where the code was fetched and run the server code:

cd $GOPATH/src/github.com/brunoga/go-webservice-sample
go run server.go
 

In case you did not do this yet, the full source code for this example can be found here. This also includes a simple Web service client that allows you to test Web services that accept JSON data. To run it, you can do something like this at the command line.The server starts running and accepts requests on port 3000. Add entry:

go run client.go –request_url=”http://127.0.0.1:3000/guestbook” –request_method=post \
                             –request_data='{“Id”:0,”Email”:”EMAIL”,”Title”:”TITLE”,”Content”:”CONTENT”}’
 

Get single entry:

go run client.go –request_url=”http://127.0.0.1:3000/guestbook/0″
 

Get all entries:

go run client.go –request_url=”http://127.0.0.1:3000/guestbook”
 

Delete single entry:

go run client.go –request_url=”http://127.0.0.1:3000/guestbook/0″ –request_method=delete
 

Delete all entries:

go run client.go –request_url=”http://127.0.0.1:3000/guestbook” –request_method=delete
 

Note that although we now have a fully-working Web service, there are some desirable functionalities that we did not implement. For example, access to the API is not controlled in any way, and anyone can send requests. Martini makes it a breeze to add authentication as a middleware; check its documentation to see how it is done.

The best thing about having a solid framework to work with is that you can focus on writing and perfecting your actual API without having to worry about the details of the Web side of things, making you more productive. Good languages make writing good frameworks easier. In this respect, Go passes with flying colors.

See also:

Collaborator-8-3-CTA-article

subscribe-2

Comments

  1. Thanks for the article.But the code format is all messed up.

    • Bruno Albuquerque says:

      I just noticed that. Thanks for the heads up anyway. I will let people know about it to try to get it fixed.

  2. Thanks for the article and code, very helpful. How can I use curl to send message to the server? I tried curl -v -H “Content-type: application/json” -X POST -d ’{“Id”:0,”Email”:”EMAIL”,”Title”:”TITLE”,”Content”:”CONTENT”}’ http://127.0.0.1:3000/guestbook, but it didn’t work!

    Thanks in advance

  3. I didn’t try very hard, but the repository cod doesn’t work w/ go 1.2.1, print statements peppered about show the errors are not coming from the code, so something is being called improperly. For anyone who wants a debug project, here you go.

  4. web design service says:

    I just noticed that. Thanks for the heads up anyway. I will let people know about it to try to get it fixed.

Speak Your Mind

*