# Fullstack на Go Создадим простое web приложение на языке программирования Go, где можно будет оставлять и читать цитаты. Для их хранения будем использовать СУБД PostgreSQL. Директория приложения имеет следующую структуру: ``` code/ ├── static │ ├── styles.css │ ├── script.js │ └── index.html ├── amvera.yml ├── main.go ├── go.sum ├── go.mod └── Dockerfile ``` Код статических файлов доступен в конце страницы. ## Dockerfile ### Шаги: 1. Создаем Dockerfile в директории с проектом. 2. В Dockerfile указываем базовый образ с названием *builder*: ```dockerfile FROM golang:1.21.1 AS builder ``` вместо 1.21.1 можете указать любую другую версию, которая вам нужна. 3. Устанавливаем рабочую директорию: ```dockerfile WORKDIR /app ``` 4. Копируем файлы *main.go*, *go.mod* и *go.sum* в текущую директорию рабочего каталога: ```dockerfile COPY main.go go.mod go.sum ./ ``` 5. Выполняем сборку проекта и создаем исполняемый файл *server*: ```dockerfile RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o server ``` 6. Используем образ Alpine Linux (это уменьшит размер конечного образа и улучшит его производительность): ```dockerfile FROM alpine:latest ``` 7. Копируем исполняемый файл *server*, собранный в предыдущем образе, в текущую директорию рабочего каталога: ```dockerfile COPY --from=builder /app/server ./ ``` 8. Копируем статические файлы внутрь контейнера: ```dockerfile COPY static/ ./static/ ``` 9. Открываем порт 80 для внешних подключений: ```dockerfile EXPOSE 80 ``` 10. Добавляем команду для запуска приложения: ```dockerfile CMD ["./server", "--port", "80"] ``` **Получившийся Dockerfile:** ```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"] ``` ## Amvera.yml Написать yaml файл можно как самостоятельно, так и воспользоваться нашим генератором yaml, перейдя по [ссылке](https://manifest.amvera.ru/), либо заполнить в разделе «Конфигурация» личного кабинета. **Пример файла amvera.yml:** ```yaml meta: environment: docker toolchain: docker build: dockerfile: Dockerfile skip: false run: persistenceMount: /data containerPort: "80" ``` ```{eval-rst} .. admonition:: Подсказка :class: hint Если вы используете Dockerfile, то конфигурационный файл amvera.yaml можно не добавлять. ``` ## Зависимости (go.mod и sum.go) Инициализируем новый модуль для управления зависимостями. Для этого выполняем следующую команду, которая создаст файл *go.mod*: ```bash go mod init main ``` ```{eval-rst} .. admonition:: Подсказка :class: hint Вместо main можно написать произвольную строку ``` Остаётся выполнить ещё одну команду, которая добавит по две записи на каждую зависимость и создаст файл *go.sum*: ```bash go mod tidy ``` ## Развертывание СУБД (PostgreSQL) Базу данных нужно развернуть как отдельное приложение, а затем можно будет подключаться к ней из основного приложения. Подробная инструкция доступна по [ссылке](https://docs.amvera.ru/databases/postgreSQL.html#postgresql). ## Создание проекта в Amvera Последний шаг - развернуть само приложение. В файле *main.go* содержится основной код и выполняется подключение к базе данных. Не забудьте поменять параметры для подключения к базе данных на те, которые вы использовали в прошлом шаге при создании базы данных на Amvera: - **user** - Имя пользователя - **password** - Пароль пользователя - **dbname** - Имя создаваемой БД - параметр **host** можно найти на странице *Инфо* вашего PostgreSQL проекта (например, amvera-username-cnpg-appname-rw) **main.go:** ```golang package main import ( "database/sql" "encoding/json" "log" "net/http" "strconv" _ "github.com/lib/pq" ) // Укажите те значения, которые задавали при создании БД на Amvera Cloud const ( host = "amvera-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("e); 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)) } ``` ```{eval-rst} .. admonition:: Важно :class: warning Код является демонстрационным примером и мы настоятельно не рекомендуем указывать логин и пароль для подключения к базе данных в коде. Используйте переменные окружения (секреты)! ``` **Чтобы развернуть основное приложение в Amvera, нужно выполнить следующие простые шаги:** 1. Открываем страницу https://cloud.amvera.ru/projects 2. Нажимаем кнопку *Создать* и выбираем *тип сервиса* **приложение** 3. Выгружаем все файлы (можно через git, а можно через интерфейс). Убедитесь, что вы выгрузили все нужные файлы: - go.mod (обязательно) - go.sum (обязательно) - main.go (обязательно) - Dockerfile (обязательно) - static/index.html (если используете) - static/script.js (если используете) - static/styles.css (если используете) - amvera.yml (необязательно) 4. После этого начнется [сборка](https://docs.amvera.ru/applications/build.html) и [развертывание](https://docs.amvera.ru/applications/run.html) приложения. Дождитесь появления статуса «Успешно развернуто». ## Проверка работоспособности 1. Переходим в настройки проекта и активируем доменное имя: ![](../../img/go_full_test_web_app.png) 2. Теперь можно перейти по данному URL и откроется наше приложение: ![](../../img/go_full_app_example.png) Если что-то не работает, рекомендуем ознакомиться с логами Сборки и Приложения. Поздравляем, вы успешно создали свое первое приложение в Amvera! ## Код статических файлов **static/index.html:** ```html Quote Board

Quote Board

``` **static/styles.css:** ```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:** ```javascript 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); } }); }); ```