Why I'm Interested In Golang?
I recently took a coding test for a job, and let’s just say it reminded me how fond I am of technical interviews... Spoiler: I didn’t get the job. 😕
Anyway, one thing that stood out was that Go was one of the language options. I’ve been noticing more and more job postings, especially in Japan, listing Go as a requirement. So, I figured it was time to familiarize myself with it.
Project Overview
With Go popping up more and more in job listings, I figured it was time to dive in. And what better way to learn than by building something hands-on? I decided to tackle a simple Todo app, nothing too fancy, just enough to get my feet wet.
It was my first real experience with Go, and let’s just say it was full of surprises, mostly the good kind. I thought I’d share my journey with you—steps, code snippets, and a bit of the logic behind it all.
Live Demo
The App is already live, but the Fly.io backend VM is set to auto-suspend (to save a bit of cash). So please give the backend VM a moment to wake up.
https://golang-todo-app.vercel.app/
Getting Started
In this post, we're sticking to the backend, focusing on Go and getting everything deployed on Fly.io. In the next post, we’ll focus on the frontend, where we’ll explore UI generation using Vercel’s v0.dev.
- Setting Up the Go Project
If you haven't already, download and install Go from the official website. Make sure it's properly installed by running:
go version
You should see something like:
go version go1.23.1 darwin/amd64
This is actual version I used for this project BTW.
- Create the Project Directory
I created a new directory for my project and initialized a Go module.
mkdir go-backend
cd go-backend
go mod init github.com/yourusername/go-backend
This sets up a new Go module and creates a go.mod
file to manage dependencies.
Building the Backend with Go
- Choosing a Web Framework
While Go's standard library is powerful, I wanted something to make routing and middleware easier. After some research, I chose Gin, a lightweight and fast web framework.
I installed Gin using:
go get github.com/gin-gonic/gin
- Setting Up the Main Application File
I created a main.go
file as the entry point of the application.
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Define routes later here
router.Run()
}
This sets up a basic Gin router and starts the server on the default port 8080
.
- Defining the Todo Model
I needed a Todo model to represent the tasks.
package main
type Todo struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
- Setting Up the Database with GORM
I wanted to interact with a PostgreSQL database, so I used GORM, an ORM library for Go.
Installed GORM and the PostgreSQL driver:
go get gorm.io/gorm
go get gorm.io/driver/postgres
- Connecting to the Database
In main.go
, I set up the database connection.
package main
import (
"github.com/gin-gonic/gin"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"log"
"os"
)
var db *gorm.DB
var err error
func main() {
dsn := os.Getenv("DATABASE_URL")
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
db.AutoMigrate(&Todo{})
router := gin.Default()
// Define routes here
router.Run()
}
Let's examine it closely:
- os.Getenv("DATABASE_URL"): Retrieves the database URL from an environment variable.
- gorm.Open(): Opens a connection to the database.
- db.AutoMigrate(&Todo): Automatically migrates the Todo model to create the table if it doesn't exist.
- Handling Environment Variables
To keep things secure and flexible, I used environment variables for sensitive information like the database URL. During development, I used a .env
file.
Installed the godotenv
package:
go get github.com/joho/godotenv
Then I updated main.go
to load the .env
file:
import (
// ... other imports ...
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Println("No .env file found, using environment variables.")
}
// ... rest of the code ...
}
Creating the API Endpoints
Time to set up the routes and handlers!
- Structuring the Routes
I decided to organize my code by creating separate functions for each route.
func main() {
// ... previous code ...
router := gin.Default()
router.GET("/todos", GetTodos)
router.POST("/todos", CreateTodo)
router.PUT("/todos/:id", UpdateTodo)
router.DELETE("/todos/:id", DeleteTodo)
router.Run()
}
- Implementing the Handlers
GetTodos:
- Retrieves all todos from the database.
- Returns them in JSON format.
func GetTodos(c *gin.Context) {
var todos []Todo
db.Find(&todos)
c.JSON(200, todos)
}
CreateTodo:
- Binds the JSON payload to a Todo struct.
- Creates a new record in the database.
- Returns the created todo.
func CreateTodo(c *gin.Context) {
var todo Todo
if err := c.ShouldBindJSON(&todo); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
db.Create(&todo)
c.JSON(201, todo)
}
UpdateTodo:
- Fetches the todo by ID.
- Updates it with the new data.
- Saves the changes to the database.
func UpdateTodo(c *gin.Context) {
id := c.Param("id")
var todo Todo
if err := db.First(&todo, id).Error; err != nil {
c.JSON(404, gin.H{"error": "Todo not found"})
return
}
if err := c.ShouldBindJSON(&todo); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
db.Save(&todo)
c.JSON(200, todo)
}
DeleteTodo:
- Fetches the todo by ID.
- Deletes it from the database.
- Returns a success message.
func DeleteTodo(c *gin.Context) {
id := c.Param("id")
var todo Todo
if err := db.First(&todo, id).Error; err != nil {
c.JSON(404, gin.H{"error": "Todo not found"})
return
}
db.Delete(&todo)
c.JSON(200, gin.H{"message": "Todo deleted"})
}
Testing the Application Locally
Before moving on, I wanted to ensure everything worked locally.
- Setting Up the Database
I used PostgreSQL for the database. If you don't have it installed, you can get it from here.
You can create a new one locally:
createdb go_todo_list
Or you can also create one in Vercel. Since my frontend will be hosted there, I ended up using that for this project. After creating it, you will get:
POSTGRES_URL="************"
POSTGRES_PRISMA_URL="************"
POSTGRES_URL_NO_SSL="************"
POSTGRES_URL_NON_POOLING="************"
POSTGRES_USER="************"
POSTGRES_HOST="************"
POSTGRES_PASSWORD="************"
POSTGRES_DATABASE="************"
- Configuring the .env file
With those variables, you can create a .env
file in the root directory:
DATABASE_URL=postgres://username:password@localhost:5432/go_todo_list?sslmode=disable
Replace username
and password
with your PostgreSQL credentials.
- Running the Application
Started the server:
go run main.go
Output:
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] GET /todos --> main.GetTodos (3 handlers)
[GIN-debug] POST /todos --> main.CreateTodo (3 handlers)
[GIN-debug] PUT /todos/:id --> main.UpdateTodo (3 handlers)
[GIN-debug] DELETE /todos/:id --> main.DeleteTodo (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080
- Testing the Endpoints
Used curl
or Postman to test the endpoints.
Example: Creating a Todo
curl -X POST -H "Content-Type: application/json" -d '{"title":"Learn Go","completed":false}' http://localhost:8080/todos
And, the response:
{
"id": 1,
"title": "Learn Go",
"completed": false
}
Awesome! The backend is working locally. Now It's time to move on.
Preparing for Production Deployment
Before deploying, I needed to make a few adjustments.
- Handling CORS
Since the frontend will be hosted on Vercel, I added CORS support.
Installed the Gin CORS middleware:
go get github.com/gin-contrib/cors
Updated main.go:
import (
// ... other imports ...
"github.com/gin-contrib/cors"
)
func main() {
// ... previous code ...
router := gin.Default()
// Configure CORS
router.Use(cors.Default())
// ... rest of the code ...
}
Deploying to Fly.io
Fly.io is a cloud hosting platform designed to run full-stack applications close to users by deploying apps to global edge locations. It is particularly developer-friendly and ideal for running containerized applications, including those built with Go (Golang).
I ended up using Fly.io because:
- Global Edge Deployment: Run Go apps closer to users for low latency.
- Easy Setup: Simple CLI for fast deployment.
- Auto Scaling: Handles traffic spikes effortlessly.
- DDoS Protection: Built-in security for production apps.
- Free Tier: Ideal for small projects and testing. (The free tier is especially important for me since building projects while applying to jobs can quickly get expensive).
Okay, It's time to get this app live!
- Installing Flyctl
Installed the Fly.io command-line tool:
brew install flyctl
- Logging In and Creating an App
After you create your account and verify your email, you need to login in the terminal:
flyctl auth login
Then, I created a new app:
flyctl launch
During the launch process:
- App Name: You can accept the default or provide a custom name.
- Select Region: Choose a region close to your target users.
- Dockerfile: Since there's no Dockerfile yet, Fly.io will prompt to create one (we'll create it manually in the next step).
- Create fly.toml: Accept to create this configuration file.
- Deploy Now: Choose "n" (no) for now; we'll deploy after configuring everything.
This command creates a fly.toml
file in your backend directory, which contains configuration for your Fly.io app.
- Create a Dockerfile for Your Go Application
Created a Dockerfile in the root directory:
- Multi-stage build: To keep the final image slim.
- Uses Alpine Linux: A lightweight base image.
# syntax=docker/dockerfile:1
# Build stage
FROM golang:1.23.1-alpine AS builder
WORKDIR /app
# Copy go.mod and go.sum files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy the source code
COPY . .
# Build the Go application
RUN go build -o app .
# Final stage
FROM alpine:latest
WORKDIR /root/
# Copy the built binary from the builder stage
COPY --from=builder /app/app .
# Command to run the executable
CMD ["./app"]
- Setting Environment Variables
Set the DATABASE_URL secret, this will automatically use whatever variables you have in your .env local file and use it for deployment:
flyctl secrets set DATABASE_URL="postgres://user:pass@hostname:5432/dbname"
- Deploying the Application
The deployment process built the Docker image and released it to Fly.io.
flyctl deploy
- Verifying the Deployment
Double checked the logs just in case:
flyctl logs
Wrapping Up
And there you have it! I successfully built and deployed a Go backend for a Todo app. This project was an good learning experience. Here's what I took away:
- Go's Simplicity: The language is straightforward, and the syntax is clean.
- Gin Framework: Makes building web applications in Go a breeze.
- GORM ORM: Simplifies database interactions.
- Environment Management: Using environment variables and .env files keeps configurations flexible and secure.
- Deployment: Fly.io makes deploying applications simple and efficient.
Final Thoughts
If you're new to Go like I am, I highly recommend diving in with a small project like this. It helps solidify the concepts and gives you hands-on experience. Plus, it's super satisfying to see your application, the thing that you created, live and running!
Feel free to check out the GitHub repo to see the backend code.
Stay tuned for the next blog post, where I'll walk through building the frontend with Next.js and deploying it on Vercel.