DEV Community

Cover image for Blog Project with Go, Gin, MySQL and Docker - Part 1
Umesh Chaudhary
Umesh Chaudhary

Posted on • Originally published at nizu.tech

Blog Project with Go, Gin, MySQL and Docker - Part 1

Contents

Target Audience

This Series doesn't focuses on fundamentals of Go. It focuses on how to build backend services. Indiviuals interested in learning web development with Gin Framework are ideal candidates.

Learning Objectives

This Series teaches you the fundamental of building Rest APIs with Go. After completing this series you will know how to do the following :

  • Setup Golang with MySQL and Docker for Local Development
  • Gin Framework for CRUD APIs.
  • Domain Driven Development
  • Dependency Injection with Go
  • Authentication with Firebase and so. on

Requirements

  • Go Installed ( version : go1.16.3 linux/amd64 )
  • Docker ( version 20.10.6 ) & Docker-compose ( version 1.29.1).
  • Prefarrably Linux Machine ( As i will be using linux but any would be fine )
  • General Understanding of Restful Services/APIs.

The above mentioned versions are installed on my machine.
You can find the full source of the series in this repository, feel free to fork it:

Getting Started

Let's start by creating a blog folder in your preferred location. Then initialize a new Go module inside blog folder with following command.

go mod init blog
Enter fullscreen mode Exit fullscreen mode

The above command creates a go.mod file. It will allow you to manage project dependencies. Let's add some dependencies.

go get github.com/gin-gonic/gin
Enter fullscreen mode Exit fullscreen mode

The above command will install Gin Framework and Gorm and create go.sum file. This file stores info about the dependencies and their versions.

Gin is lightweight, well-documented and fast HTTP web framework. The creators claims that Gin is 40 times faster than other similar framework to Gin

Getting Up Hello world Server

Create a main.go file in the root level; open up your favorite editor and write up the following code

package main

import (
  "net/http"
  "github.com/gin-gonic/gin"
)

func main() {
  router := gin.Default() //new gin router initialization
  router.GET("/", func(context *gin.Context) {
    context.JSON(http.StatusOK, gin.H{"data": "Hello World !"})    
  }) // first endpoint returns Hello World
  router.Run(":8000") //running application, Default port is 8080
}
Enter fullscreen mode Exit fullscreen mode

Let's go through the code in main.go

  • package main : Each go file is a package. The main package is always an entry point for gin project.

  • ** import ** : Imports project dependenices.

  • ** func main ** : Main function is triggered whenever you run the application. Inside the function, a new Gin router is initialized to the router variable. A new endpoint / is set on the router.
    *Setting up a route/endpoint requires two things : *

    • Endpoint : It is the path from where data is fetched. For instance, if the visitor wants to get all posts, they’d fetch the /posts endpoint.
    • Handler : It determines how you provide the data to endpoint. Business logic, like getting/saving data from/to the database, validating the user input, and so on. JSON method of context object is used to send data in JSON format. This method takes an HTTP status code and a JSON response as the parameters.

Use the following command to start the server.

go run main.go
Enter fullscreen mode Exit fullscreen mode

If you see output similar to the following that means you are good to go.

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8000
Enter fullscreen mode Exit fullscreen mode

visit http://localhost:8000/ on your browser. You should see {"data":"Hello World !"}.

Setting Up Docker

The above method of running the server is the Go way of running applications. Let's setup it via the Docker way.
Create a file Dockerfile at the project root with the following code

FROM golang:alpine

RUN mkdir /app

WORKDIR /app

ADD go.mod .
ADD go.sum .

RUN go mod download
ADD . .

RUN go get github.com/githubnemo/CompileDaemon

EXPOSE 8000

ENTRYPOINT CompileDaemon --build="go build main.go" --command=./main

Enter fullscreen mode Exit fullscreen mode

To build the docker container from above Dockerfile use the below command

docker build -t hello_world :1.0 .
Enter fullscreen mode Exit fullscreen mode

containers can be listed out with docker ps -a on your terminal. You should see containers with other infomation.

CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS                      PORTS     NAMES
62cddf7c2659   hello_world:1.0      "/bin/sh -c 'Compile…"   23 minutes ago   Exited (0) 23 minutes ago             gifted_kilby
Enter fullscreen mode Exit fullscreen mode

To spin up the container use the below command

docker run -p 8000:8000 hello_world:1.0
Enter fullscreen mode Exit fullscreen mode

visit http://localhost:8000/ on your browser. You should see {"data":"Hello World !"}.

Let’s Review Docker Commands mentioned above in Dockerfile :

  • FROM : FROM refers which base image to use for the container. The golang:1.16 image is a Linux-based image which has golang installed and no other additional programs or software.

  • WORKDIR : WORKDIR Changes the working directory. In our case to /app. It sets a working directory for subsequent commands.

  • ADD : ADD instruction literally copies the file from one location to another. ADD [SOURCE] [DESTINATION] is the syntax of the command. Similary, there is also a COPY command for similar purpose. Here, we are copying the go.sum and go.mod file first so that we will have all the libraries installed before copying all the files.

  • RUN : RUN instruction will execute any commands in a new layer on top of the current image and commit the results. The resulting committed image will be used for the next step in the Dockerfile.

  • EXPOSE : EXPOSE instructs that services running on Docker container is available in port 8000.

  • ENTRYPOINT : Entrypoint runs the command inside the container once a container is created from an image. You can only have one Entrypoint instruction in a Dockerfile. If multiple Entrypoint instructions are used, the last one will be executed. Here, once the container is created, the Entrypoint command will run our golang project.

Setting Up The Database

MySQL package is needed to connect the database with the application. Install it with following command.

go get gorm.io/driver/mysql gorm.io/gorm
Enter fullscreen mode Exit fullscreen mode

Gorm is fantastic ORM library for Golang.
As connecting database is essential/building part of application. Let's create a folder infrastructure and db.go file inside infrastructure. Let's write some come inside db.go.

package infrastructure

import (
    "fmt"
    "os"
    "time"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

//Database struct
type Database struct {
    DB *gorm.DB
}

//NewDatabase : intializes and returns mysql db
func NewDatabase() Database {
    USER := os.Getenv("DB_USER")
    PASS := os.Getenv("DB_PASSWORD")
    HOST := os.Getenv("DB_HOST")
    DBNAME := os.Getenv("DB_NAME")

    URL := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", USER, PASS, 
    HOST, DBNAME)
    fmt.Println(URL)
    db, err := gorm.Open(mysql.Open(URL))

    if err != nil {
        panic("Failed to connect to database!")

    }
    fmt.Println("Database connection established")
    return Database{
        DB: db,
    }

}
Enter fullscreen mode Exit fullscreen mode

Inside NewDatabase function the following work is being done :

  1. database credentials USER, PASS, HOST and DBNAME is fetched from environment variables
  2. Database url is constructed utilizing environment variables and saving it to URL variable
  3. A new mysql connection is opened with the gorm.Open method from URL.
  4. Lastly, Database struct is returned with gorm database instance as parameter, which is used later on the application to access the database.

Environment Variables

There are two type of variables in the project program variables and environment variables. Program variables are normal variables that stores values withing the code block or module, where as environment variables is available globally through the project.

Setting Up Environment Variables

Environment variables can be set up using various ways. Here, we will be using .env file to set them. Create .env file on the project root and add below variables.

MYSQL_ROOT_PASSWORD=root
DB_USER=user
DB_PASSWORD=Password@123
DB_HOST=db
DB_PORT=3306
DB_NAME=golang
SERVER_PORT=8000
ENVIRONMENT=local
Enter fullscreen mode Exit fullscreen mode

Reading Environment Variables

Now, let's write code to read .env file. Create a file env.go in infrastructure folder and fill up with following code

package infrastructure

import (
    "log"
    "github.com/joho/godotenv"
)

//LoadEnv loads environment variables from .env file
func LoadEnv() {
    err := godotenv.Load(".env")
    if err != nil {
        log.Fatalf("unable to load .env file")
    }
}

Enter fullscreen mode Exit fullscreen mode

Install godotenv package by following command

go get github.com/joho/godotenv
Enter fullscreen mode Exit fullscreen mode

*In order to load .env file and database we need to edit main.go as below *

func main() {

    router := gin.Default()

    router.GET("/", func(context *gin.Context) {
        infrastructure.LoadEnv()  //loading env
        infrastructure.NewDatabase() //new database connection
        context.JSON(http.StatusOK, gin.H{"data": "Hello World !"})
    })
    router.Run(":8000")
}

Enter fullscreen mode Exit fullscreen mode

Docker Compose file

We need docker compose for defining and running multi-container Docker applications. In our case Database and our Gin application. Create a file docker-compose.yml at project level and fill up with following code

version: '3'
services:
  db:
    image: mysql/mysql-server:5.7
    ports:
      - "3305:3306"
    environment:
      - "MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}"
      - "MYSQL_USER=${DB_USER}"
      - "MYSQL_PASSWORD=${DB_PASSWORD}"
      - "MYSQL_DATABASE=${DB_NAME}"   
  web:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - ".:/app"
    depends_on:
      - db
    links:
      - "db:database"
Enter fullscreen mode Exit fullscreen mode

Let’s review terms mentioned inside compose file.

  • version ‘3’: Docker compose version, Latest is 3.7

  • services: The services section defines all the different containers that are to be created. We have two services namely, web and db.

  • web: This is the name of our Gin app service. It can be anything. Docker Compose will create containers with this name.

  • build: This clause specifies the Dockerfile location. . represents the current directory where the docker-compose.yml file is located and Dockerfile is used to build an image and run the container from it. We can also enter a path to Dockerfile there.

  • ports: The ports clause is used to map or expose the container ports to the host machine. Here mapping port "8000:8000" , so that we can access our services on 8000 port of host machine.

  • volumes: Here, we are attaching our code files directory to the containers, ./app directory so that we don’t have to rebuild the images for every change in the files. This will also help in auto-reloading the server when running in debug mode.

  • links: Links literally links one service to another. Here, we are linking the database container to the web container, so that our web container can reach the database in the bridge network. (Networking alert !!). Please if you want to learn about network in docker in detail do refer to : Network containers

  • image: If we don’t have a Dockerfile and want to run a service directly using an already built image, we can specify the image location using the ‘image’ clause. Compose will pull the image and fork a container from it. In our case We mentioned mysql/mysql-server:5.7 to our database service

  • environment: Any environment variable that needs to be set up in the container can be created using the ‘environment’ clause.

   environment:
      - "MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}"
Enter fullscreen mode Exit fullscreen mode

The value ${MYSQL_ROOT_PASSWORD} and other variables are read from .env.
You are now all set to spin up the containers. Start build and run with the following command.

 docker-compose up --build
Enter fullscreen mode Exit fullscreen mode

If you see something like below on your terminal that means, you have server running up:

db_1   | 2021-05-24T09:18:02.841094Z 0 [Note] Event Scheduler: Loaded 0 events
db_1   | 2021-05-24T09:18:02.841370Z 0 [Note] mysqld: ready for connections.
db_1   | Version: '5.7.34'  socket: '/var/lib/mysql/mysql.sock'  port: 3306  MySQL Community Server (GPL)
web_1  | 2021/05/24 09:18:06 Running build command!
web_1  | 2021/05/24 09:18:17 Build ok.
web_1  | 2021/05/24 09:18:17 Restarting the given command.
web_1  | 2021/05/24 09:18:17 stdout: [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
web_1  | 2021/05/24 09:18:17 stdout:
web_1  | 2021/05/24 09:18:17 stdout: [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
web_1  | 2021/05/24 09:18:17 stdout:  - using env:      export GIN_MODE=release
web_1  | 2021/05/24 09:18:17 stdout:  - using code:     gin.SetMode(gin.ReleaseMode)
web_1  | 2021/05/24 09:18:17 stdout:
web_1  | 2021/05/24 09:18:17 stdout: [GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
web_1  | 2021/05/24 09:18:17 stdout: [GIN-debug] Listening and serving HTTP on :8000
Enter fullscreen mode Exit fullscreen mode

Navigate to http://localhost:8000/ and check on terminal, Database connection established should be printed on standard output.

The repository of this series can be found here

Wrap Up

That's it for part 1 of the series.
In the next part we will be working on :

  • Adding models (structs) to the application
  • Adopting Domain Driven Development pattern

Make sure to follow me to get the update for the next part or subscribe so that you never miss my upcoming articles.

Thank you !!

Top comments (5)

Collapse
 
khpahmil profile image
khpahmil

hello Umesh, I got error when I run the container. with :
docker run -p 8000:8000 hello_world:1.0
the error is: /bin/sh: CompileDaemon: not found
Im not sure, why this error appear. please help, thank you.

Collapse
 
ngutu profile image
Nichita Gutu

With newer versions of Go go get is used to adjust dependencies. To install packages, CompileDaemon in this case, go install is used, so you'll need to change in your Dockerfile line
RUN go get github.com/githubnemo/CompileDaemon
to
RUN go install github.com/githubnemo/CompileDaemon
and it should work as it should

Collapse
 
tungnt profile image
Tom Nguyen

I use Go v.1.19 and I need two lines above to install CompileDaemon. Having just one of them is not enough.

RUN go get github.com/githubnemo/CompileDaemon
RUN go install github.com/githubnemo/CompileDaemon

Collapse
 
hugonorte profile image
hugonorte

The same happened here and I fixed using this line in my Dockerfile:
instead of
RUN go get github.com/githubnemo/CompileDaemon
Use
RUN go install -mod=mod github.com/githubnemo/CompileDaemon

Collapse
 
neeraj1997dev profile image
Neeraj Kumar

neerajyadav@Neerajs-MBP-2 buildrun_golang % sudo docker-compose up

[+] Running 2/0
✔ Container buildrun_golang-db-1 Running 0.0s
✔ Container buildrun_golang-web-1 Created 0.0s
Attaching to buildrun_golang-db-1, buildrun_golang-web-1
buildrun_golang-web-1 | /bin/sh: CompileDaemon: not found
buildrun_golang-web-1 exited with code 127