🚀 Go-ing Full-Stack: Building Dynamic Web Apps with Go 🐹, PostgreSQL 🐘, Docker 🐳, and HTTP Servers 🌐
Allan Githaiga
Posted on November 15, 2024
You know, sometimes Go is all you need to... well, go far.
In this tutorial, we’re going full-stack using Go as our backend, PostgreSQL as our database, and a simple HTML + Docker. Why? Because we’re brave, we’re learning, and let’s face it – we love a good challenge.
Prerequisites
Make sure you have:
- Go installed (version 1.15 or higher)
- PostgreSQL running on your machine or Docker
- Docker installed
- A sense of humor (or at least an appreciation for developer jokes)
Step 1: Setting Up the Project and Connecting to PostgreSQL
Let’s start by setting up a new Go project.
mkdir go-fullstack-app
cd go-fullstack-app
go mod init go-fullstack-app
Now, we need to install the PostgreSQL driver for Go:
go get github.com/lib/pq
Step 2:Setting Up PostgreSQL with Docker
Instead of installing PostgreSQL directly on your machine, we’re going to use Docker to run PostgreSQL in a container. This makes it easier to manage and keep things isolated.
1. Pull the PostgreSQL Docker image:
In the terminal, run the following command to pull the official PostgreSQL image from Docker Hub:
docker pull postgres
2. Run the PostgreSQL container:
Now, we’ll run the container with a custom name and credentials. You can adjust the POSTGRES_PASSWORD, POSTGRES_USER, and POSTGRES_DB values as needed.
docker run --name my_postgres_container -e POSTGRES_PASSWORD=mysecretpassword -e POSTGRES_USER=myuser -e POSTGRES_DB=mydatabase -p 5432:5432 -d postgres
3. Access the PostgreSQL Database:
After running the above command, you can access the PostgreSQL container via the following command:
docker exec -it my_postgres_container psql -U myuser -d mydatabase
This opens up the PostgreSQL shell, where you can start interacting with the database.
Step 3: Creating a Table in PostgreSQL
Now that we have PostgreSQL running inside a Docker container, we’ll create a table to store user data. For this, we need to write SQL queries.
1. Create the users
table:
In the PostgreSQL shell, run the following query to create a table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100) UNIQUE NOT NULL
);
bash
-
id SERIAL PRIMARY KEY
: Automatically generates unique IDs for each user. -
name VARCHAR(100)
: Stores the user's name -
email VARCHAR(100) UNIQUE NOT NULL
: Stores the email address, ensuring it's unique and cannot be empty.
2. Insert data into the users
table: You can insert a few sample records:
INSERT INTO users (name, email) VALUES ('allan', 'allan@gmail.com');
INSERT INTO users (name, email) VALUES ('robinson', 'robinson@gmail.com');
3. Verify the data:
To see the data you’ve just inserted, run:
SELECT * FROM users;
You should see the users list:
id | name | email
----+---------+-------------------
1 | allan | allan@gmail.com
2 | robinson| robinson@gmail.com
Step 4: Writing Go Code to Connect to PostgreSQL
Next, we’ll write the Go code to interact with the PostgreSQL database.
- Set Up the Go Code to Connect to the Database: Here's a simple Go program that connects to PostgreSQL and fetches the list of users.
1. Import Statements:
import (
"database/sql" //Interacts with the SQL database.
"fmt"
"log"
"net/http"
"os"
"github.com/joho/godotenv" //Loads environment variables from a .env file.
_ "github.com/lib/pq" //A PostgreSQL driver for Go, enabling communication with PostgreSQL databases.
"encoding/json" //Provides functionality to encode and decode JSON data.
)
2.User Struct:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
Defines a simple User
struct, which will represent a user in the application.
3.getUsers Function:
func getUsers(w http.ResponseWriter, r *http.Request) {
rows, err := DB.Query("SELECT id, name, email FROM users")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
log.Println("Error scanning user", err)
continue
}
users = append(users, user)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
- Purpose: Fetches a list of users from the PostgreSQL database and returns it as a JSON response.
- Key Operations:
- Executes a SQL query to fetch id, name, and email from the users table.
- Loops through the result set (rows.Next()), scanning each row into the User struct.
- Appends each user to the users slice.
- Responds with the list of users encoded in JSON.
4.Global DB Variable & initDB Function:
var DB *sql.DB
func initDB() {
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"),
os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_NAME"))
var errConn error
DB, errConn = sql.Open("postgres", connStr)
if errConn != nil {
log.Fatalf("Error opening database: %v", errConn)
}
if err = DB.Ping(); err != nil {
log.Fatalf("Cannot connect to the database: %v", err)
}
fmt.Println("Database connected successfully!")
}
- A global variable DB that holds the connection pool for PostgreSQL. This will be used throughout the app to query the database.
InitDB Purpose: Initializes the connection to the PostgreSQL database.
Key Operations:
- Loads environment variables from the .env file using godotenv.
- Constructs a PostgreSQL connection string using the loaded environment variables.
- Opens a connection to the PostgreSQL database using sql.Open.
- Pings the database to ensure the connection is successful.
- Logs an error and exits if the connection fails.
5.loadhomepage Function:
func loadhomepage(w http.ResponseWriter, r *http.Request) {
// Query the database to get users
rows, err := DB.Query("SELECT id, name, email FROM users")
if err != nil {
http.Error(w, "Error fetching users: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
http.Error(w, "Error scanning user: "+err.Error(), http.StatusInternalServerError)
return
}
users = append(users, user)
}
// HTML template to render the users
html := "<html><head><title>Users List</title></head><body>"
html += "<h1>Users List</h1>"
html += "<table border='1'><tr><th>ID</th><th>Name</th><th>Email</th></tr>"
// Loop through users and display them in a table
for _, user := range users {
html += fmt.Sprintf("<tr><td>%d</td><td>%s</td><td>%s</td></tr>", user.ID, user.Name, user.Email)
}
html += "</table></body></html>"
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(html))
}
-
Purpose: Renders the list of users in HTML format.
Key Operations:- Executes a SQL query to fetch user data from the users table.
- Creates an HTML table to display the users.
- Loops through the users slice and formats each user as a table row in HTML.
- Sends the HTML response back to the client.
6.main Function:
func main() {
initDB()
// Set up routes and start your server here...
http.HandleFunc("/users", getUsers)
http.HandleFunc("/", loadhomepage)
//start server
fmt.Println("starting server on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
Purpose: The entry point of the application, responsible for starting the server and defining the routes.
Key Operations:
- Calls initDB to initialize the database connection.
- Sets up two routes:
- /users: This will invoke the getUsers function to return users in JSON format.
- /: This serves the homepage, calling loadhomepage to display users in an HTML table.
- Starts the HTTP server on port 8080 and listens for incoming requests.
- Sets up two routes:
Step 5: Running and Testing the App
Run the Go server:
go run main.go
Now, open in your browser, and voila! You should see a list of users fetched from your database. If not… well, debugging is half the fun (and sometimes 90% of the time).
Wrapping Up
In this project, you:
- Set up a Go backend with PostgreSQL.
- Created API routes to manage users.
- Built a frontend to display user data.
- Setting Up PostgreSQL with Docker
And that’s a wrap! Building a full-stack app in Go is surprisingly straightforward, and you now have a foundation to grow into more complex projects. Happy coding, and remember: If it works, don’t touch it. Unless it’s Go – then Go for it!
Posted on November 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.