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:
- Implements a traditional grid view of data calling into MongoDB
- Provides a modal dialog box to view details using a partial view to generate the HTML
- Implements a web service that returns a JSON document
- Takes configuration parameters from the environment using envconfig
- Implements tests via goconvey
- Leverages my logging package
The code for the sample can be found in the GoingGo repository up on Github:
https://github.com/goinggo/beego-mgoYou 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 StructureLet'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 ServicesThese 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 PackageThe 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 PackageThe 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 ServiceThis 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 CallsWith the base types, boilerplate code and service functionality in place, we can now implement the web calls.
Buoy ControllerThe 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:"stationId" valid:"Required; MinSize(4)" error:"invalid_station_id"`
}{}
if this.ParseAndValidate(¶ms) == 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:":stationId" valid:"Required; MinSize(4)" error:"invalid_station_id"`
}{}
if this.ParseAndValidate(¶ms) == 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 EndpointIn 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:"station_id"`
Name string `json:"name"`
LocDesc string `json:"location_desc"`
Condition struct {
Type string `json:"type"`
Coordinates []float64 `json:"coordinates"`
} `json:"condition"`
Location struct {
WindSpeed float64 `json:"wind_speed_milehour"`
WindDirection int `json:"wind_direction_degnorth"`
WindGust float64 `json:"gust_wind_speed_milehour"`
} `json:"location"`
}{}
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)
ConclusionTake 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.