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
- Define REST Routes.
- Define Model.
- Implement Validation Logic.
- Set up the GorpController.
- Register Configuration.
- Set up the Database.
- Extend the GorpController.
- Handle JSON Request Body (for Creation and Updates).
- 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,175