Revel
Building an MVC Web App in Golang

Richard Clayton
Sr. Software Engineer
Next Level Security Systems

@richardclayton http://rclayton.silvrback.com

Agenda

  • What is Go (golang)?
  • Overview of Web Frameworks in Go.
  • Implement a single resource in Revel.

What is Go?

  • Language created by the inventors of C and Unicode (working at Google).
  • Go is a simple, memory-managed language designed to be nearly as performant as native C.
  • Applications compile to platform-specific binaries. No software needs to be installed on the host machine.
  • Emerging language of cloud infrastructure [1].

[1] http://redmonk.com/dberkholz/2014/03/18/go-the-emerging-language-of-cloud-infrastructure/

Language Characteristics

  • Statically typed (though there is some dynamic typing features).  Allows "reflective/meta programming".
  • Garbage-collected.
  • Large standard library (amazingly fast built in web server!)
  • First-class functions.
  • Structures (with inheritance) and interfaces.
  • Simple concurrency with message passing (channels).
package main

import "fmt"

func runMeConcurrently(done chan bool) {

  fmt.Println("Hello concurrently running function!")

  done <- true
}

func main() {

  fmt.Println("'Go'ing concurrent!")

  doneSignal := make(chan bool)

  go runMeConcurrently(doneSignal)

  <- doneSignal
}

Written in Go.

Web Frameworks in Go

  • Martini
  • Gorilla Web Toolkit
  • Negroni
  • Beego
  • Revel

Martini

Sinatra-style web framework.  

While probably the most popular because of its easy of use, it's been abandoned because it's not idiomatic Go.

 

  • http://blog.codegangsta.io/blog/2014/05/19/my-thoughts-on-martini/
  • https://stephensearles.com/?p=254

Gorilla

Middleware toolkit similar to Connect on the Node.js platform.

"Use what you want, ignore the rest" philosophy.

 

Great for simple applications, but you have to provide the structure.

Negroni

Middleware toolkit created by Martini's author.

 

Provides simple and idiomatic (to Go) ways of interacting with the net/http library.

 

Once again, you have to provide the structure.

Beego

All-in-one web application framework with it's own ORM.

 

Comprehensive like Rails.  Great if you want structure.  Lots of extra tooling to support testing and hot code reload.

Revel

Another all-in-one framework, but you bring your own ORM (BYOORM).

 

Not as comprehensive as Beego, but it's more flexible.

* Beego has become a lot more comprehensive since I've learned Go.  I need to explore more before I can make a Beego vs. Revel recommendation.

Implement a Resource in Revel

  1. Define REST Routes.
  2. Define Model.
  3. Implement Validation Logic.
  4. Set up the GorpController.
  5. Register Configuration.
  6. Set up the Database.
  7. Extend the GorpController.
  8. Handle JSON Request Body (for Creation and Updates).
  9. Implement Controller CRUD functionality.

1.  Define REST Routes

File: <app-root>/conf/routes

GET     /user/:id  UserCtrl.Get
POST    /user      UserCtrl.Add
PUT     /user/:id  UserCtrl.Update
DELETE  /user/:id  UserCtrl.Delete
GET     /users     UserCtrl.List

2.  Define Model

File: <app-root>/app/models/user.go

package models

type User struct {
  Id        int64  `db:"id" json:"id"`
  FirstName string `db:"first_name" json:"first_name"`
  LastName  string `db:"last_name" json:"last_name"`
  Email     string `db:"email" json:"email"`
}

3.  Implement Validation Logic

File: <app-root>/app/models/user.go

var emailRegex = regexp.MustCompile("\\w*@\\w+[.]\\w+")

func (user *User) Validate(v *revel.Validation) {
  v.Check(user.FirstName, 
    revel.ValidRequired(), 
    revel.ValidMaxSize(25))
    
  v.Check(user.LastName, 
    revel.ValidRequired(), 
    revel.ValidMaxSize(25))
    
  v.Check(user.Email, 
    revel.ValidMatch(emailRegex))
}

4.  Setup the GorpController

package controllers

import (
    "github.com/coopernurse/gorp"
    "database/sql"
    "github.com/revel/revel"
)

var (
    Dbm *gorp.DbMap
)

type GorpController struct {
    *revel.Controller
    Txn *gorp.Transaction
}

func (c *GorpController) Begin() revel.Result {
    txn, err := Dbm.Begin()
    if err != nil {
        panic(err)
    }
    c.Txn = txn
    return nil
}

func (c *GorpController) Commit() revel.Result {
    if c.Txn == nil {
        return nil
    }
    if err := c.Txn.Commit(); err != nil && err != sql.ErrTxDone {
        panic(err)
    }
    c.Txn = nil
    return nil
}

func (c *GorpController) Rollback() revel.Result {
    if c.Txn == nil {
        return nil
    }
    if err := c.Txn.Rollback(); err != nil && err != sql.ErrTxDone {
        panic(err)
    }
    c.Txn = nil
    return nil
}

File: <app-root>/app/controllers/gorp.go

4.  Setup the GorpController (cont.)

package controllers

import "github.com/revel/revel"

func init(){
    revel.InterceptMethod((*GorpController).Begin, revel.BEFORE)
    revel.InterceptMethod((*GorpController).Commit, revel.AFTER)
    revel.InterceptMethod((*GorpController).Rollback, revel.FINALLY)
}

File: <app-root>/app/controllers/init.go

5.  Register Configuration

[dev]
db.user = myapp
db.password = password
db.host = 192.168.24.42
db.port = 3306
db.name = myapp

File: <app-root>/conf/app.conf

func getParamString(param string, defaultValue string) string {
    p, found := revel.Config.String(param)
    if !found {
        if defaultValue == "" {
            revel.ERROR.Fatal("Cound not find parameter: " + param)
        } else {
            return defaultValue
        }
    }
    return p
}

File: <app-root>/app/controllers/init.go

5.  Register Configuration (cont.)

6.  Setup the Database

func getConnectionString() string {
    host := getParamString("db.host", "")
    port := getParamString("db.port", "3306")
    user := getParamString("db.user", "")
    pass := getParamString("db.password", "")
    dbname := getParamString("db.name", "myapp")
    protocol := getParamString("db.protocol", "tcp")
    dbargs := getParamString("dbargs", " ")

    if strings.Trim(dbargs, " ") != "" {
        dbargs = "?" + dbargs
    } else {
        dbargs = ""
    }
    return fmt.Sprintf("%s:%s@%s([%s]:%s)/%s%s", 
        user, pass, protocol, host, port, dbname, dbargs)
}

File: <app-root>/app/controllers/init.go

6.  Setup the Database (cont.)

func defineUserTable(dbm *gorp.DbMap) {
  // set "id" as primary key and autoincrement
  t := dbm.AddTable(models.User{}).SetKeys(true, "id")
  // e.g. VARCHAR(25)
  t.ColMap("first_name").SetMaxSize(25)
  t.ColMap("last_name").SetMaxSize(25)
}

File: <app-root>/app/controllers/init.go

6.  Setup the Database (cont.)

var InitDb func() = func() {
  connectionString := getConnectionString()
  if db, err := sql.Open("mysql", connectionString); err != nil {
    revel.ERROR.Fatal(err)
  } else {
    Dbm = &gorp.DbMap{
      Db: db,
      Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}
  }
  // Defines the table for use by GORP
  // This is a function we will create soon.
  defineUserTable(Dbm)
  if err := Dbm.CreateTablesIfNotExists(); err != nil {
    revel.ERROR.Fatal(err)
  }
}

File: <app-root>/app/controllers/init.go

6.  Setup the Database (cont.)

func init() {
  revel.OnAppStart(InitDb)
  revel.InterceptMethod((*GorpController).Begin, revel.BEFORE)
  revel.InterceptMethod((*GorpController).Commit, revel.AFTER)
  revel.InterceptMethod((*GorpController).Rollback, revel.FINALLY)
}

File: <app-root>/app/controllers/init.go

7.  Extend the GorpController

package controllers

import (
  "revel-demo/app/models"
  "github.com/revel/revel"
  "encoding/json"
)

type UserCtrl struct {
  GorpController
}

File: <app-root>/app/controllers/user.go

8.  Handle JSON Request Body

func (c UserCtrl) parseUser() (models.User, error) {
  user := models.User{}
  err := json.NewDecoder(c.Request.Body).Decode(&user)
  return user, err
}

File: <app-root>/app/controllers/user.go

9.  Implement CRUD Functionality

func (c UserCtrl) Add() revel.Result {
  if user, err := c.parseUser(); err != nil {
    return c.RenderText("Unable to parse the User from JSON.")
  } else {
    // Validate the model
    user.Validate(c.Validation)
    if c.Validation.HasErrors() {
      // Do something better here!
      return c.RenderText("You have error with the User.")
    } else {
      if err := c.Txn.Insert(&user); err != nil {
        return c.RenderText(
          "Error inserting record into database!")
      } else {
        return c.RenderJson(user)
      }
    }
  }
}

File: <app-root>/app/controllers/user.go

9.  Implement CRUD Functionality (cont.)

func (c UserCtrl) Get(id int64) revel.Result {
  user := new(models.User)
  err := c.Txn.SelectOne(user, `SELECT * FROM User WHERE id = ?`, id)
  if err != nil {
    return c.RenderText("Error.  User probably doesn't exist.")
  }
  return c.RenderJson(user)
}

File: <app-root>/app/controllers/user.go

9.  Implement CRUD Functionality (cont.)

func (c UserCtrl) List() revel.Result {
  lastId := parseIntOrDefault(c.Params.Get("lid"), -1)
  limit := parseUintOrDefault(c.Params.Get("limit"), uint64(25))
  users, err := c.Txn.Select(models.User{},
    `SELECT * FROM User WHERE id > ? LIMIT ?`, lastId, limit)
  if err != nil {
    return c.RenderText("Error trying to get records from DB.")
  }
  return c.RenderJson(users)
}

File: <app-root>/app/controllers/user.go

9.  Implement CRUD Functionality (cont.)

func (c UserCtrl) Update(id int64) revel.Result {
  user, err := c.parseUser()
  if err != nil {
    return c.RenderText("Unable to parse the User from JSON.")
  }
  // Ensure the Id is set.
  user.Id = id
  success, err := c.Txn.Update(&user)
  if err != nil || success == 0 {
    return c.RenderText("Unable to update user.")
  }
  return c.RenderText("Updated %v", id)
}

File: <app-root>/app/controllers/user.go

9.  Implement CRUD Functionality (cont.)

func (c UserCtrl) Delete(id int64) revel.Result {
  success, err := c.Txn.Delete(&models.User{Id: id})
  if err != nil || success == 0 {
    return c.RenderText("Failed to remove User")
  }
  return c.RenderText("Deleted %v", id)
}

File: <app-root>/app/controllers/user.go

The End

Resources

  • Code:  https://github.com/ProStack/revel-demo
  • Walkthrough:  http://rclayton.silvrback.com/revel-gorp-and-mysql
  • This presentation: http://slides.com/richclayton/revel

Revel: Building an MVC Web App in Golang

By richclayton

Revel: Building an MVC Web App in Golang

  • 4,128