Building a Go Microservice with CI/CD

Creating the Route Handler

articles := getAllArticles()
c.HTML( // Set the HTTP status to 200 (OK) http.StatusOK, // Use the index.html template "index.html", // Pass the data that the page uses gin.H{ "title": "Home Page", "payload": articles, }, )
// handlers.article.go package main import ( "net/http" "github.com/gin-gonic/gin" ) func showIndexPage(c *gin.Context) { articles := getAllArticles() // Call the HTML method of the Context to render a template c.HTML( // Set the HTTP status to 200 (OK) http.StatusOK, // Use the index.html template "index.html", // Pass the data that the page uses gin.H{ "title": "Home Page", "payload": articles, }, ) }
├── common_test.go ├── handlers.article.go ├── handlers.article_test.go ├── models.article.go └── models.article_test.go

Displaying a Single Article

Setting Up the Route

router.GET("/article/view/:article_id", getArticle)
func main() { router := gin.Default() router.LoadHTMLGlob("templates/*") // Handle Index router.GET("/", showIndexPage) // Handle GET requests at /article/view/some_article_id router.GET("/article/view/:article_id", getArticle) router.Run() } . . .

Creating the View Templates

<!--article.html--> <!--Embed the header.html template at this location--> {{ template "header.html" .}} <!--Display the title of the article--> <h1>{{.payload.Title}}</h1> <!--Display the content of the article--> <p>{{.payload.Content}}</p> <!--Embed the footer.html template at this location--> {{ template "footer.html" .}}

Specifying the Requirement for the Go Microservice Router

Creating the Route Handler

c.Param("article_id")
article, err := getArticleByID(articleID)
// models.article.go package main import ( "errors" ) type article struct { ID int `json:"id"` Title string `json:"title"` Content string `json:"content"` } // For this demo, we're storing the article list in memory // In a real application, this list will most likely be fetched // from a database or from static files var articleList = []article{ article{ID: 1, Title: "Article 1", Content: "Article 1 body"}, article{ID: 2, Title: "Article 2", Content: "Article 2 body"}, } // Return a list of all the articles func getAllArticles() []article { return articleList } func getArticleByID(id int) (*article, error) { for _, a := range articleList { if a.ID == id { return &a, nil } } return nil, errors.New("Article not found") }
c.HTML( // Set the HTTP status to 200 (OK) http.StatusOK, // Use the article.html template "article.html", // Pass the data that the page uses gin.H{ "title": article.Title, "payload": article, }, )
// handlers.article.go package main import ( "net/http" "strconv" "github.com/gin-gonic/gin" ) func showIndexPage(c *gin.Context) { articles := getAllArticles() // Call the HTML method of the Context to render a template c.HTML( // Set the HTTP status to 200 (OK) http.StatusOK, // Use the index.html template "index.html", // Pass the data that the page uses gin.H{ "title": "Home Page", "payload": articles, }, ) } func getArticle(c *gin.Context) { // Check if the article ID is valid if articleID, err := strconv.Atoi(c.Param("article_id")); err == nil { // Check if the article exists if article, err := getArticleByID(articleID); err == nil { // Call the HTML method of the Context to render a template c.HTML( // Set the HTTP status to 200 (OK) http.StatusOK, // Use the index.html template "article.html", // Pass the data that the page uses gin.H{ "title": article.Title, "payload": article, }, ) } else { // If the article is not found, abort with an error c.AbortWithError(http.StatusNotFound, err) } } else { // If an invalid article ID is specified in the URL, abort with an error c.AbortWithStatus(http.StatusNotFound) } }
└── templates └── article.html

Responding With JSON/XML

Creating a Reusable Function

// c is the Gin Context c.Request.Header.Get("Accept")
// Render one of HTML, JSON or CSV based on the 'Accept' header of the request // If the header doesn't specify this, HTML is rendered, provided that // the template name is present func render(c *gin.Context, data gin.H, templateName string) { switch c.Request.Header.Get("Accept") { case "application/json": // Respond with JSON c.JSON(http.StatusOK, data["payload"]) case "application/xml": // Respond with XML c.XML(http.StatusOK, data["payload"]) default: // Respond with HTML c.HTML(http.StatusOK, templateName, data) } }

Modifying the Requirement for the Route Handlers With a Unit Test

Updating the Route Handlers

func showIndexPage(c *gin.Context) { articles := getAllArticles() // Call the HTML method of the Context to render a template c.HTML( // Set the HTTP status to 200 (OK) http.StatusOK, // Use the index.html template "index.html", // Pass the data that the page uses gin.H{ "title": "Home Page", "payload": articles, }, ) }
func showIndexPage(c *gin.Context) { articles := getAllArticles() // Call the render function with the name of the template to render render(c, gin.H{ "title": "Home Page", "payload": articles}, "index.html") }
curl -X GET -H "Accept: application/json" http://localhost:8080/
[{"id":1,"title":"Article 1","content":"Article 1 body"},{"id":2,"title":"Article 2","content":"Article 2 body"}]
curl -X GET -H "Accept: application/xml" http://localhost:8080/article/view/1
<article><ID>1</ID><Title>Article 1</Title><Content>Article 1 body</Content></article>

Testing the Application

=== RUN TestShowIndexPageUnauthenticated [GIN] 2022/05/11 - 11:33:20 | 200 | 429.084µs | | GET "/" --- PASS: TestShowIndexPageUnauthenticated (0.00s) === RUN TestGetAllArticles --- PASS: TestGetAllArticles (0.00s) PASS ok github.com/tomfern/semaphore-demo-go-gin	0.704s

Continuous Integration for Go on Semaphore

$ git init $ git remote add YOUR_REPOSITORY_URL $ git add -A $ git commit -m "initial commit" $ git push origin main

Add Semaphore to Your Project

sem-version go 1.18

Improving the Pipeline

sem-version go 1.18 export GO111MODULE=on export GOPATH=~/go export PATH=/home/semaphore/go/bin:$PATH checkout cache restore go mod vendor cache store

Testing with Semaphore

sem-version go 1.18 export GO111MODULE=on export GOPATH=~/go export PATH=/home/semaphore/go/bin:$PATH checkout cache restore go mod vendor
go build -v -o go-gin-app artifact push project --force go-gin-app

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store