1. Introduction2. GoMakesThingsSimple3. Thenet/httppackage4. CreatingaBasicWebApp5. Deployment6. URLRouting7. Middleware8. Rendering
i. JSONii. HTMLTemplatesiii. UsingTherenderpackage
9. Testingi. UnitTestingii. EndtoEndTesting
10. Controllers11. Databases12. TipsandTricks13. MovingForward
TableofContents
BuildingWebAppswithGo
2
WelcometoBuildingWebAppswithGo!Ifyouarereadingthisthenyouhavejuststartedyourjourneyfromnoobtopro.Noseriously,webprogramminginGoissofunandeasythatyouwon'tevennoticehowmuchinformationyouarelearningalongtheway!
Keepinmindthattherearestillportionsofthisbookthatareincompleteandneedsomelove.ThebeautyofopensourcepublishingisthatIcangiveyouanincompletebookanditisstillofvaluetoyou.
Beforewegetintoallthenittygrittydetails,let'sstartwithsomegroundrules:
Tokeepthistutorialsmallandfocused,I'massumingthatyouarepreparedinthefollowingways:
1. YouhaveinstalledtheGoProgrammingLanguage.2. YouhavesetupaGOPATHbyfollowingtheHowtoWriteGoCodetutorial.3. YouaresomewhatfamiliarwiththebasicsofGo.(TheGoTourisaprettygoodplacetostart)4. Youhaveinstalledalltherequiredpackages5. YouhaveinstalledtheHerokuToolbelt6. YouhaveaHerokuaccount
Forthemostpartwewillbeusingthebuiltinpackagesfromthestandardlibrarytobuildoutourwebapps.CertainlessonssuchasDatabases,MiddlewareandURLRoutingwillrequireathirdpartypackage.Hereisalistofallthegopackagesyouwillneedtoinstallbeforestarting:
Name ImportPath Description
httprouter github.com/julienschmidt/httprouter AhighperformanceHTTPrequestrouterthatscaleswell
Negroni github.com/codegangsta/negroni IdiomaticHTTPMiddleware
BlackFriday github.com/russross/blackfriday amarkdownprocessor
Render gopkg.in/unrolled/render.v1 EasyrenderingforJSON,XML,andHTML
SQLite3 github.com/mattn/go-sqlite3 sqlite3driverforgo
Youcaninstall(orupdate)thesepackagesbyrunningthefollowingcommandinyourconsole
goget-u<import_path>
Forinstance,ifyouwishtoinstallNegroni,thefollowingcommandwouldbe:
goget-ugithub.com/codegangsta/negroni
Introduction
Prerequisites
RequiredPackages
BuildingWebAppswithGo
3Introduction
Ifyouhavebuiltawebapplicationbefore,yousurelyknowthattherearequitealotofconceptstokeepinyourhead.HTTP,HTML,CSS,JSON,databases,sessions,cookies,forms,middleware,routingandcontrollersarejustafewamongthemanythingsyourwebappmayneedtointeractwith.
Whileeachoneofthesethingscanbeimportantinthebuildingofyourwebapplications,noteveryoneofthemisimportantforanygivenapp.Forinstance,awebAPImayjustuseJSONasitsserializationformat,thusmakingconceptslikeHTMLnotrelevantforthatparticularwebapp.
TheGocommunityunderstandsthisdilemma.Ratherthanrelyonlarge,heavyweightframeworksthattrytocoverallthebases,Goprogrammerspullinthebarenecessitiestogetthejobdone.Thisminimalistapproachtowebprogrammingmaybeoff-puttingatfirst,buttheresultofthiseffortisamuchsimplerprogramintheend.
Gomakesthingssimple,it'saseasyasthat.Ifwetrainourselvestoalignwiththe"Goway"ofprogrammingfortheweb,wewillendupwithmoresimple,flexible,andmaintainablewebapplications.
Aswegothroughtheexercisesinthisbook,Ithinkyouwillbesurprisedbyhowsimplesomeoftheseprogramscanbewhilststillaffordingabunchoffunctionality.
WhensittingdowntocraftyourownwebapplicationsinGo,thinkhardaboutthecomponentsandconceptsthatyourappwillbefocusedon,andusejustthosepieces.Thisbookwillbecoveringawidearrayofwebtopics,butdonotfeelobligatedtousethemall.InthewordsofourfriendLonestar,"Takeonlywhatyouneedtosurvive".
GoMakesThingsSimple
TheGoWay
PowerinSimplicity
BuildingWebAppswithGo
4GoMakesThingsSimple
BuildingWebAppswithGo
5GoMakesThingsSimple
YouhaveprobablyheardthatGoisfantasticforbuildingwebapplicationsofallshapesandsizes.Thisispartlyduetothefantasticworkthathasbeenputintomakingthestandardlibraryclean,consistent,andeasytouse.
PerhapsoneofthemostimportantpackagesforanybuddingGowebdeveloperisthenet/httppackage.ThispackageallowsyoutobuildHTTPserversinGowithitspowerfulcompositionalconstructs.Beforewestartcoding,let'sdoanextremelyquickoverviewofHTTP.
Whenwetalkaboutbuildingwebapplications,weusuallymeanthatwearebuildingHTTPservers.HTTPisaprotocolthatwasoriginallydesignedtotransportHTMLdocumentsfromaservertoaclientwebbrowser.Today,HTTPisusedtotransportawholelotmorethanHTML.
TheimportantthingtonoticeinthisdiagramisthetwopointsofinteractionbetweentheServerandtheBrowser.TheBrowsermakesanHTTPrequestwithsomeinformation,theServerthenprocessesthatrequestandreturnsaResponse.
Thispatternofrequest-responseisoneofthekeyfocalpointsinbuildingwebapplicationsinGo.Infact,thenet/httppackage'smostimportantpieceisthehttp.HandlerInterface.
AsyoubecomemorefamiliarwithGo,youwillnoticehowmuchofanimpactinterfacesmakeinthedesignofyourprograms.Thenet/httpinterfaceencapsulatestherequest-responsepatterninonemethod:
Thenet/httpPackage
HTTPBasics
Thehttp.HandlerInterface
BuildingWebAppswithGo
6Thenet/httppackage
typeHandlerinterface{
ServeHTTP(ResponseWriter,*Request)
}
Implementorsofthisinterfaceareexpectedtoinspectandprocessdatacomingfromthehttp.Requestobjectandwriteoutaresponsetothehttp.ResponseWriterobject.
Thehttp.ResponseWriterinterfacelookslikethis:
typeResponseWriterinterface{
Header()Header
Write([]byte)(int,error)
WriteHeader(int)
}
Becausemuchofthenet/httppackageisbuiltoffofwelldefinedinterfacetypes,wecan(andareexpectedto)buildourwebapplicationswithcompositioninmind.Eachhttp.Handlerimplementationcanbethoughtofasitsownwebserver.
Manypatternscanbefoundinthatsimplebutpowerfulassumption.Throughoutthisbookwewillcoversomeofthesepatternsandhowwecanusethemtosolverealworldproblems.
Let'ssolvearealworldproblemin1lineofcode.
Mostofthetimepeoplejustneedtoservestaticfiles.MaybeyouhaveastaticHTMLlandingpageandjustwanttoserveupsomeHTML,images,andCSSandcallitaday.Sure,youcouldpullinApacheorPython'sSimpleHTTPServer,butApacheistoomuchforthislittlesiteandSimpleHTTPServeris,well,tooslow.
WewillbeginbycreatinganewprojectinourGOPATH.
cdGOPATH/src
mkdirfileserver&&cdfileserver
Createamain.gowithourtypicalgoboilerplate.
packagemain
import"net/http"
funcmain(){
}
Allweneedtoimportisthenet/httppackageforthistowork.RememberthatthisisallpartofthestandardlibraryinGo.
Let'swriteourfileservercode:
http.ListenAndServe(":8080",http.FileServer(http.Dir(".")))
ComposingWebServices
Exercise:1LineFileServer
BuildingWebAppswithGo
7Thenet/httppackage
Thehttp.ListenAndServefunctionisusedtostarttheserver,itwillbindtotheaddresswegaveit(:8080)andwhenitreceivesanHTTPrequest,itwillhanditofftothehttp.Handlerthatwesupplyasthesecondargument.Inourcaseitisthebuilt-inhttp.FileServer.
Thehttp.FileServerfunctionbuildsanhttp.Handlerthatwillserveanentiredirectoryoffilesandfigureoutwhichfiletoservebasedontherequestpath.WetoldtheFileServertoservethecurrentworkingdirectorywithhttp.Dir(".").
Theentireprogramlookslikethis:
packagemain
import"net/http"
funcmain(){
http.ListenAndServe(":8080",http.FileServer(http.Dir(".")))
}
Let'sbuildandrunourfileserverprogram:
gobuild
./fileserver
Ifwevisitlocalhost:8080/main.goweshouldseethecontentsofourmain.gofileinourwebbrowser.Wecanrunthisprogramfromanydirectoryandservethetreeasastaticfileserver.Allin1lineofGocode.
BuildingWebAppswithGo
8Thenet/httppackage
NowthatwearedonegoingoverthebasicsofHTTP,let'screateasimplebutusefulwebapplicationinGo.
Pullingfromourfileserverprogramthatweimplementedlastchapter,wewillimplementaMarkdowngeneratorusingthegithub.com/russross/blackfridaypackage.
Forstarters,wewillneedabasicHTMLformforthemarkdowninput:
<html>
<head>
<linkhref="/css/bootstrap.min.css"rel="stylesheet">
</head>
<body>
<divclass="container">
<divclass="page-title">
<h1>MarkdownGenerator</h1>
<pclass="lead">GenerateyourmarkdownwithGo</p>
<hr/>
</div>
<formaction="/markdown"method="POST">
<divclass="form-group">
<textareaclass="form-control"name="body"cols="30"rows="10"></textarea>
</div>
<divclass="form-group">
<inputtype="submit"class="btnbtn-primarypull-right"/>
</div>
</form>
</div>
<scriptsrc="/js/bootstrap.min.js"></script>
</body>
</html>
PutthisHTMLintoafilenamedindex.htmlinthe"public"folderofourapplicationandthebootstrap.min.cssfromhttp://getbootstrap.com/inthe"public/css"folder.NoticethattheformmakesanHTTPPOSTtothe"/markdown"endpointofourapplication.Wedon'tactuallyhandlethatrouterightnow,solet'saddit.
Theprogramtohandlethe'/markdown'routeandservethepublicindex.htmlfilelookslikethis:
packagemain
import(
"net/http"
"github.com/russross/blackfriday"
)
funcmain(){
http.HandleFunc("/markdown",GenerateMarkdown)
http.Handle("/",http.FileServer(http.Dir("public")))
http.ListenAndServe(":8080",nil)
}
funcGenerateMarkdown(rwhttp.ResponseWriter,r*http.Request){
CreatingaBasicWebApp
HTMLForm
The"/markdown"route
BuildingWebAppswithGo
9CreatingaBasicWebApp
markdown:=blackfriday.MarkdownCommon([]byte(r.FormValue("body")))
rw.Write(markdown)
}
Let'sbreakitdownintosmallerpiecestogetabetterideaofwhatisgoingon.
http.HandleFunc("/markdown",GenerateMarkdown)
http.Handle("/",http.FileServer(http.Dir("public")))
Weareusingthehttp.HandleFuncandhttp.Handlemethodstodefinesomesimpleroutingforourapplication.Itisimportanttonotethatcallinghttp.Handleonthe"/"patternwillactasacatch-allroute,sowedefinethatroutelast.http.FileServerreturnsanhttp.Handlersoweusehttp.Handletomapapatternstringtoahandler.Thealternativemethod,http.HandleFunc,usesanhttp.HandlerFuncinsteadofanhttp.Handler.Thismaybemoreconvenient,tothinkofhandlingroutesviaafunctioninsteadofanobject.
funcGenerateMarkdown(rwhttp.ResponseWriter,r*http.Request){
markdown:=blackfriday.MarkdownCommon([]byte(r.FormValue("body")))
rw.Write(markdown)
}
OurGenerateMarkdownfunctionimplementsthestandardhttp.HandlerFuncinterfaceandrendersHTMLfromaformfieldcontainingmarkdown-formattedtext.Inthiscase,thecontentisretrievedwithr.FormValue("body").Itisverycommontogetinputfromthehttp.Requestobjectthatthehttp.HandlerFuncreceivesasanargument.Someotherexamplesofinputarether.Header,r.Body,andr.URLmembers.
Wefinalizetherequestbywritingitouttoourhttp.ResponseWriter.Noticethatwedidn'texplicitlysendaresponsecode.Ifwewriteouttotheresponsewithoutacode,thenet/httppackagewillassumethattheresponseisa200OK.Thismeansthatifsomethingdidhappentogowrong,weshouldsettheresponsecodeviatherw.WriteHeader()method.
http.ListenAndServe(":8080",nil)
Thelastbitofthisprogramstartstheserver,wepassnilasourhandler,whichassumesthattheHTTPrequestswillbehandledbythenet/httppackagesdefaulthttp.ServeMux,whichisconfiguredusinghttp.Handleandhttp.HandleFunc,respectively.
AndthatisallyouneedtobeabletogeneratemarkdownasaserviceinGo.Itisasurprisinglysmallamountofcodefortheamountofheavyliftingitdoes.InthenextchapterwewilllearnhowtodeploythisapplicationtothewebusingHeroku.
BuildingWebAppswithGo
10CreatingaBasicWebApp
Herokumakesdeployingapplicationseasy.Itisaperfectplatformforsmalltomediumsizewebapplicationsthatarewillingtosacrificealittlebitofflexibilityininfrastructuretogainafairlypain-freeenvironmentfordeployingandmaintainingwebapplications.
IamchoosingtodeployourwebapplicationtoHerokuforthesakeofthistutorialbecauseinmyexperienceithasbeenthefastestwaytogetawebapplicationupandrunninginnotime.RememberthatthefocusofthistutorialishowtobuildwebapplicationsinGoandnotgettingcaughtupinallofthedistractionofprovisioning,configuring,deploying,andmaintainingthemachinesthatourGocodewillberunon.
Ifyoudon'talreadyhaveaHerokuaccount,signupatid.heroku.com/signup.It'squick,easyandfree.
ApplicationmanagementandconfigurationisdonethroughtheHerokutoolbelt,whichisafreecommandlinetoolmaintainedbyHeroku.WewillbeusingittocreateourapplicationonHeroku.Youcangetitfromtoolbelt.heroku.com.
TomakesuretheapplicationfromourlastchapterwillworkonHeroku,wewillneedtomakeafewchanges.HerokugivesusaPORTenvironmentvariableandexpectsourwebapplicationtobindtoit.Let'sstartbyimportingthe"os"packagesowecangrabthatPORTenvironmentvariable:
import(
"net/http"
"os"
"github.com/russross/blackfriday"
)
Next,weneedtograbthePORTenvironmentvariable,checkifitisset,andifitisweshouldbindtothatinsteadofourhardcodedport(8080).
port:=os.Getenv("PORT")
ifport==""{
port="8080"
}
Lastly,wewanttobindtothatportinourhttp.ListenAndServecall:
http.ListenAndServe(":"+port,nil)
Thefinalcodeshouldlooklikethis:
packagemain
import(
"net/http"
"os"
Deployment
Gettingsetup
ChangingtheCode
BuildingWebAppswithGo
11Deployment
"github.com/russross/blackfriday"
)
funcmain(){
port:=os.Getenv("PORT")
ifport==""{
port="8080"
}
http.HandleFunc("/markdown",GenerateMarkdown)
http.Handle("/",http.FileServer(http.Dir("public")))
http.ListenAndServe(":"+port,nil)
}
funcGenerateMarkdown(rwhttp.ResponseWriter,r*http.Request){
markdown:=blackfriday.MarkdownCommon([]byte(r.FormValue("body")))
rw.Write(markdown)
}
WeneedacouplesmallconfigurationfilestotellHerokuhowitshouldrunourapplication.ThefirstoneistheProcfile,whichallowsustodefinewhichprocessesshouldberunforourapplication.Bydefault,Gowillnametheexecutableafterthecontainingdirectoryofyourmainpackage.Forinstance,ifmywebapplicationlivedinGOPATH/github.com/codegangsta/bwag/deployment,myProcfilewilllooklikethis:
web:deployment
SpecificallytorunGoapplications,weneedtoalsospecifya.godirfiletotellHerokuwhichdirisinfactourpackagedirectory.
deployment
Onceallthesethingsinplace,Herokumakesiteasytodeploy.
InitializetheprojectasaGitrepository:
gitinit
gitadd-A
gitcommit-m"InitialCommit"
CreateyourHerokuapplication(specifyingtheGobuildpack):
herokucreate-bhttps://github.com/kr/heroku-buildpack-go.git
PushittoHerokuandwatchyourapplicationbedeployed!
gitpushherokumaster
Configuration
Deployment
BuildingWebAppswithGo
12Deployment
Viewyourapplicationinyourbrowser:
herokuopen
BuildingWebAppswithGo
13Deployment
Forsomesimpleapplications,thedefaulthttp.ServeMuxcantakeyouprettyfar.IfyouneedmorepowerinhowyouparseURLendpointsandroutethemtotheproperhandler,youmayneedtopullinathirdpartyroutingframework.Forthistutorial,wewillusethepopulargithub.com/julienschmidt/httprouterlibraryasourrouter.github.com/julienschmidt/httprouterisagreatchoiceforarouterasitisaverysimpleimplementationwithoneofthebestperformancebenchmarksoutofallthethirdpartyGorouters.
Inthisexample,wewillcreatesomeroutingforaRESTfulresourcecalled"posts".Belowwedefinemechanismstoviewindex,show,create,update,destroy,andeditposts.
packagemain
import(
"fmt"
"net/http"
"github.com/julienschmidt/httprouter"
)
funcmain(){
r:=httprouter.New()
r.GET("/",HomeHandler)
//Postscollection
r.GET("/posts",PostsIndexHandler)
r.POST("/posts",PostsCreateHandler)
//Postssingular
r.GET("/posts/:id",PostShowHandler)
r.PUT("/posts/:id",PostUpdateHandler)
r.GET("/posts/:id/edit",PostEditHandler)
fmt.Println("Startingserveron:8080")
http.ListenAndServe(":8080",r)
}
funcHomeHandler(rwhttp.ResponseWriter,r*http.Request,phttprouter.Params){
fmt.Fprintln(rw,"Home")
}
funcPostsIndexHandler(rwhttp.ResponseWriter,r*http.Request,phttprouter.Params){
fmt.Fprintln(rw,"postsindex")
}
funcPostsCreateHandler(rwhttp.ResponseWriter,r*http.Request,phttprouter.Params){
fmt.Fprintln(rw,"postscreate")
}
funcPostShowHandler(rwhttp.ResponseWriter,r*http.Request,phttprouter.Params){
id:=p.ByName("id")
fmt.Fprintln(rw,"showingpost",id)
}
funcPostUpdateHandler(rwhttp.ResponseWriter,r*http.Request,phttprouter.Params){
fmt.Fprintln(rw,"postupdate")
}
funcPostDeleteHandler(rwhttp.ResponseWriter,r*http.Request,phttprouter.Params){
fmt.Fprintln(rw,"postdelete")
}
funcPostEditHandler(rwhttp.ResponseWriter,r*http.Request,phttprouter.Params){
fmt.Fprintln(rw,"postedit")
}
URLRouting
BuildingWebAppswithGo
14URLRouting
1. Explorethedocumentationforgithub.com/julienschmidt/httprouter.2. Findouthowwellgithub.com/julienschmidt/httprouterplaysnicelywithexistinghttp.Handlerslikehttp.FileServer3. httprouterhasaverysimpleinterface.Explorewhatkindofabstractionscanbebuiltontopofthisfastroutertomake
buildingthingslikeRESTfulroutingeasier.
Exercises
BuildingWebAppswithGo
15URLRouting
Ifyouhavesomecodethatneedstoberunforeveryrequest,regardlessoftheroutethatitwilleventuallyendupinvoking,youneedsomewaytostackhttp.Handlersontopofeachotherandruntheminsequence.Thisproblemissolvedelegantlythroughmiddlewarepackages.NegroniisapopularmiddlewarepackagethatmakesbuildingandstackingmiddlewareveryeasywhilekeepingthecomposablenatureoftheGowebecosystemintact.
NegronicomeswithsomedefaultmiddlewaresuchasLogging,ErrorRecovery,andStaticfileserving.SooutoftheboxNegroniwillprovideyouwithalotofvaluewithoutalotofoverhead.
TheexamplebelowshowshowtouseaNegronistackwiththebuiltinmiddlewareandhowtocreateyourowncustommiddleware.
packagemain
import(
"log"
"net/http"
"github.com/codegangsta/negroni"
)
funcmain(){
//Middlewarestack
n:=negroni.New(
negroni.NewRecovery(),
negroni.HandlerFunc(MyMiddleware),
negroni.NewLogger(),
negroni.NewStatic(http.Dir("public")),
)
n.Run(":8080")
}
funcMyMiddleware(rwhttp.ResponseWriter,r*http.Request,nexthttp.HandlerFunc){
log.Println("Loggingonthewaythere...")
ifr.URL.Query().Get("password")=="secret123"{
next(rw,r)
}else{
http.Error(rw,"NotAuthorized",401)
}
log.Println("Loggingonthewayback...")
}
1. ThinkofsomecoolmiddlewareideasandtrytoimplementthemusingNegroni.2. ExplorehowNegronicanbecomposedwithgithub.com/gorilla/muxusingthehttp.Handlerinterface.3. PlaywithcreatingNegronistacksforcertaingroupsofroutesinsteadoftheentireapplication.
Middleware
Exercises
BuildingWebAppswithGo
16Middleware
Renderingistheprocessoftakingdatafromyourapplicationordatabaseandpresentingitfortheclient.TheclientcanbeabrowserthatrendersHTML,oritcanbeanotherapplicationthatconsumesJSONasitsserializationformat.InthischapterwewilllearnhowtorenderbothoftheseformatsusingthemethodsthatGoprovidesforusinthestandardlibrary.
Rendering
BuildingWebAppswithGo
17Rendering
JSONisquicklybecomingtheubiquitousserializationformatforwebAPIs,soitmaybethemostrelevantwhenlearninghowtobuildwebappsusingGo.Fortunately,GomakesitsimpletoworkwithJSON--itisextremelyeasytoturnexistingGostructsintoJSONusingtheencoding/jsonpackagefromthestandardlibrary.
packagemain
import(
"encoding/json"
"net/http"
)
typeBookstruct{
Titlestring`json:"title"`
Authorstring`json:"author"`
}
funcmain(){
http.HandleFunc("/",ShowBooks)
http.ListenAndServe(":8080",nil)
}
funcShowBooks(whttp.ResponseWriter,r*http.Request){
book:=Book{"BuildingWebAppswithGo","JeremySaenz"}
js,err:=json.Marshal(book)
iferr!=nil{
http.Error(w,err.Error(),http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type","application/json")
w.Write(js)
}
1. ReadthroughtheJSONAPIdocsandfindouthowtorenameandignorefieldsforJSONserialization.2. Insteadofusingthejson.Marshalmethod,tryusingthejson.EncoderAPI.3. FigureourhowtoprettyprintJSONwiththeencoding/jsonpackage.
JSON
Exercises
BuildingWebAppswithGo
18JSON
ServingHTMLisanimportantjobforsomewebapplications.Gohasoneofmyfavoritetemplatinglanguagestodate.Notforitsfeatures,butforitssimplicityandoutoftheboxsecurity.RenderingHTMLtemplatesisalmostaseasyasrenderingJSONusingthe'html/template'packagefromthestandardlibrary.HereiswhatthesourcecodeforrenderingHTMLtemplateslookslike:
packagemain
import(
"html/template"
"net/http"
"path"
)
typeBookstruct{
Titlestring
Authorstring
}
funcmain(){
http.HandleFunc("/",ShowBooks)
http.ListenAndServe(":8080",nil)
}
funcShowBooks(whttp.ResponseWriter,r*http.Request){
book:=Book{"BuildingWebAppswithGo","JeremySaenz"}
fp:=path.Join("templates","index.html")
tmpl,err:=template.ParseFiles(fp)
iferr!=nil{
http.Error(w,err.Error(),http.StatusInternalServerError)
return
}
iferr:=tmpl.Execute(w,book);err!=nil{
http.Error(w,err.Error(),http.StatusInternalServerError)
}
}
Thisisthefollowingtemplatewewillbeusing.Itshouldbeplacedinatemplates/index.htmlfileinthedirectoryyourprogramisrunfrom:
<html>
<h1>{{.Title}}</h1>
<h3>by{{.Author}}</h3>
</html>
1. Lookthroughthedocsfortext/templateandhtml/templatepackage.Playwiththetemplatinglanguageabittogetafeelforitsgoals,strengths,andweaknesses.
2. Intheexampleweparsethefilesoneveryrequest,whichcanbealotofperformanceoverhead.Experimentwithparsingthefilesatthebeginningofyourprogramandexecutingtheminyourhttp.Handler(hint:makeuseoftheCopy()methodonhtml.Template).
3. Experimentwithparsingandusingmultipletemplates.
HTMLTemplates
Exercises
BuildingWebAppswithGo
19HTMLTemplates
IfyouwantrenderingJSONandHTMLtobeevensimpler,thereisthegithub.com/unrolled/renderpackage.Thispackagewasinspiredbythemartini-contrib/renderpackageandismygotowhenitcomestorenderingdataforpresentationinmywebapplications.
packagemain
import(
"net/http"
"gopkg.in/unrolled/render.v1"
)
funcmain(){
r:=render.New(render.Options{})
mux:=http.NewServeMux()
mux.HandleFunc("/",func(whttp.ResponseWriter,req*http.Request){
w.Write([]byte("Welcome,visitsubpagesnow."))
})
mux.HandleFunc("/data",func(whttp.ResponseWriter,req*http.Request){
r.Data(w,http.StatusOK,[]byte("Somebinarydatahere."))
})
mux.HandleFunc("/json",func(whttp.ResponseWriter,req*http.Request){
r.JSON(w,http.StatusOK,map[string]string{"hello":"json"})
})
mux.HandleFunc("/html",func(whttp.ResponseWriter,req*http.Request){
//Assumesyouhaveatemplatein./templatescalled"example.tmpl"
//$mkdir-ptemplates&&echo"<h1>Hello{{.}}.</h1>">templates/example.tmpl
r.HTML(w,http.StatusOK,"example",nil)
})
http.ListenAndServe(":8080",mux)
}
1. Havefunplayingwithalloftheoptionsavailablewhencallingrender.New()2. Tryusingthe.yieldhelperfunction(withthecurlybraces)andalayoutwithHTMLtemplates.
Usingtherenderpackage
Exercises
BuildingWebAppswithGo
20UsingTherenderpackage
Testingisanimportantpartofanyapplication.TherearetwoapproacheswecantaketotestingGowebapplications.Thefirstapproachisaunit-teststyleapproach.Theotherismoreofanend-to-endapproach.Inthischapterwe'llcoverbothapproaches.
Testing
BuildingWebAppswithGo
21Testing
Unittestingallowsustotestahttp.HandlerFuncdirectlywithoutrunninganymiddleware,routers,oranyothertypeofcodethatmightotherwisewrapthefunction.
packagemain
import(
"fmt"
"net/http"
)
funcHelloWorld(reshttp.ResponseWriter,req*http.Request){
fmt.Fprint(res,"HelloWorld")
}
funcmain(){
http.HandleFunc("/",HelloWorld)
http.ListenAndServe(":3000",nil)
}
Thisisthetestfile.Itshouldbeplacedinthesamedirectoryasyourapplicationandnamemain_test.go.
packagemain
import(
"net/http"
"net/http/httptest"
"testing"
)
funcTest_HelloWorld(t*testing.T){
req,err:=http.NewRequest("GET","http://example.com/foo",nil)
iferr!=nil{
t.Fatal(err)
}
res:=httptest.NewRecorder()
HelloWorld(res,req)
exp:="HelloWorld"
act:=res.Body.String()
ifexp!=act{
t.Fatalf("Expected%sgog%s",exp,act)
}
}
1. ChangetheoutputofHelloWorldtoprintaparameterandthentestthattheparameterisrendered.2. CreateaPOSTrequestandtestthattherequestisproperlyhandled.
UnitTesting
Exercises
BuildingWebAppswithGo
22UnitTesting
Endtoendallowsustotestapplicationsthroughthewholerequestcycle.Whereunittestingismeanttojusttestaparticularfunction,endtoendtestswillrunthemiddleware,router,andotherthatarequestmypassthrough.
packagemain
import(
"fmt"
"net/http"
"github.com/codegangsta/negroni"
"github.com/julienschmidt/httprouter"
)
funcHelloWorld(reshttp.ResponseWriter,req*http.Request,phttprouter.Params){
fmt.Fprint(res,"HelloWorld")
}
funcApp()http.Handler{
n:=negroni.Classic()
m:=func(reshttp.ResponseWriter,req*http.Request,nexthttp.HandlerFunc){
fmt.Fprint(res,"Before...")
next(res,req)
fmt.Fprint(res,"...After")
}
n.Use(negroni.HandlerFunc(m))
r:=httprouter.New()
r.GET("/",HelloWorld)
n.UseHandler(r)
returnn
}
funcmain(){
http.ListenAndServe(":3000",App())
}
Thisisthetestfile.Itshouldbeplacedinthesamedirectoryasyourapplicationandnamemain_test.go.
packagemain
import(
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
funcTest_App(t*testing.T){
ts:=httptest.NewServer(App())
deferts.Close()
res,err:=http.Get(ts.URL)
iferr!=nil{
t.Fatal(err)
}
body,err:=ioutil.ReadAll(res.Body)
res.Body.Close()
iferr!=nil{
t.Fatal(err)
}
exp:="Before...HelloWorld...After"
EndToEndTesting
BuildingWebAppswithGo
23EndtoEndTesting
ifexp!=string(body){
t.Fatalf("Expected%sgot%s",exp,body)
}
}
1. Createanotherpieceofmiddlewarethatmutatesthestatusoftherequest.2. CreateaPOSTrequestandtestthattherequestisproperlyhandled.
Exercises
BuildingWebAppswithGo
24EndtoEndTesting
Controllersareafairlyfamiliartopicinotherwebdevelopmentcommunities.Sincemostwebdevelopersrallyaroundthemightynet/httpinterface,notmanycontrollerimplementationshavecaughtonstrongly.However,thereisgreatbenefitinusingacontrollermodel.Itallowsforclean,welldefinedabstractionsaboveandbeyondwhatthenet/httphandlerinterfacecanaloneprovide.
InthisexamplewewillexperimentwithbuildingourowncontrollerimplementationusingsomestandardfeaturesinGo.Butfirst,letsstartwiththeproblemswearetryingtosolve.Sayweareusingtherenderlibrarythatwetalkedaboutinpreviouschapters:
varRender=render.New(render.Options{})
Ifwewantourhttp.Handlerstobeableaccessourrender.Renderinstance,wehaveacoupleoptions.
1.Useaglobalvariable:Thisisn'ttoobadforsmallprograms,butwhentheprogramgetslargeritquicklybecomesamaintenancenightmare.
2.Passthevariablethroughaclosuretothehttp.Handler:Thisisagreatidea,andweshouldbeusingitmostofthetime.Theimplementationendsuplookinglikethis:
funcMyHandler(r*render.Render)http.Handler{
returnhttp.HandlerFunc(func(rwhttp.ResponseWriter,r*http.Request){
//nowwecanaccessr
})
}
Whenyourprogramgrowsinsize,youwillstarttonoticethatmanyofyourhttp.Handlerswillsharethesamedependenciesandyouwillhavealotoftheseclosurizedhttp.Handlerswiththesamearguments.ThewayIliketocleanthisupistowritealittlebasecontrollerimplementationthataffordsmeafewwins:
1. Allowsmetosharethedependenciesacrosshttp.Handlersthathavesimilargoalsorconcepts.2. Avoidsglobalvariablesandfunctionsforeasytesting/mocking.3. GivesmeamorecentralizedandGo-likemechanismforhandlingerrors.
Thegreatpartaboutcontrollersisthatitgivesusallthesethingswithoutimportinganexternalpackage!MostofthisfunctionalitycomesfromcleveruseoftheGofeatureset,namelyGostructsandembedding.Let'stakealookattheimplementation.
packagemain
import"net/http"
//Actiondefinesastandardfunctionsignatureforustousewhencreating
//controlleractions.Acontrolleractionisbasicallyjustamethodattachedto
//acontroller.
typeActionfunc(rwhttp.ResponseWriter,r*http.Request)error
Controllers
HandlerDependencies
CaseforControllers
BuildingWebAppswithGo
25Controllers
//ThisisourBaseController
typeAppControllerstruct{}
//Theactionfunctionhelpswitherrorhandlinginacontroller
func(c*AppController)Action(aAction)http.Handler{
returnhttp.HandlerFunc(func(rwhttp.ResponseWriter,r*http.Request){
iferr:=a(rw,r);err!=nil{
http.Error(rw,err.Error(),500)
}
})
}
Thatsit!Thatisalltheimplementationthatweneedtohavethepowerofcontrollersatourfingertips.Allwehavelefttodoisimplementanexamplecontroller:
packagemain
import(
"net/http"
"gopkg.in/unrolled/render.v1"
)
typeMyControllerstruct{
AppController
*render.Render
}
func(c*MyController)Index(rwhttp.ResponseWriter,r*http.Request)error{
c.JSON(rw,200,map[string]string{"Hello":"JSON"})
returnnil
}
funcmain(){
c:=&MyController{Render:render.New(render.Options{})}
http.ListenAndServe(":8080",c.Action(c.Index))
}
1. ExtendMyControllertohavemultipleactionsfordifferentroutesinyourapplication.2. Playwithmorecontrollerimplementations,getcreative.3. OverridetheActionmethodonMyControllertorenderaerrorHTMLpage.
Exercises
BuildingWebAppswithGo
26Controllers
OneofthemostaskedquestionsIgetaboutwebdevelopmentinGoishowtoconnecttoaSQLdatabase.Thankfully,GohasafantasticSQLpackageinthestandardlibrarythatallowsustouseawholeslewofdriversfordifferentSQLdatabases.InthisexamplewewillconnecttoaSQLitedatabase,butthesyntax(minussomesmallSQLsemantics)isthesameforaMySQLorPostgreSQLdatabase.
packagemain
import(
"database/sql"
"fmt"
"log"
"net/http"
_"github.com/mattn/go-sqlite3"
)
funcmain(){
db:=NewDB()
log.Println("Listeningon:8080")
http.ListenAndServe(":8080",ShowBooks(db))
}
funcShowBooks(db*sql.DB)http.Handler{
returnhttp.HandlerFunc(func(rwhttp.ResponseWriter,r*http.Request){
vartitle,authorstring
err:=db.QueryRow("selecttitle,authorfrombooks").Scan(&title,&author)
iferr!=nil{
panic(err)
}
fmt.Fprintf(rw,"Thefirstbookis'%s'by'%s'",title,author)
})
}
funcNewDB()*sql.DB{
db,err:=sql.Open("sqlite3","example.sqlite")
iferr!=nil{
panic(err)
}
_,err=db.Exec("createtableifnotexistsbooks(titletext,authortext)")
iferr!=nil{
panic(err)
}
returndb
}
1. MakeuseoftheQueryfunctiononoursql.DBinstancetoextractacollectionofrowsandmapthemtostructs.2. AddtheabilitytoinsertnewrecordsintoourdatabasebyusinganHTMLform.3. gogetgithub.com/jmoiron/sqlxandobservetheimprovementsmadeovertheexistingdatabase/sqlpackageinthe
standardlibrary.
Databases
Exercises
BuildingWebAppswithGo
27Databases
Sometimesyouwanttopassdatatoahttp.HandlerFunconinitialization.Thiscaneasilybedonebycreatingaclosureofthehttp.HandlerFunc:
funcMyHandler(database*sql.DB)http.Handler{
returnhttp.HandlerFunc(func(rwhttp.ResponseWriter,r*http.Request){
//younowhaveaccesstothe*sql.DBhere
})
}
ItisprettyoftenthatweneedtostoreandretrievedatathatisspecifictothecurrentHTTPrequest.Usegorilla/contexttomapvaluesandretrievethemlater.Itcontainsaglobalmutexonamapofrequestobjects.
funcMyHandler(whttp.ResponseWriter,r*http.Request){
val:=context.Get(r,"myKey")
//returns("bar",true)
val,ok:=context.GetOk(r,"myKey")
//...
}
TipsandTricks
Wrapahttp.HandlerFuncclosure
Usinggorilla/contextforrequest-specificdata
BuildingWebAppswithGo
28TipsandTricks
You'vedoneit!YouhavegottenatasteofGowebdevelopmenttoolsandlibraries.Atthetimeofthiswriting,thisbookisstillinflux.ThissectionisreservedformoreGowebresourcestocontinueyourlearning.
MovingForward
BuildingWebAppswithGo
29MovingForward