Fullstack on Go

Let’s create a simple web application in the Go programming language, where you can leave and read quotes. To store them we will use the PostgreSQL DBMS.

The application directory has the following structure:

code/
├── static
│   ├── styles.css
│   ├── script.js
│   └── index.html
├── amverum.yml
├── main.go
├── go.sum
├── go.mod
└── Dockerfile

The code for the static files is available at the end of the page.

Dockerfile

Steps:

  1. Create a Dockerfile in the project directory.

  2. In the Dockerfile we specify the base image called builder:

    FROM golang:1.21.1 AS builder
    

    instead of 1.21.1 you can specify any other version that you need.

  3. Setting the working directory:

    WORKDIR /app
    
  4. Copy the files main.go, go.mod and go.sum to the current directory of the working directory:

    COPY main.go go.mod go.sum ./
    
  5. We build the project and create the executable file server:

    RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o server
    
  6. We use the Alpine Linux image (this will reduce the size of the final image and improve its performance):

    FROM alpine:latest
    
  7. Copy the server executable file, compiled in the previous image, to the current directory of the working directory:

    COPY --from=builder /app/server ./
    
  8. Copying static files inside the container:

    COPY static/ ./static/
    
  9. Open port 80 for external connections:

    EXPOSE 80
    
  10. Adding a command to launch the application:

CMD ["./server", "--port", "80"]

The resulting Dockerfile:

FROM golang:1.21.1 AS builder
    
WORKDIR /app

COPY main.go go.mod go.sum ./

RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o server

FROM alpine:latest

COPY --from=builder /app/server ./

COPY static/ ./static/

EXPOSE 80

CMD ["./server", "--port", "80"]

Amverum.yaml

You can write a yaml file yourself or fill it out in the “Configuration” section of your personal account. Example file amverum.yml:

meta:
  environment: docker
  toolchain: docker
build:
  dockerfile: Dockerfile
  skip: false
run:
  persistenceMount: /data
  containerPort: "80"

Clue

If you use Dockerfile, then the amverum.yaml configuration file does not need to be added.

Dependencies (go.mod and sum.go)

We initialize a new module for managing dependencies. To do this, run the following command, which will create the file go.mod:

go mod init main

Clue

Instead of main, you can write an arbitrary line

All that remains is to run one more command, which will add two entries for each dependency and create a file go.sum:

go mod tidy

DBMS deployment (PostgreSQL)

The database needs to be deployed as a separate application, and then you can connect to it from the main application. Detailed instructions are available at link.

Creating a project in Amverum

The last step is to deploy the application itself. The main.go file contains the main code and connects to the database. Don’t forget to change the parameters for connecting to the database to those that you used in the previous step when creating a database on Amverum:

  • user - Username

  • password - User password

  • dbname - Name of the database being created

  • the host parameter can be found on the Info page of your PostgreSQL project (for example, amverum-username-cnpg-appname-rw)

main.go:

package main
   
import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "strconv"

    _ "github.com/lib/pq"
)

// Specify the values that you specified when creating the database on Amverum Cloud
const (
    host     = "amverum-nskripko-cnpg-godb-rw"
    port     = 5432
    user     = "nick"
    password = "href239"
    dbname   = "godb"
)

func main() {
    portStr := strconv.Itoa(port)
    dbinfo := "host=" + host + " port=" + portStr + " user=" + user + " password=" + password + " dbname=" + dbname + " sslmode=disable"

    db, err := sql.Open("postgres", dbinfo)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    _, err = db.Exec(`CREATE TABLE IF NOT EXISTS quotes (
                        id SERIAL PRIMARY KEY,
                        quote TEXT NOT NULL
                    )`)
    if err != nil {
        log.Fatal(err)
    }
    
    http.HandleFunc("/quotes", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }

        rows, err := db.Query("SELECT quote FROM quotes")
        if err != nil {
            log.Println("Error querying database:", err)
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
        defer rows.Close()

        var quotes []string
        for rows.Next() {
            var quote string
            if err := rows.Scan(&quote); err != nil {
                log.Println("Error scanning rows:", err)
                continue
            }
            quotes = append(quotes, quote)
        }

        json.NewEncoder(w).Encode(quotes)
    })

    http.HandleFunc("/addquote", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }

        var data struct {
            Quote string `json:"quote"`
        }
        if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
            http.Error(w, "Bad request", http.StatusBadRequest)
            return
        }

        _, err := db.Exec("INSERT INTO quotes (quote) VALUES ($1)", data.Quote)
        if err != nil {
            log.Println("Error inserting quote into database:", err)
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }

        w.WriteHeader(http.StatusCreated)
    })

    fs := http.FileServer(http.Dir("static"))
    http.Handle("/", fs)

    log.Fatal(http.ListenAndServe(":80", nil))
}

Important

The code is a demo example and we strongly do not recommend specifying a login and password to connect to the database in code. Use environment variables (secrets)!

To deploy your main application in Amverum you need to follow these simple steps:

  1. Open the page https://cloud.amverum.ru/projects

  2. Click the Create button and select service type application

  3. We upload all the files (you can use git, or you can use the interface). Make sure you have downloaded all the necessary files:

    • go.mod (necessarily)

    • go.sum (necessarily)

    • main.go (necessarily)

    • Dockerfile (necessarily)

    • static/index.html (if you use)

    • static/script.js (if you use)

    • static/styles.css (if you use

    • amverum.yml (necessarily)

  4. After this, the build and deployment application will begin. Wait for the “Successfully Deployed” status to appear.

Functionality check

  1. Go to the project settings and activate the domain name:

  2. Now you can go to this URL and our application will open:

If something does not work, we recommend that you read the Build and Application logs.

Congratulations, you have successfully created your first application in Amverum!

Static files code

static/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Quote Board</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>Quote Board</h1>
        <form id="quoteForm">
            <input type="text" id="quoteInput" placeholder="Enter your quote" required>
            <button type="submit">Submit</button>
        </form>
        <div id="quoteList"></div>
    </div>
    <script src="script.js"></script>
</body>
</html>

static/styles.css:

body {
  font-family: Arial, sans-serif;
}

.container {
  max-width: 600px;
  margin: 50px auto;
  padding: 0 20px;
}

input[type="text"] {
  width: calc(100% - 80px);
  padding: 10px;
  margin-right: 10px;
}

button {
  padding: 10px 20px;
  background-color: #007bff;
  color: #fff;
  border: none;
  cursor: pointer;
}

button:hover {
  background-color: #0056b3;
}

#quoteList {
  margin-top: 20px;
}

static/script.js:

document.addEventListener('DOMContentLoaded', () => {
  const quoteForm = document.getElementById('quoteForm');
  const quoteInput = document.getElementById('quoteInput');
  const quoteList = document.getElementById('quoteList');

  // Function to fetch quotes from the server and display them
  const fetchQuotes = async () => {
      try {
          const response = await fetch('/quotes');
          const quotes = await response.json();

          // Clear previous quotes
          quoteList.innerHTML = '';

          // Append new quotes to the list
          quotes.forEach(quote => {
              const quoteItem = document.createElement('div');
              quoteItem.textContent = quote;
              quoteList.appendChild(quoteItem);
          });
      } catch (error) {
          console.error('Error fetching quotes:', error);
      }
  };

  // Fetch initial quotes when the page loads
  fetchQuotes();

  // Submit quote form
  quoteForm.addEventListener('submit', async event => {
      event.preventDefault();
      const newQuote = quoteInput.value.trim();

      if (newQuote === '') {
          alert('Please enter a quote.');
          return;
      }

      try {
          // Send the new quote to the server
          await fetch('/addquote', {
              method: 'POST',
              headers: {
                  'Content-Type': 'application/json'
              },
              body: JSON.stringify({ quote: newQuote })
          });

          // Clear the input field
          quoteInput.value = '';

          // Fetch and display updated quotes
          fetchQuotes();
      } catch (error) {
          console.error('Error adding quote:', error);
      }
  });
});