Book Image

.Go Programming Blueprints - Second Edition

By : Mat Ryer
Book Image

.Go Programming Blueprints - Second Edition

By: Mat Ryer

Overview of this book

Go is the language of the Internet age, and the latest version of Go comes with major architectural changes. Implementation of the language, runtime, and libraries has changed significantly. The compiler and runtime are now written entirely in Go. The garbage collector is now concurrent and provides dramatically lower pause times by running in parallel with other Go routines when possible. This book will show you how to leverage all the latest features and much more. This book shows you how to build powerful systems and drops you into real-world situations. You will learn to develop high quality command-line tools that utilize the powerful shell capabilities and perform well using Go's in-built concurrency mechanisms. Scale, performance, and high availability lie at the heart of our projects, and the lessons learned throughout this book will arm you with everything you need to build world-class solutions. You will get a feel for app deployment using Docker and Google App Engine. Each project could form the basis of a start-up, which means they are directly applicable to modern software markets.
Table of Contents (13 chapters)

Modeling a chat room and clients on the server

All users (clients) of our chat application will automatically be placed in one big public room where everyone can chat with everyone else. The room type will be responsible for managing client connections and routing messages in and out, while the client type represents the connection to a single client.

Tip

Go refers to classes as types and instances of those classes as objects.

To manage our web sockets, we are going to use one of the most powerful aspects of the Go community open source third-party packages. Every day, new packages solving real-world problems are released, ready for you to use in your own projects, and they even allow you to add features, report and fix bugs, and get support.

Tip

It is often unwise to reinvent the wheel unless you have a very good reason. So before embarking on building a new package, it is worth searching for any existing projects that might have already solved your very problem. If you find one similar project that doesn't quite satisfy your needs, consider contributing to the project and adding features. Go has a particularly active open source community (remember that Go itself is open source) that is always ready to welcome new faces or avatars.

We are going to use Gorilla Project's websocket package to handle our server-side sockets rather than write our own. If you're curious about how it works, head over to the project home page on GitHub, https://github.com/gorilla/websocket, and browse the open source code.

Modeling the client

Create a new file called client.go alongside main.go in the chat folder and add the following code:

package main  
import ( 
  "github.com/gorilla/websocket" 
) 
// client represents a single chatting user. 
type client struct { 
  // socket is the web socket for this client. 
  socket *websocket.Conn 
  // send is a channel on which messages are sent. 
  send chan []byte 
  // room is the room this client is chatting in. 
  room *room 
} 

In the preceding code, socket will hold a reference to the web socket that will allow us to communicate with the client, and the send field is a buffered channel through which received messages are queued ready to be forwarded to the user's browser (via the socket). The room field will keep a reference to the room that the client is chatting in this is required so that we can forward messages to everyone else in the room.

If you try to build this code, you will notice a few errors. You must ensure that you have called go get to retrieve the websocket package, which is as easy as opening a terminal and typing the following:

go get github.com/gorilla/websocket

Building the code again will yield another error:

./client.go:17 undefined: room

The problem is that we have referred to a room type without defining it anywhere. To make the compiler happy, create a file called room.go and insert the following placeholder code:

package main 
type room struct { 
  // forward is a channel that holds incoming messages 
  // that should be forwarded to the other clients. 
  forward chan []byte 
} 

We will improve this definition later once we know a little more about what our room needs to do, but for now, this will allow us to proceed. Later, the forward channel is what we will use to send the incoming messages to all other clients.

Note

You can think of channels as an in-memory thread-safe message queue where senders pass data and receivers read data in a non-blocking, thread-safe way.

In order for a client to do any work, we must define some methods that will do the actual reading and writing to and from the web socket. Adding the following code to client.go outside (underneath) the client struct will add two methods called read and write to the client type:

func (c *client) read() { 
  defer c.socket.Close() 
  for { 
    _, msg, err := c.socket.ReadMessage() 
    if err != nil { 
      return 
    } 
    c.room.forward <- msg 
  } 
} 
func (c *client) write() { 
  defer c.socket.Close() 
  for msg := range c.send { 
    err := c.socket.WriteMessage(websocket.TextMessage, msg) 
    if err != nil { 
      return 
    } 
  } 
} 

The read method allows our client to read from the socket via the ReadMessage method, continually sending any received messages to the forward channel on the room type. If it encounters an error (such as 'the socket has died'), the loop will break and the socket will be closed. Similarly, the write method continually accepts messages from the send channel writing everything out of the socket via the WriteMessage method. If writing to the socket fails, the for loop is broken and the socket is closed. Build the package again to ensure everything compiles.

Note

In the preceding code, we introduced the defer keyword, which is worth exploring a little. We are asking Go to run c.socket.Close() when the function exits. It's extremely useful for when you need to do some tidying up in a function (such as closing a file or, as in our case, a socket) but aren't sure where the function will exit. As our code grows, if this function has multiple return statements, we won't need to add any more calls to close the socket, because this single defer statement will catch them all.

Some people complain about the performance of using the defer keyword, since it doesn't perform as well as typing the close statement before every exit point in the function. You must weigh up the runtime performance cost against the code maintenance cost and potential bugs that may get introduced if you decide not to use defer. As a general rule of thumb, writing clean and clear code wins; after all, we can always come back and optimize any bits of code we feel is slowing our product down if we are lucky enough to have such success.

Modeling a room

We need a way for clients to join and leave rooms in order to ensure that the c.room.forward <- msg code in the preceding section actually forwards the message to all the clients. To ensure that we are not trying to access the same data at the same time, a sensible approach is to use two channels: one that will add a client to the room and another that will remove it. Let's update our room.go code to look like this:

package main 
type room struct { 
  // forward is a channel that holds incoming messages 
  // that should be forwarded to the other clients. 
  forward chan []byte 
  // join is a channel for clients wishing to join the room. 
  join chan *client 
  // leave is a channel for clients wishing to leave the room. 
  leave chan *client 
  // clients holds all current clients in this room. 
  clients map[*client]bool 
} 

We have added three fields: two channels and a map. The join and leave channels exist simply to allow us to safely add and remove clients from the clients map. If we were to access the map directly, it is possible that two goroutines running concurrently might try to modify the map at the same time, resulting in corrupt memory or unpredictable state.

Concurrency programming using idiomatic Go

Now we get to use an extremely powerful feature of Go's concurrency offerings the select statement. We can use select statements whenever we need to synchronize or modify shared memory, or take different actions depending on the various activities within our channels.

Beneath the room structure, add the following run method that contains three select cases:

func (r *room) run() { 
  for { 
    select { 
    case client := <-r.join: 
      // joining 
      r.clients[client] = true 
    case client := <-r.leave: 
      // leaving 
      delete(r.clients, client) 
      close(client.send) 
    case msg := <-r.forward: 
      // forward message to all clients 
      for client := range r.clients { 
        client.send <- msg 
      } 
    } 
  } 
} 

Although this might seem like a lot of code to digest, once we break it down a little, we will see that it is fairly simple, although extremely powerful. The top for loop indicates that this method will run forever, until the program is terminated. This might seem like a mistake, but remember, if we run this code as a goroutine, it will run in the background, which won't block the rest of our application. The preceding code will keep watching the three channels inside our room: join, leave, and forward. If a message is received on any of those channels, the select statement will run the code for that particular case.

Note

It is important to remember that it will only run one block of case code at a time. This is how we are able to synchronize to ensure that our r.clients map is only ever modified by one thing at a time.

If we receive a message on the join channel, we simply update the r.clients map to keep a reference of the client that has joined the room. Notice that we are setting the value to true. We are using the map more like a slice, but do not have to worry about shrinking the slice as clients come and go through time setting the value to true is just a handy, low-memory way of storing the reference.

If we receive a message on the leave channel, we simply delete the client type from the map, and close its send channel. If we receive a message on the forward channel, we iterate over all the clients and add the message to each client's send channel. Then, the write method of our client type will pick it up and send it down the socket to the browser.

Turning a room into an HTTP handler

Now we are going to turn our room type into an http.Handler type like we did with the template handler earlier. As you will recall, to do this, we must simply add a method called ServeHTTP with the appropriate signature.

Add the following code to the bottom of the room.go file:

const ( 
  socketBufferSize  = 1024 
  messageBufferSize = 256 
)  
var upgrader = &websocket.Upgrader{ReadBufferSize:  socketBufferSize,
  WriteBufferSize: socketBufferSize}  
func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) { 
  socket, err := upgrader.Upgrade(w, req, nil) 
  if err != nil { 
    log.Fatal("ServeHTTP:", err) 
    return 
  } 
  client := &client{ 
    socket: socket, 
    send:   make(chan []byte, messageBufferSize), 
    room:   r, 
  } 
  r.join <- client 
  defer func() { r.leave <- client }() 
  go client.write() 
  client.read() 
} 

The ServeHTTP method means a room can now act as a handler. We will implement it shortly, but first let's have a look at what is going on in this snippet of code.

Tip

If you accessed the chat endpoint in a web browser, you would likely crash the program and see an error like ServeHTTPwebsocket: version != 13. This is because it is intended to be accessed via a web socket rather than a web browser.

In order to use web sockets, we must upgrade the HTTP connection using the websocket.Upgrader type, which is reusable so we need only create one. Then, when a request comes in via the ServeHTTP method, we get the socket by calling the upgrader.Upgrade method. All being well, we then create our client and pass it into the join channel for the current room. We also defer the leaving operation for when the client is finished, which will ensure everything is tidied up after a user goes away.

The write method for the client is then called as a goroutine, as indicated by the three characters at the beginning of the line go (the word go followed by a space character). This tells Go to run the method in a different thread or goroutine.

Note

Compare the amount of code needed to achieve multithreading or concurrency in other languages with the three key presses that achieve it in Go, and you will see why it has become a favorite among system developers.

Finally, we call the read method in the main thread, which will block operations (keeping the connection alive) until it's time to close it. Adding constants at the top of the snippet is a good practice for declaring values that would otherwise be hardcoded throughout the project. As these grow in number, you might consider putting them in a file of their own, or at least at the top of their respective files so they remain easy to read and modify.

Using helper functions to remove complexity

Our room is almost ready to go, although in order for it to be of any use, the channels and map need to be created. As it is, this could be achieved by asking the developer to use the following code to be sure to do this:

r := &room{ 
  forward: make(chan []byte), 
  join:    make(chan *client), 
  leave:   make(chan *client), 
  clients: make(map[*client]bool), 
} 

Another, slightly more elegant, solution is to instead provide a newRoom function that does this for us. This removes the need for others to know about exactly what needs to be done in order for our room to be useful. Underneath the type room struct definition, add this function:

// newRoom makes a new room. 
func newRoom() *room { 
  return &room{ 
    forward: make(chan []byte), 
    join:    make(chan *client), 
    leave:   make(chan *client), 
    clients: make(map[*client]bool), 
  } 
} 

Now the users of our code need only call the newRoom function instead of the more verbose six lines of code.

Creating and using rooms

Let's update our main function in main.go to first create and then run a room for everybody to connect to:

func main() { 
  r := newRoom() 
  http.Handle("/", &templateHandler{filename: "chat.html"}) 
  http.Handle("/room", r) 
  // get the room going 
  go r.run() 
  // start the web server 
  if err := http.ListenAndServe(":8080", nil); err != nil { 
    log.Fatal("ListenAndServe:", err) 
  } 
} 

We are running the room in a separate goroutine (notice the go keyword again) so that the chatting operations occur in the background, allowing our main goroutine to run the web server. Our server is now finished and successfully built, but remains useless without clients to interact with.