You do not need a framework

use standard or component libraries

26 November 2015

Gediminas Morkevicius

Senior Gopher at, DATA-DOG

You can also identify me by this image

And I work at

Where some of the teams are gophers

Today

If coffee does not help anymore

Take my advice

Instead of looking into the void. Try GO

Lets start with HTTP library

package main

import (
    "fmt"
    "log"
    "net/http"
)

func hello(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello world")
}

func main() {
    http.HandleFunc("/hello", hello)
    http.Handle("/", http.NotFoundHandler())

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Chaining handlers is all you need

func Timing() alice.Constructor {
    return func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
            started := time.Now()
            h.ServeHTTP(w, req)
            log.Printf("%s %s in %s\n", req.Method, req.URL.String(), time.Since(started))
        })
    }
}
func Method(method string) alice.Constructor {
    return func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
            if req.Method != method {
                w.WriteHeader(http.StatusMethodNotAllowed)
                fmt.Fprintf(w, http.StatusText(http.StatusMethodNotAllowed))
                return
            }
            h.ServeHTTP(w, req)
        })
    }
}

Chain middleware

func hello(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello world")
}

func main() {
    http.Handle("/hello", alice.New(Timing(), Method("GET")).ThenFunc(hello))
    http.Handle("/", http.NotFoundHandler())

    log.Fatal(http.ListenAndServe(":8080", nil))
}

func Timing() alice.Constructor {

You can create and chain any interface, http.Handler in this case

Use powerful router only if you need to

func product(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "category - %s product %s!\n", ps.ByName("cid"), ps.ByName("pid"))
}

func main() {
    router := httprouter.New()
    router.GET("/category/:cid/product/:pid", product)

    http.Handle("/hello", alice.New(Timing(), Method("GET")).ThenFunc(hello))
    http.Handle("/", alice.New(Timing()).Then(router))

    log.Fatal(http.ListenAndServe(":8080", nil))
}

func Timing() alice.Constructor {

Because, complex algorithms are best with huge n, and in most cases n is small.
Your APIs usually top 5-50 routes, not more.

You can predefine middleware chain

var Standard = alice.New(Timing())

And use or extend it:

func main() {
    router := httprouter.New()
    router.GET("/category/:cid/product/:pid", product)

    http.Handle("/hello", Standard.Append(Method("GET")).ThenFunc(hello))
    http.Handle("/", Standard.Then(router))

    log.Fatal(http.ListenAndServe(":8080", nil))
}

You can customize your handler to manage errors

type ErrorHandler func(w http.ResponseWriter, req *http.Request) error

func HandleErr(h ErrorHandler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        if err := h(w, req); err != nil {
            w.WriteHeader(http.StatusBadRequest)
            fmt.Fprintf(w, http.StatusText(http.StatusBadRequest)+" - "+err.Error())
        }
    })
}
func hello(w http.ResponseWriter, req *http.Request) error {
    if errMsg := req.URL.Query().Get("err"); len(errMsg) > 0 {
        return fmt.Errorf(errMsg)
    }
    fmt.Fprintf(w, "hello world\n")
    return nil
}

You can make a custom error type

type HTTPError struct {
    statusCode int
    message    string
}

func NewHTTPError(code int, msg string) *HTTPError {
    return &HTTPError{code, msg}
}

func (e *HTTPError) Error() string {
    return e.message
}

The hello handler now looks like:

func hello(w http.ResponseWriter, req *http.Request) error {
    switch req.URL.Query().Get("err") {
    case "400":
        return NewHTTPError(400, "some 400 error")
    case "500":
        return NewHTTPError(500, "unexpected error")
    }
    fmt.Fprintf(w, "hello world\n")
    return nil
}

Add error to JSON transformation

func (e *HTTPError) JSON(w http.ResponseWriter) error {
    w.Header().Set("Content-Type", "application/json")

    body := struct {
        Status    int    `json:"status"`
        Error     string `json:"error"`
        Message   string `json:"message"`
        Timestamp string `json:"timestamp"`
    }{
        Status:    e.statusCode,
        Error:     http.StatusText(e.statusCode),
        Message:   e.message,
        Timestamp: time.Now().UTC().Format(time.RFC3339),
    }

    response, err := json.Marshal(body)
    if err != nil {
        return err
    }
    w.WriteHeader(e.statusCode)
    _, err = w.Write(response)
    return err
}

And update your error handler wrapper to check interface type

func HandleErr(h ErrorHandler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        if err := h(w, req); err != nil {
            switch t := err.(type) {
            case *HTTPError:
                t.JSON(w)
            default:
                w.WriteHeader(http.StatusInternalServerError)
                fmt.Fprintf(w, t.Error())
            }
        }
    })
}

I think we made a new "REST" framework, didn't we?

What is next on our white board?

People are so used to frameworks, they just need one

We can chain http.Client too

type Client interface {
    Do(*http.Request) (*http.Response, error)
}

type ClientFunc func(*http.Request) (*http.Response, error)

func (f ClientFunc) Do(r *http.Request) (*http.Response, error) {
    return f(r)
}

type Chainable func(Client) Client

type Chain []Chainable

func New(ch ...Chainable) Chain {
    return Chain(ch)
}

func (c Chain) Then(cli Client) Client {
    final := cli
    for i := len(c) - 1; i >= 0; i-- {
        final = c[i](final)
    }
    return final
}

Add a simple middleware

func Timing() Chainable {
    return func(cli Client) Client {
        return ClientFunc(func(r *http.Request) (*http.Response, error) {
            start := time.Now()
            resp, err := cli.Do(r)
            log.Printf("%s - %s took: %s\n", r.Method, r.URL.String(), time.Since(start))
            return resp, err
        })
    }
}

Et voilĂ 

func main() {
    req, _ := http.NewRequest("GET", "http://localhost:8080/hello", nil)
    New(Timing()).Then(http.DefaultClient).Do(req)
}

We can also do the same with io.Writer

func Leveled(levels map[string]int, minLevel string) Chainable {
    lowest := levels[minLevel]
    return func(w io.Writer) io.Writer {
        return WriterFunc(func(b []byte) (int, error) {
            if start := bytes.IndexByte(b, byte('[')); start != -1 {
                if end := bytes.IndexByte(b, byte(']')); end != -1 {
                    if level, ok := levels[string(b[start+1:end])]; ok {
                        if level >= lowest {
                            return w.Write(b)
                        }
                        return 0, nil
                    }
                }
            }
            return w.Write(b)
        })
    }
}

The same chainable interface, but io.Writer instead

Lets see leveled error log

func main() {
    levels := map[string]int{"DEBUG": 0, "ERROR": 1}
    log.SetOutput(New(Leveled(levels, "ERROR")).Then(os.Stdout))

    log.Println("without level message")
    log.Println("[DEBUG] debug message")
    log.Println("[ERROR] error message")
}

So ...

There is so much controversion?

- I think people are just afraid of what they do not understand or not used to

Lets see actual numbers

What about last 90 days

What does it say about PHP?

It says that

They do not spend time learning PHP on weekends, because it is so boring dude

Or maybe these are needle - haystack victims

Anyway - Go brings the fun back

It is easy to write pipes

func main() {
    var records [][]string
    if err := json.NewDecoder(os.Stdin).Decode(&records); err != nil {
        log.Fatalln(err)
    }
    w := csv.NewWriter(os.Stdout)
    for _, record := range records {
        if err := w.Write(record); err != nil {
            log.Fatalln(err)
        }
    }
    w.Flush()
}

Transforms json to csv

[
  ["gopher", "go"],
  ["elephant", "php"],
  ["haskel", "?"]
]

Lets try it out

Testing

If you love Behat

It feels like at home with Godog

Did you know

That you can extend types of package in your test files?

func (e *HTTPError) shouldMatchJSON(expects interface{}, t *testing.T) {
    expected, err := json.Marshal(expects)
    if err != nil {
        t.Fatal(err)
    }

    recorder := httptest.NewRecorder()
    if err := e.JSON(recorder); err != nil {
        t.Fatal(err)
    }

    if bytes.Compare(recorder.Body.Bytes(), expected) != 0 {
        t.Fatalf("does not match")
    }
}

Those assertion libraries are not that useful

And running a test

func TestShouldMatch(t *testing.T) {
    expecting := struct {
        Status    int    `json:"status"`
        Error     string `json:"error"`
        Message   string `json:"message"`
        Timestamp string `json:"timestamp"`
    }{
        Status:    400,
        Error:     http.StatusText(400),
        Message:   "bad error",
        Timestamp: time.Now().UTC().Format(time.RFC3339),
    }

    err := NewHTTPError(400, "bad error")
    err.shouldMatchJSON(expecting, t)
}

Library packages

A great library - does not have any external dependencies.

Keep you libraries clean and make gophers happy!

Lithuanian gopher user group

Follow the news and next events

And you should really subscribe and follow http://golangweekly.com/
Also, check the archives. And visit #golang channel on twitter.

Thank you

Gediminas Morkevicius

Senior Gopher at, DATA-DOG

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)