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
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
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
}
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 therouter
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.
-
Endpoint : It is the path from where data is fetched. For instance, if the visitor wants to get all posts, they’d fetch the
Use the following command to start the server.
go run main.go
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
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
To build the docker container from above Dockerfile
use the below command
docker build -t hello_world :1.0 .
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
To spin up the container use the below command
docker run -p 8000:8000 hello_world:1.0
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
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,
}
}
Inside NewDatabase
function the following work is being done :
- database credentials USER, PASS, HOST and DBNAME is fetched from environment variables
- Database url is constructed utilizing environment variables and saving it to
URL
variable - A new
mysql
connection is opened with thegorm.Open
method fromURL
. - 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
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")
}
}
Install godotenv package by following command
go get github.com/joho/godotenv
*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")
}
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"
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
anddb
.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 on8000
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}"
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
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
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)
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.
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 lineRUN go get github.com/githubnemo/CompileDaemon
to
RUN go install github.com/githubnemo/CompileDaemon
and it should work as it should
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
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
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