How to build a REST API with Golang using Gin and Gorm

文章推薦指數: 80 %
投票人數:10人

First, let's implement the FindBooks controller. // controllers/books.go package controllers import ( "net ... Share Reply 18 RahmanFadhilFollow Developerandcontentwriter. HowtobuildaRESTAPIwithGolangusingGinandGorm October15,2021 9minread 2594 Editor’snote:Thistutorialwaslastupdatedon1November2021tomakethecodeexamplescompatiblewiththelatestversionofGo(atthetimeofwriting,Go1.17)andaddressquestionsposedinthecommentssection. Goisaverypopularlanguageforgoodreason.Itofferssimilarperformancetoother“low-level”programminglanguagessuchasJavaandC++,butit’salsoincrediblysimple,whichmakesthedevelopmentexperiencedelightful. Whatifwecouldcombineafastprogramminglanguagewithaspeedywebframeworktobuildahigh-performanceRESTfulAPIthatcanhandleacrazyamountoftraffic? Afterdoingalotofresearchtofindafastandreliableframeworkforthisbeast,Icameacrossafantasticopen-sourceprojectcalledGin.Thisframeworkislightweight,well-documented,and,ofcourse,extremelyfast. UnlikeotherGowebframeworks,GinusesacustomversionofHttpRouter,whichmeansitcannavigatethroughyourAPIroutesfasterthanmostframeworksoutthere.Thecreatorsalsoclaimitcanrun40timesfasterthanMartini,arelativelysimilarframeworktoGin.Youcanseeamoredetailedcomparisoninthisbenchmark. However,Ginisamicroframeworkthatdoesn’tcomewithatonoffancyfeaturesoutofthebox.ItonlygivesyoutheessentialtoolstobuildanAPI,suchasrouting,formvalidation,etc.Sofortaskssuchasauthenticatingusers,uploadingfiles,andsendingemails,youneedtoeitherinstallanotherthird-partylibraryorimplementthemyourself. Thismightnotbethebestfitforasmallteamofdevelopersthatneedstoshipalotoffeaturesinaveryshorttime.Anotherwebframework,suchasLaravelandRubyonRails,mightbemoreappropriateforsuchateam.Suchframeworksareopinionated,easiertolearn,andprovidealotoffeaturesoutofthebox,whichenablesyoutodevelopafullyfunctioningwebapplicationinaninstant. Ifyou’repartofasmallteam,thisstackmaybeoverkill.Butifyouhavetheappetitetomakealong-terminvestment,youcanreallytakeadvantageoftheextraordinaryperformanceandflexibilityofGin. BuildingaRESTAPIinGousingGinandGorm Inthistutorial,we’lldemonstratehowtobuildabookstoreRESTAPIthatprovidesbookdataandperformsCRUDoperations. Beforewegetbegin,I’llassumethatyou: Wemadeacustomdemofor.Noreally.Clickheretocheckitout. Clickheretoseethefulldemowithnetworkrequests HaveGoinstalledonyourmachine UnderstandthebasicsofGolanguage HaveageneralunderstandingofRESTfulAPI Let’sstartbyinitializinganewGomoduletomanageourproject’sdependencies.MakesureyourunthiscommandinsideyourGoenvironmentfolder. $gomodinit Nowlet’sinstallGinandGorm. gogetgithub.com/gin-gonic/gingithub.com/jinzhu/gorm Aftertheinstallationiscomplete,yourfoldershouldcontaintwofiles:mod.modandgo.sum.Bothofthesefilescontaininformationaboutthepackagesyouinstalled,whichishelpfulwhenworkingwithotherdevelopers.Ifsomebodywantstocontributetotheproject,alltheyneedtodoisrunthegomoddownloadcommandontheirterminaltoinstallalltherequireddependenciesontheirmachine. Forreference,IpublishedtheentiresourcecodeofthisprojectonmyGitHub.Feelfreetopokearoundorcloneitontoyourcomputer. $gitclonehttps://github.com/rahmanfadhil/gin-bookstore.git Settinguptheserver Let’sstartbycreatingaHelloWorldserverinsidethemain.gofile. packagemain import( "net/http" "github.com/gin-gonic/gin" ) funcmain(){ r:=gin.Default() r.GET("/",func(c*gin.Context){ c.JSON(http.StatusOK,gin.H{"data":"helloworld"}) }) r.Run() } Wefirstneedtodeclarethemainfunctionthatwillbetriggeredwheneverwerunourapplication.Insidethisfunction,we’llinitializeanewGinrouterwithinthervariable.We’reusingtheDefaultrouterbecauseGinprovidessomeusefulmiddlewareswecanusetodebugourserver. Next,we’lldefineaGETroutetothe/endpoint.Ifyou’veworkedwithotherframeworks,suchasExpress.js,Flask,orSinatra,youshouldbefamiliarwiththispattern. Todefinearoute,weneedtospecifytwothings:theendpointandthehandler.Theendpointisthepaththeclientwantstofetch.Forinstance,iftheuserwantstograballbooksinourbookstore,they’dfetchthe/booksendpoint.Thehandler,ontheotherhand,determineshowweprovidethedatatotheclient.Thisiswhereweputourbusinesslogic,suchasgrabbingthedatafromthedatabase,validatingtheuserinput,andsoon. Wecansendseveraltypesofresponsetotheclient,butRESTfulAPIstypicallygivetheresponseinJSONformat.TodothatinGin,wecanusetheJSONmethodprovidedfromtherequestcontext.ThismethodrequiresanHTTPstatuscodeandaJSONresponseastheparameters. Lastly,wecanrunourserverbysimplyinvokingtheRunmethodofourGininstance. Totestitout,we’llstartourserverbyrunningthecommandbelow. $gorunmain.go Settingupthedatabase Thenextthingweneedtodoistobuildourdatabasemodels. Modelisaclass(orstructsinGo)thatallowsustocommunicatewithaspecifictableinourdatabase.InGorm,wecancreateourmodelsbydefiningaGostruct.Thismodelwillcontainthepropertiesthatrepresentfieldsinourdatabasetable.Sincewe’retryingtobuildabookstoreAPI,let’screateaBookmodel. //models/book.go packagemodels typeBookstruct{ IDuint`json:"id"gorm:"primary_key"` Titlestring`json:"title"` Authorstring`json:"author"` } OurBookmodelisprettystraightforward.Eachbookshouldhaveatitleandtheauthornamethathasastringdatatype,aswellasanID,whichisauniquenumbertodifferentiateeachbookinourdatabase. Wealsospecifythetagsoneachfieldusingbacktickannotation.ThisallowsustomapeachfieldintoadifferentnamewhenwesendthemasaresponsesinceJSONandGohavedifferentnamingconventions. Toorganizeourcodealittlebit,wecanputthiscodeinsideaseparatemodulecalledmodels. Next,weneedtocreateautilityfunctioncalledConnectDatabasethatallowsustocreateaconnectiontothedatabaseandmigrateourmodel’sschema.Wecanputthisinsidethesetup.gofileinourmodelsmodule. //models/setup.go packagemodels import( "github.com/jinzhu/gorm" _"github.com/jinzhu/gorm/dialects/sqlite" ) varDB*gorm.DB funcConnectDatabase(){ database,err:=gorm.Open("sqlite3","test.db") iferr!=nil{ panic("Failedtoconnecttodatabase!") } database.AutoMigrate(&Book{}) DB=database } Insidethisfunction,wecreateanewconnectionwiththegorm.Openmethod.Here,wespecifywhichkindofdatabaseweplantouseandhowtoaccessit.Currently,GormonlysupportsfourtypesofSQLdatabases.Forlearningpurposes,we’lluseSQLiteandstoreourdatainsidethetest.dbfile.Toconnectourservertothedatabase,weneedtoimportthedatabase’sdriver,whichislocatedinsidethegithub.com/jinzhu/gorm/dialectsmodule. Wealsoneedtocheckwhethertheconnectioniscreatedsuccessfully.Ifitdoesn’t,itwillprintouttheerrortotheconsoleandterminatetheserver. Next,wemigratethedatabaseschemausingAutoMigrate.Makesuretocallthismethodoneachmodelyouhavecreated. Lastly,wepopulatethetheDBvariablewithourdatabaseinstance.Wewillusethisvariableinourcontrollertogetaccesstoourdatabase. Inmain.go,weneedtocallthefollowingfunctionbeforewerunourapp. packagemain import( "github.com/gin-gonic/gin" "github.com/rahmanfadhil/gin-bookstore/models"//new ) funcmain(){ r:=gin.Default() models.ConnectDatabase()//new r.Run() } RESTfulroutes We’realmostthere! Thelastthingweneedtodoistoimplementourcontrollers.Intheprevioussection,welearnedhowtocreatearoutehandler(i.e.,controller)insideourmain.gofile.However,thisapproachmakesourcodemuchhardertomaintain.Insteadofdoingthat,wecanputourcontrollersinsideaseparatemodulecalledcontrollers. First,let’simplementtheFindBookscontroller. //controllers/books.go packagecontrollers import( "net/http" "github.com/gin-gonic/gin" "github.com/rahmanfadhil/gin-bookstore/models" ) //GET/books //Getallbooks funcFindBooks(c*gin.Context){ varbooks[]models.Book models.DB.Find(&books) c.JSON(http.StatusOK,gin.H{"data":books}) } Here,wehaveaFindBooksfunctionthatwillreturnallbooksfromourdatabase.TogetaccesstoourmodelandDBinstance,weneedtoimportourmodelsmoduleatthetop. Next,wecanregisterourfunctionasaroutehandlerinmain.go. packagemain import( "github.com/gin-gonic/gin" "github.com/rahmanfadhil/gin-bookstore/models" "github.com/rahmanfadhil/gin-bookstore/controllers"//new ) funcmain(){ r:=gin.Default() models.ConnectDatabase() r.GET("/books",controllers.FindBooks)//new r.Run() } Now,let’srunourserverandhitthe/booksendpoint. { "data":[] } Ifyouseeanemptyarrayastheresult,itmeansyourapplicationsareworking.Wegetthisbecausewehaven’tcreatedabookyet.Todoso,let’screateacreatebookcontroller. Tocreateabook,weneedtohaveaschemathatcanvalidatetheuser’sinputtopreventusfromgettinginvaliddata. typeCreateBookInputstruct{ Titlestring`json:"title"binding:"required"` Authorstring`json:"author"binding:"required"` } Theschemaisverysimilartoourmodel.Wedon’tneedtheIDpropertysinceitwillbegeneratedautomaticallybythedatabase. Nowwecanusethatschemainourcontroller. //POST/books //Createnewbook funcCreateBook(c*gin.Context){ //Validateinput varinputCreateBookInput iferr:=c.ShouldBindJSON(&input);err!=nil{ c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()}) return } //Createbook book:=models.Book{Title:input.Title,Author:input.Author} models.DB.Create(&book) c.JSON(http.StatusOK,gin.H{"data":book}) } WefirstvalidatetherequestbodybyusingtheShouldBindJSONmethodandpasstheschema.Ifthedataisinvalid,itwillreturna400errortotheclientandtellthemwhichfieldsareinvalid.Otherwise,itwillcreateanewbook,saveittothedatabase,andreturnthebook. Now,wecanaddtheCreateBookcontrollerinmain.go. funcmain(){ //... r.GET("/books",controllers.FindBooks) r.POST("/books",controllers.CreateBook)//new r.Run() } So,ifwetrytosendaPOSTrequestto/booksendpointwiththisrequestbody: { "title":"StartwithWhy", "author":"SimonSinek" } Theresponseshouldlookslikethis: { "data":{ "id":1, "title":"StartwithWhy", "author":"SimonSinek" } } We’vesuccessfullycreatedourfirstbook.Let’saddcontrollerthatcanfetchasinglebook. //GET/books/:id //Findabook funcFindBook(c*gin.Context){//Getmodelifexist varbookmodels.Book iferr:=models.DB.Where("id=?",c.Param("id")).First(&book).Error;err!=nil{ c.JSON(http.StatusBadRequest,gin.H{"error":"Recordnotfound!"}) return } c.JSON(http.StatusOK,gin.H{"data":book}) } OurFindBookcontrollerisprettysimilartotheFindBookscontroller.However,weonlygetthefirstbookthatmatchestheIDthatwegotfromtherequestparameter.Wealsoneedtocheckwhetherthebookexistsbysimplywrappingitinsideanifstatement. Next,registeritintoyourmain.go. funcmain(){ //... r.GET("/books",controllers.FindBooks) r.POST("/books",controllers.CreateBook) r.GET("/books/:id",controllers.FindBook)//new r.Run() } Togettheidparameter,weneedtospecifyitfromtheroutepath,asshownabove. Let’sruntheserverandfetch/books/1togetthebookwejustcreated. { "data":{ "id":1, "title":"StartwithWhy", "author":"SimonSinek" } } Sofar,sogood.Nowlet’saddtheUpdateBookcontrollertoupdateanexistingbook.Butbeforewedothat,weneedtodefinetheschemaforvalidatingtheuserinputfirst. structUpdateBookInput{ Titlestring`json:"title"` Authorstring`json:"author"` } TheUpdateBookInputschemaisprettymuchthesameasourCreateBookInput,exceptthatwedon’tneedtomakethosefieldsrequiredsincetheuserdoesn’thavetofillallthepropertiesofthebook. Toaddthecontroller: //PATCH/books/:id //Updateabook funcUpdateBook(c*gin.Context){ //Getmodelifexist varbookmodels.Book iferr:=models.DB.Where("id=?",c.Param("id")).First(&book).Error;err!=nil{ c.JSON(http.StatusBadRequest,gin.H{"error":"Recordnotfound!"}) return } //Validateinput varinputUpdateBookInput iferr:=c.ShouldBindJSON(&input);err!=nil{ c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()}) return } models.DB.Model(&book).Updates(input) c.JSON(http.StatusOK,gin.H{"data":book}) } First,wecancopythecodefromtheFindBookcontrollertograbasinglebookandmakesureitexists.Afterwefindthebook,weneedtovalidatetheuserinputwiththeUpdateBookInputschema.Finally,weupdatethebookmodelusingtheUpdatesmethodandreturntheupdatedbookdatatotheclient. Registeritintoyourmain.go. funcmain(){ //... r.GET("/books",controllers.FindBooks) r.POST("/books",controllers.CreateBook) r.GET("/books/:id",controllers.FindBook) r.PATCH("/books/:id",controllers.UpdateBook)//new r.Run() } Let’stestitout!FireaPATCHrequesttothe/books/:idendpointtoupdatethebooktitle. { "title":"TheInfiniteGame" } Theresultshouldbeasfollows. { "data":{ "id":1, "title":"TheInfiniteGame", "author":"SimonSinek" } } Thelaststepistoimplementtodeletebookfeature. //DELETE/books/:id //Deleteabook funcDeleteBook(c*gin.Context){ //Getmodelifexist varbookmodels.Book iferr:=models.DB.Where("id=?",c.Param("id")).First(&book).Error;err!=nil{ c.JSON(http.StatusBadRequest,gin.H{"error":"Recordnotfound!"}) return } models.DB.Delete(&book) c.JSON(http.StatusOK,gin.H{"data":true}) } Justliketheupdatecontroller,wegetthebookmodelfromtherequestparametersifitexistsanddeleteitwiththeDeletemethodfromourdatabaseinstance,whichwegetfromourmiddleware.Then,returntrueastheresultsincethereisnoreasontoreturnadeletedbookdatabacktotheclient. funcmain(){ //... r.GET("/books",controllers.FindBooks) r.POST("/books",controllers.CreateBook) r.GET("/books/:id",controllers.FindBook) r.PATCH("/books/:id",controllers.UpdateBook) r.DELETE("/books/:id",controllers.DeleteBook)//new r.Run() } Let’stestitoutbysendingaDELETErequesttothe/books/1endpoint. { "data":true } Ifwefetchallbooksin/books,we’llseeanemptyarrayagain. { "data":[] } Conclusion Goofferstwomajorqualitiesthatalldevelopersdesireandallprogramminglanguagesaimtoachieve:simplicityandperformance.Whilethistechnologymaynotbethebestoptionforeverydeveloperteam,it’sstillaverysolidsolutionandaskillworthlearning. Bybuildingthisprojectfromscratch,IhopeyougainedabasicunderstandingofhowtodevelopaRESTfulAPIwithGinandGorm,howtheyworktogether,andhowtoimplementtheCRUDfeatures.Thereisstillplentyofroomforimprovement,suchasauthenticatinguserswithJWT,implementingunittesting,containerizingyourappwithDocker,andalotofothercoolstuffyoucanmessaroundwithifyouwanttodigdeeper. Sharethis:TwitterRedditLinkedInFacebook RahmanFadhilFollow Developerandcontentwriter. Uncategorized #go «UsingthenewJavaScript.at()method ScaffoldinganappwithVue3,Nuxt,andTypeScript» UsingFetchEventSourceforserver-senteventsinReact MadarsBišs Dec15,2021 7minread ResponsiveCSSborderradiuswiththeFabFourtechnique KirilPeyanski Dec15,2021 4minread GettinganelementwithinacomponentwithVuequerySelector PopoolaTemitope Dec14,2021 4minread 18Repliesto“HowtobuildaRESTAPIwithGolangusing…” Iencountereda404pagenotfound whenIdownloadedgitversionandexecutedgorunmain.go Haveyoucheckedtherouteyou’regoingto?Goingtolocalhost:8080/bookswilltakeyoutothelistofbooks. Providingdbvariablethroughcontextisareallybadidea. Don’tusethisonproductionifyoudon’twanttobehacked.Passingdbthroughcontextisareallygoodidea(!) I’veupdatedthepostnow!Thedatabaseinstanceisnotprovideddirectlyfromthe`models/setup.go`. Itisgivingmeerroras->importedandnotused:“github.com/jinzhu/gorm” Youcanremovethelinewhereitimportsthepackageandre-runthecode.Itjustmeansthatyou’reimportingapackagebutyoudon’tdoanythingwithit. DoIneedpredefinestructforeverypostrequest? Yes,Goisastronglytypedlanguage.Youhavetoensureallthetypesintherequestbody. ThanksfortheArticle..Veryvaluable.. SomeissuesIfoundare 1.Importing`gorm`packagetobooksmodelthrows“importedandnotused”error. 2.Sincethemainfunctionhaschangedtoaroutehandler,thereisnoneedtoimport“net/http”packageandwillthrowthesameaboveerror. 3.The`delete`routerseemstobemissingcontrolleraction. Ifyouwanttoimprovesomethingyouneedtosharetheworkaroundaswell,notjustsaytheexistingstuffisbad. Foryourfirstissue,youcanaddanunderscorealiasinfrontofthegormimport: “` import_“github.com/jinzhu/gorm” “` Onewoulddothistoimport“sideeffects”(staticreference)ofamodule Excellentarticleforstarters. canyoupleasegivemeagithublinkforthisproject? IfoundareflecterrorwhenusingtheUpdateBookmethoditisbecausethetypesarenotthesame,sinceweareusinganUpdateBookInputstructtoupdateaBookstruct.Ifhelpssomeoneinthesamesituationthisisthecodeichanged: Startedlikethis: models.DB.Model(&book).Updates(input) Changedittothis: models.DB.Model(&book).Updates(models.Book{Title:input.Title,Author:input.Author}) Thanksforpostingthis!I’vebeenstrugglingwiththispart. Thanks,greatarticle. Mayyougivesomeexampleaboutrelationlikeonetoone,onetomany,manytomanyandetc? Thanksalotforyourgoodarticle. Whydoyouusecallbyreferencesomewherebutusecallbyvalueanotherwhere? e.g. 1.input: models.DB.Model(&book).Updates(input) 2.book models.DB.Create(&book) LeaveaReplyCancelreply



請為這篇文章評分?