Articles‎ > ‎Coding in Go‎ > ‎

Generic request/response handing

Internal server functionality in Go typically uses one or more goroutines as its backend. This can be done globally in a package or with multiple instances using an own type with methods. In this case a struct containing the server status as well as the channels is used. If the server has only a small external interface with a small number of methods one channel per every different command can be used. This is the most typesafe way. But also produces a higher number of command types to define, one for each command and one for each reply. Additionally the according number of channels has to be declared and instantiated.

package authentication

type SessionKey unit64

type cmdLogin struct {
    login     string
    password  string
    replyChan chan SessionKey
}

type cmdLogout struct { ... }

type cmdChangePassword { ... }

var (
    cmdLoginChan      chan cmdLogin
    cmdLogoutChan     chan cmdLogout
    cmdChangePassword chan cmdChangePassword
)

// Automatically start the backend goroutine.

func init() {
    cmdLoginChan = make(chan cmdLogin)

    go backend()
}

// Make a login, returning a session key and true for a valid login.

func Login(login, password string) (SessionKey, bool) {
    rc := make(chan SessionKey)

    cmdLoginChan <- cmdLogin{login, password, rc}

    sk := <- rc

    if sk == 0 {
        return 0, false
    }

    return sk, true
}

// Backend goroutine.

func backend() {
    for {
        select {
        case cmd := <-cmdLoginChan:
            // Handle login ...

            cmd.replyChan <- sessionKey
        case cmd := <-cmdLogoutChan:
            ...
        case cmd := <-cmdChangePasswordChan:
            ...
        }
    }
}

An alternative way would be the usage of generic request and response types. They have to be implemented once in an own  package and can then be used everywhere for any internal server communication. The payload for each request or response is flexible and can be any kind of data. On the other hand the payload data has to be casted to the expected type. This is a possible source for errors like in all languages using duck typing.

package rrcom

// Request type.

type Request struct {
    subject      string
    payload      map[string]interface{}
    responseChan chan *Response
}

// Create a request.

func Request(subject string, response bool) *Request {
    r := &Request{subject, make(map[string]interface{}), nil}

    if response { r.responseChan = make(chan *Response) }
    
    return r
}

// Set a payload.

func (r *Request) SetPayload(key string, data interface{}) { r.payload[key] = data }

// Get a payload.

func (r *Request) Payload(key string) (interface{}, bool) {
    if data, ok := r.payload[key]; ok {
        return data, true
    }

    return nil, false
}

// Get the subject.

func (r *Request) Subject() string { return r.subject }

// Respond to a request, has to be called by the receiver.

func (r *Request) Respond(response *Response) {
    r.responseChan <- response
}

// Send the request.

func (r *Request) Send(rc chan *Request) {
    rc <- r
}

// Get the response.

func (r *Request) Response() *Response {
    return <-r.responseChan
}

// Send the request and wait for the response

func (r *Request) SendAndWait(rc chan *Request) *Response {
    r.Send(rc)
    
    return r.Response()
}

// Response type.

type Response struct {
    ok      bool
    payload map[string]interface{}
}

// Create a response.

func Response(ok bool) *Response {
    return &Response{ok, make(map[string]interface{})}
}

// Set and get payload like above ...

// Test if the request went ok.

func (r *Response) IsOk() bool { return r.ok }

This construct can be optimized or changed, e.g. through a lazy creation of the payload maps only when needed. Or through using a slice of empty interfaces for the payload, accessing it using an index. But the overall construct stays the same. Now the login of the example above looks like this:

func Login(login, password string) (SessionKey, bool) {
    request := rrcom.Request("login", true)

    request.SetPayload("login", login)
    request.SetPayload("password", password)

    response := request.SendAndWait(requestChan)

    if response.IsOk() {
        sk, ok := response.Payload("sessionKey").(uint64)

        return sk, true
    }

    return 0, false
}

Additionally the server code has to be changed:

func backend() {
    for {
        request := <-requestChan

        switch request.Subject() {
        case "login":
            // Handle login ...

            response := rrcom.Response(true)

            response.SetPayload("sessionKey", sessionKey)
            request.Respond(response)
        default:
            // Handle illegal request ...
        }
    }
}

The default statement helps to handle unknown request subjects.