November 11, 2019

Мини-руководство для начинающих

Создание REST API как микросервис Go вместе с MySQL.

Настройка API
Первое, что нужно сделать, это выбрать пакет маршрутизации. Маршрутизация - это то, что связывает URL с исполняемой функцией. В данном руководстве импользуется mux, но есть и другие альтернативы без особой разницы в производительности, такие как httprouter и .

Для простоты создадана одна конечная точка, которая печатает сообщение.

package main

import ( "log" "net/http"

"github.com/gorilla/mux" )

func setupRouter(router *mux.Router) { router. Methods("POST"). Path("/endpoint"). HandlerFunc(postFunction) }

func postFunction(w http.ResponseWriter, r *http.Request) { log.Println("You called a thing!") }

func main() { router := mux.NewRouter().StrictSlash(true)

setupRouter(router)

log.Fatal(http.ListenAndServe(":8080", router)) }


Приведенный выше код создает маршрутизатор, связывает URL-адрес с функцией-обработчиком, в данном случае postFunction, и запускает сервер на порту 8080, используя этот маршрутизатор.

Подключение к базе данных
Go предоставляет интерфейс для баз данных SQL, но требует драйвера. Можно использовать go-sql-driver.

package db

import ( "database/sql"

_ "github.com/go-sql-driver/mysql" )

func CreateDatabase() (*sql.DB, error) { serverName := "localhost:3306" user := "myuser" password := "pw" dbName := "demo"

connectionString := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=true&multiStatements=true", user, password, serverName, dbName) db, err := sql.Open("mysql", connectionString) if err != nil { return nil, err }

return db, nil }

Код помещается в другой пакет, называемый db, и предполагает, что на localhost:3306 работает база данных, называемая demo. База данных автоматически обрабатывает connection pool.

Далее нужно обновить postFunction из предыдущего фрагмента кода, чтобы использовать эту базу данных.

func postFunction(w http.ResponseWriter, r *http.Request) { database, err := db.CreateDatabase() if err != nil { log.Fatal("Database connection failed") }

_, err = database.Exec("INSERT INTO `test` (name) VALUES ('myname')") if err != nil { log.Fatal("Database INSERT failed") }

log.Println("You called a thing!") }

Это довольно просто, но есть несколько проблем с приведенным выше кодом и несколько недостатков.

Структуры и зависимости
Изучив приведенный выше код, можно заметить, что база данных открывается при каждом вызове API. Это неправильно, даже если открытая база данных безопасна для одновременного использования. Необходимо некоторое управление зависимостями, чтобы убедиться, что база данных открывается только один раз.

package app

import ( "database/sql" "log" "net/http"

"github.com/gorilla/mux" )

type App struct { Router *mux.Router Database *sql.DB }

func (app *App) SetupRouter() { app.Router. Methods("POST"). Path("/endpoint"). HandlerFunc(app.postFunction) }

func (app *App) postFunction(w http.ResponseWriter, r *http.Request) { _, err := app.Database.Exec("INSERT INTO `test` (name) VALUES ('myname')") if err != nil { log.Fatal("Database INSERT failed") }

log.Println("You called a thing!") w.WriteHeader(http.StatusOK) }

*****
Нужно начать с создания нового пакета, называемого app, для размещения структуры и ее методов. Структура приложения имеет два поля: маршрутизатор и база данных. В конце метода устанавливается код состояния.

Основной пакет и функция также нуждаются в нескольких изменениях, чтобы использовать новую структуру App. Нужно удалить функции postFunction и setupRouter из этого пакета, поскольку они теперь находятся в пакете приложения. Остается следующее:

package main

import ( "log" "net/http"

"github.com/gorilla/mux" "github.com/johan-lejdung/go-microservice-api-guide/rest-api/app" "github.com/johan-lejdung/go-microservice-api-guide/rest-api/db" )

func main() { database, err := db.CreateDatabase() if err != nil { log.Fatal("Database connection failed: %s", err.Error()) }

app := &app.App{ Router: mux.NewRouter().StrictSlash(true), Database: database, }

app.SetupRouter()

log.Fatal(http.ListenAndServe(":8080", app.Router)) }

Чтобы использовать новую структуру, нужно открыть базу данных и новый маршрутизатор. Затем необходимо вставить их оба в поля новой структуры App.

Теперь с базой данных есть соединение, которое будет использоваться одновременно во всех входящих вызовах API.

В качестве последнего шага следует добавить GET-Metod в настройку маршрутизатора и вернуть данные в формате JSON. Начать стоит с добавления структуры, чтобы заполнить данные, и отображения поля в JSON.

package app

import ( "time" )

type DbData struct { ID int `json:"id"` Date time.Time `json:"date"` Name string `json:"name"` }

С расширением app.go файла, с новым методом getFunction, который извлекает и записывает данные в ответ клиента, окончательный файл выглядит следующим образом:


package app

import ( "database/sql" "encoding/json" "log" "net/http"

"github.com/gorilla/mux" )

type App struct { Router *mux.Router Database *sql.DB }

func (app *App) SetupRouter() { app.Router. Methods("GET"). Path("/endpoint/{id}"). HandlerFunc(app.getFunction)

app.Router. Methods("POST"). Path("/endpoint"). HandlerFunc(app.postFunction) }

func (app *App) getFunction(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, ok := vars["id"] if !ok { log.Fatal("No ID in the path") }

dbdata := &DbData{} err := app.Database.QueryRow("SELECT id, date, name FROM `test` WHERE id = ?", id).Scan(&dbdata.ID, &dbdata.Date, &dbdata.Name) if err != nil { log.Fatal("Database SELECT failed") }

log.Println("You fetched a thing!") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(dbdata); err != nil { panic(err) } }

func (app *App) postFunction(w http.ResponseWriter, r *http.Request) { _, err := app.Database.Exec("INSERT INTO `test` (name) VALUES ('myname')") if err != nil { log.Fatal("Database INSERT failed") }

log.Println("You called a thing!") w.WriteHeader(http.StatusOK) }

Миграция базы данных
Окончательное дополнение к проекту. Когда база данных уже тесно связана с приложением или службой, можно избавиться от непостижимых головных болей, правильно обрабатывая миграции этой базы данных. Следующим шагом будет использование миграции для этого и расширение package db .

package db

import ( "database/sql" "fmt" "log" "os"

_ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate" "github.com/golang-migrate/migrate/database/mysql" _ "github.com/golang-migrate/migrate/source/file" )

func CreateDatabase() (*sql.DB, error) { // I shortened the code here. Here is where the DB setup were made. // In order to save some space I've removed the connection setup, but it can // be seen here: https://gist.github.com/johan-lejdung/ecea9dab9b9621d0ceb054cec70ae676#file-database_connect-go

if err := migrateDatabase(db); err != nil { return db, err }

return db, nil }

func migrateDatabase(db *sql.DB) error { driver, err := mysql.WithInstance(db, &mysql.Config{}) if err != nil { return err }

dir, err := os.Getwd() if err != nil { log.Fatal(err) }

migration, err := migrate.NewWithDatabaseInstance( fmt.Sprintf("file://%s/db/migrations", dir), "mysql", driver, ) if err != nil { return err }

migration.Log = &MigrationLogger{}

migration.Log.Printf("Applying database migrations") err = migration.Up() if err != nil && err != migrate.ErrNoChange { return err }

version, _, err := migration.Version() if err != nil { return err }

migration.Log.Printf("Active database version: %d", version)

return nil }

Сразу после открытия базы данных добавляется еще один вызов функции в migrateDatabase. Затем вставляется структура MigrationLogger для обработки журналов во время процесса. Миграции выполняются из обычных sql-запросов. Файлы миграции считываются из папки.

Каждый раз, когда база данных открыта, будут применены все непримененные миграции базы данных. Тем самым поддерживая базу данных в актуальном состоянии без какого-либо вмешательства со стороны.

Это в сочетании с файлом docker-compose, содержащим базу данных, делает разработку на нескольких машинах чрезвычайно простой.

Нерасполагаемый микросервис бесполезен, поэтому нужно добавить Dockerfile, чтобы упаковать приложение для удобного распространения

FROM golang:1.11 as builder WORKDIR $GOPATH/src/github.com/johan-lejdung/go-microservice-api-guide/rest-api COPY ./ . RUN GOOS=linux GOARCH=386 go build -ldflags="-w -s" -v RUN cp rest-api /

FROM alpine:latest COPY --from=builder /rest-api / CMD ["/rest-api"]


Оригинал можно посмотреть тут.

Код на github.