Sample Web Application Using Beego and Mgo

Dec 11, 2013


Introduction
I am very excited about the Beego web framework. I wanted to share with you how I use the framework to build real world web sites and web services. Here is a picture of the sample website the post is going to showcase:


The sample web application:
  1. Implements a traditional grid view of data calling into MongoDB
  2. Provides a modal dialog box to view details using a partial view to generate the HTML
  3. Implements a web service that returns a JSON document
  4. Takes configuration parameters from the environment using envconfig
  5. Implements tests via goconvey
  6. Leverages my logging package
The code for the sample can be found in the GoingGo repository up on Github:
https://github.com/goinggo/beego-mgo

You can bring the code down and run it. It uses a public MongoDB database I created at MongoLab. You will need git and bazaar installed on your system before running go get.

go get github.com/goinggo/beego-mgo

To quickly run or test the web application, use the scripts located in the zscripts folder.

Web Application Code Structure
Let’s take a look at the project structure and the different folders that exist:

controllers Entry point for each Web call. Controllers process the requests.
localize Provides localization support for different languages and cultures
models Models are data structures used by the business and service layers
routes Mappings between URL’s and the controller code that handles those calls.
services Services provide primitive functions for the different services that exist. These could be database or web calls that perform a specific function.
static Resource files such as scripts, stylesheets and images
test Tests that can be run through the go test tool.
utilities Code that supports the web application. Boilerplate and abstraction layers for accessing the database and handling panics.
views Code related to rendering views
zscripts Support scripts to help make it easier to build, run and test the web application

Controllers, Models and Services
These layers make up the bulk of the code that implement the web application. The idea behind the framework is to hide and abstract as much boilerplate code as possible. This is accomplished by implementing a base controller package and a base services package.

Base Controller Package
The base controller package uses composition to abstract default controller behavior required by all controllers:

type (
    BaseController struct {
        beego.Controller
        services.Service
    }
)

func (this *BaseController) Prepare() {
    this.UserId = this.GetString("userId")
    if this.UserId == "" {
        this.UserId = this.GetString(":userId")
    }

    err := this.Service.Prepare()
    if err != nil {
        this.ServeError(err)
        return
    }
}

func (this *BaseController) Finish() {
    defer func() {
        if this.MongoSession != nil {
            mongo.CloseSession(this.UserId, this.MongoSession)
            this.MongoSession = nil
        }
    }()
}

A new type called BaseController is declared with the Beego Controller type and the base Service type embedded directly. This composes the fields and methods of these types directly into the BaseController type and makes them directly accessible through an object of the BaseController type.

Beego Controller framework will execute the Prepare and Finish functions on any Controller object that implements these interfaces. The Prepare function is executed prior to the Controller function being called. These functions will belong to every Controller type by default, allowing this boilerplate code to be implemented once.

Services Package
The Service package maintains state and implements boilerplate code required by all services:

type (
    // Services contains common properties
    Service struct {
        MongoSession *mgo.Session
        UserId string
    }
)

func (this *Service) Prepare() (err error) {
    this.MongoSession, err = mongo.CopyMonotonicSession(this.UserId)
    if err != nil {
        return err
    }

    return err
}

func (this *Service) Finish() (err error) {
    defer helper.CatchPanic(&err, this.UserId, "Service.Finish")

    if this.MongoSession != nil {
        mongo.CloseSession(this.UserId, this.MongoSession)
        this.MongoSession = nil
    }

    return err
}

func (this *Service) DBAction(databaseName string, collectionName string, mongoCall mongo.MongoCall) (err error) {
    return mongo.Execute(this.UserId, this.MongoSession, databaseName, collectionName, mongoCall)
}

In the Service type, the Mongo session and the id of the user is maintained. This version of Prepare handles creating a MongoDB session for use. Finish closes the session which releases the underlying connection back into the pool.  The function DBAction provides an abstraction layer for running MongoDB commands and queries.

Buoy Service
This Buoy Service package implements the calls to MongoDB. Let’s look at the FindStation function that is called by the controller methods:

func FindStation(service *services.Service, stationId string) (buoyStation *buoyModels.BuoyStation, err error) {
    defer helper.CatchPanic(&err, service.UserId, "FindStation")

    queryMap := bson.M{"station_id": stationId}

    buoyStation = &buoyModels.BuoyStation{}
    err = service.DBAction(Config.Database, "buoy_stations",
        func(collection *mgo.Collection) error {
            return collection.Find(queryMap).One(buoyStation)
        })

    if err != nil {
        if strings.Contains(err.Error(), "not found") == false {
            return buoyStation, err
        }

        err = nil
    }

    return buoyStation, err
}

The FindStation function prepares the query and then using the DBAction function to execute the query against MongoDB.

Implementing Web Calls
With the base types, boilerplate code and service functionality in place, we can now implement the web calls.

Buoy Controller
The BuoyController type is composed solely from the BaseController. By composing the BuoyController in this way, it immediately satisfies the Prepare and Finish interfaces and contains all the fields of a Beego Controller.

The controller functions are bound to routes. The routes specify the urls to the different web calls that the application supports. In our sample application we have three routes:

beego.Router("/", &controllers.BuoyController{}, "get:Index")
beego.Router("/buoy/retrievestation", &controllers.BuoyController{}, "post:RetrieveStation")
beego.Router("/buoy/station/:stationId", &controllers.BuoyController{}, "get,post:RetrieveStationJson")

The route specifies a url path, an instance of the controller used to handle the call and the name of the method from the controller to use. A prefix of which verb is accepted can be specified as well.

The Index controller method is used to deliver the initial html to the browser. This will include the javascript, style sheets and anything else needed to get the web application going:

func (this *BuoyController) Index() {
    region := "Gulf Of Mexico"

    buoyStations, err := buoyService.FindRegion(&this.Service, region)
    if err != nil {
        this.ServeError(err)
        return
    }

    this.Data["Stations"] = buoyStations
    this.Layout = "shared/basic-layout.html"
    this.TplNames = "buoy/content.html"
    this.LayoutSections = map[string]string{}
    this.LayoutSections["PageHead"] = "buoy/page-head.html"
    this.LayoutSections["Header"] = "shared/header.html"
    this.LayoutSections["Modal"] = "shared/modal.html"
}

A call is made into the service layer to retrieve the list of regions. Then the slice of stations are passed into the view system. Since this is setting up the initial view of the application, layouts and the template are specified. When the controller method returns, the beego framework will generate the html for the response and deliver it to the browser.


To generate that grid of stations, we need to be able to iterate over the slice of stations. Go templates support iterating over a slice. Here we use the .Stations variable which was passed into the view system:

{{range $index, $val := .Stations}}
<tr>      
  <td><a class="detail" data="{{$val.StationId}}" href="#">{{$val.StationId}}</a></td>
  <td>{{$val.Name}}</td>
  <td>{{$val.LocDesc}}</td>
  <td>{{$val.Condition.DisplayWindSpeed}}</td>
  <td>{{$val.Condition.WindDirection}}</td>
  <td>{{$val.Condition.DisplayWindGust}}</td>
</tr>
{{end}}

Each station id is a link that brings up a modal dialog box with the details for each station. The RetrieveStation controller method generates the html for the modal dialog:

func (this *BuoyController) RetrieveStation() {
    params := struct {
        StationId string form:&quot;stationId&quot; valid:&quot;Required; MinSize(4)&quot; error:&quot;invalid_station_id&quot;
    }{}

    if this.ParseAndValidate(&params) == false {
        return
    }

    buoyStation, err := buoyService.FindStation(&this.Service, params.StationId)
    if err != nil {
        this.ServeError(err)
        return
    }

    this.Data["Station"] = buoyStation
    this.Layout = ""
    this.TplNames = "buoy/pv_station.html"
    view, _ := this.RenderString()

    this.AjaxResponse(0, "SUCCESS", view)
}

RetrieveStation gets the details for the specified station and then uses the view system to generate the html for the dialog box. The partial view is passed back to the requesting ajax call and placed into the browser document:

function ShowDetail(result) {
    try {
        var postData = {};
        postData["stationId"] = $(result).attr(‘data’);

        var service = new ServiceResult();
        service.getJSONData("/buoy/retrievestation",
            postData,
            ShowDetail_Callback,
            Standard_ValidationCallback,
            Standard_ErrorCallback
        );
    }

    catch (e) {
        alert(e);
    }
}

function ShowDetail_Callback() {
    try {
        $(‘#system-modal-title’).html("Buoy Details");
        $(‘#system-modal-content’).html(this.ResultObject);
        $("#systemModal").modal(‘show’);
    }

    catch (e) {
        alert(e);
    }
}

Once the call to modal.(‘show’) is performed, the following modal diaglog appears.


The RetrieveStationJson function implements a web service call that returns a JSON document:

func (this *BuoyController) RetrieveStationJson() {
    params := struct {
        StationId string form:&quot;:stationId&quot; valid:&quot;Required; MinSize(4)&quot; error:&quot;invalid_station_id&quot;
    }{}

    if this.ParseAndValidate(&params) == false {
        return
    }

    buoyStation, err := buoyService.FindStation(&this.Service, params.StationId)
    if err != nil {
        this.ServeError(err)
        return
    }

    this.Data["json"] = &buoyStation
    this.ServeJson()
}

You can see how it calls into the service layer and uses the JSON support to return the response.

Testing The Endpoint
In order to make sure the application is always working, it needs to have tests:

func TestStation(t *testing.T) {
    r, _ := http.NewRequest("GET", "/station/42002", nil)
    w := httptest.NewRecorder()
    beego.BeeApp.Handlers.ServeHTTP(w, r)

    response := struct {
        StationId string json:&quot;station_id&quot;
        Name string json:&quot;name&quot;
        LocDesc string json:&quot;location_desc&quot;
        Condition struct {
            Type string json:&quot;type&quot;
            Coordinates []float64 json:&quot;coordinates&quot;
        } json:&quot;condition&quot;
        Location struct {
            WindSpeed float64 json:&quot;wind_speed_milehour&quot;
            WindDirection int json:&quot;wind_direction_degnorth&quot;
            WindGust float64 json:&quot;gust_wind_speed_milehour&quot;
        } json:&quot;location&quot;
    }{}
    json.Unmarshal(w.Body.Bytes(), &response)

    Convey("Subject: Test Station Endpoint\n", t, func() {
        Convey("Status Code Should Be 200", func() {
            So(w.Code, ShouldEqual, 200)
        })
        Convey("The Result Should Not Be Empty", func() {
            So(w.Body.Len(), ShouldBeGreaterThan, 0)
        })
        Convey("There Should Be A Result For Station 42002", func() {
            So(response.StationId, ShouldEqual, "42002")
        })
    })
}

This test creates a fake call through the Beego handler for the specified route. This is awesome because we don’t need to run the web application to test. By using goconvey we can create tests that produce nice output that is logical and easy to read.

Here is a sample when the test fails:

Subject: Test Station Endpoint

  Status Code Should Be 200 ✘
  The Result Should Not Be Empty ✔
  There Should Be A Result For Station 42002 ✘

Failures:

* /Users/bill/Spaces/Go/Projects/src/github.com/goinggo/beego-mgo/test/endpoints/buoyEndpoints_test.go
Line 35:
Expected: ‘200’
Actual: ‘400’
(Should be equal)

* /Users/bill/Spaces/Go/Projects/src/github.com/goinggo/beego-mgo/test/endpoints/buoyEndpoints_test.go
Line 37:
Expected: ‘0’
Actual: ‘9’
(Should be equal)


3 assertions thus far

— FAIL: TestStation-8 (0.03 seconds)

Here is a sample when it is successful:

Subject: Test Station Endpoint

  Status Code Should Be 200 ✔
  The Result Should Not Be Empty ✔
  There Should Be A Result For Station 42002 ✔

3 assertions thus far

— PASS: TestStation-8 (0.05 seconds)

Conclusion
Take the time to download the project and look around. I have attempted to show you the major points of the sample and how things are put together. The Beego framework makes it easy to implement your own ways to abstract and implement boilerplate code, leverage the go testing harness and run and deploy the code using Go standard mechanisms.


Go Training

We have taught Go to thousands of developers all around the world since 2014. There is no other company that has been doing it longer and our material has proven to help jump start developers 6 to 12 months ahead of their knowledge of Go. We know what knowledge developers need in order to be productive and efficient when writing software in Go.

Our Go, Web and Data Science classes are perfect for both experienced and beginning engineers. We start every class from the beginning and get very detailed about the internals, mechanics, specification, guidelines, best practices and design philosophies. We cover a lot about "if performance matters" with a focus on mechanical sympathy, data oriented design, decoupling and writing production software.

Learn More

To learn about Corporate training events, options and special pricing please contact:

William Kennedy
ArdanLabs (www.ardanlabs.com)
bill@ardanlabs.com