DEV Community

Cover image for ๐Ÿš€ Go-ing Full-Stack: Building Dynamic Web Apps with Go ๐Ÿน, PostgreSQL ๐Ÿ˜, Docker ๐Ÿณ, and HTTP Servers ๐ŸŒ
Allan Githaiga
Allan Githaiga

Posted on

28 1 1 2 4

๐Ÿš€ Go-ing Full-Stack: Building Dynamic Web Apps with Go ๐Ÿน, PostgreSQL ๐Ÿ˜, Docker ๐Ÿณ, and HTTP Servers ๐ŸŒ

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:

  1. Go installed (version 1.15 or higher)
  2. PostgreSQL running on your machine or Docker
  3. Docker installed
  4. 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

Enter fullscreen mode Exit fullscreen mode

Now, we need to install the PostgreSQL driver for Go:

go get github.com/lib/pq

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
);

Enter fullscreen mode Exit fullscreen mode


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');

Enter fullscreen mode Exit fullscreen mode

3. Verify the data:
To see the data youโ€™ve just inserted, run:

SELECT * FROM users;

Enter fullscreen mode Exit fullscreen mode

You should see the users list:

id | name    | email
----+---------+-------------------
 1  | allan   | allan@gmail.com
 2  | robinson| robinson@gmail.com
Enter fullscreen mode Exit fullscreen mode

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.
)

Enter fullscreen mode Exit fullscreen mode

2.User Struct:

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

Enter fullscreen mode Exit fullscreen mode

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)
}

Enter fullscreen mode Exit fullscreen mode

- 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!")
}


Enter fullscreen mode Exit fullscreen mode
  • 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))
}

Enter fullscreen mode Exit fullscreen mode
  • 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)
    }
}

Enter fullscreen mode Exit fullscreen mode

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.

Step 5: Running and Testing the App

Run the Go server:

go run main.go

Enter fullscreen mode Exit fullscreen mode

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).

screenshot

Wrapping Up

In this project, you:

  1. Set up a Go backend with PostgreSQL.
  2. Created API routes to manage users.
  3. Built a frontend to display user data.
    1. 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!

Sentry blog image

Identify what makes your TTFB high so you can fix it

In the past few years in the web dev world, weโ€™ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

Read more

Top comments (3)

Collapse
 
fredgitonga profile image
Fred โ€ข

nice intro to postgres!
that is a neat way to run postgres dB with docket

Collapse
 
wpcortes75 profile image
Walter R P Cortes โ€ข

The good times are back! Long live to CGI processing!

Collapse
 
arturpanteleev profile image
Artur Panteleev โ€ข

nice tutorial, easy-reading but effective

Sentry image

See why 4M developers consider Sentry, โ€œnot bad.โ€

Fixing code doesnโ€™t have to be the worst part of your day. Learn how Sentry can help.

Learn more

๐Ÿ‘‹ Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay