Full stack проект на Go за неделю. День 2: Разработка бэкенд-сервера

Full stack проект на Go за неделю. День 2: Разработка бэкенд-сервера

Это вторая часть материала. Первая часть.

Сегодня мы начнём разработку бэкенда нашего приложения. Но перед этим мы должны определиться с языком программирования.

Выполнение задач

При разработке серверного приложения на Go, имейте в виду, что оно выполняется постоянно (есть исключения, но о них не будем говорить сейчас).
В предыдущей части мы говорили о том, что необходимо загружать курсы валют один раз в час — это очень простая задача для Go. С помощью стандартной библиотеки это можно сделать так:

go func() {
for {
updateCurrencyRates()
time.Sleep(1 * time.Hour)
}
}()

Не будем пока вдаваться в детали, просто знайте, что метод updateCurrencyRates() будет вызываться каждый час. Вот так просто.

Выбор языка программирования

Буду краток. Уже долгое время я с удовольствием программирую на Go — особенно он хорош в написании сервисов типа этого, поэтому и остановимся на Go 🙂

Вообще эту задачу можно выполнить практически на любом языке программирования/технологии. Вот первые, что приходят на ум:

  • PHP
  • Ruby (Ruby on Rails)
  • Node.js
  • Java
  • Scala
  • ASP.NET

… а на самом деле их намного больше. И у всех есть свою плюсы и минусы.

Преимущество Go для меня здесь в том, что Go более быстрый, многозадачный, удобнее в выполнении повторяющихся серверных задач (как писали выше) и без дополнительного инструментария типа cronjob.

Разработка

Здесь мы опишем только часть реализованного функционала, весь код этой части можно найти здесь. И так как наш проект написан как стандартный проект Go, его можно установить простой командой:

go get github.com/goingfullstack/currencyconverter

Структура проекта:

.
├── main.go
└── server
├── currency.go
├── data.go
├── handlers.go
├── server.go
└── webhook.go

Наша серверная часть разбита на функциональные модули, каждый из которых является частью package server.

В модуле server.go мы задаём константами параметры запуска нашего сервиса — адрес и порт, при этом устанавливаем их по-умолчанию на адрес http://127.0.0.1:4000. Вот структура нашего сервера:

type Server struct {
host string
port int

hasCurrencies bool // true если валюты успешно получены и обработаны
lastUpdateTime time.Time // время, взятое из данных в файле ECB
currencies map[string]float64 // данные валюты

mutex *sync.Mutex // для блокировки при использовании веб-хуков
webhooks map[string]webhook // для хранения веб-хуков
}

Метод создания нового сервера очень прост, мы всего лишь заполнили структуру переданными данными:

func New() (s *Server, err error) {
// берём данные из переменных среды
host := getEnv(HostEnvironment, defaultHost)
portStr := getEnv(PortEnvironment, defaultPort)
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("Error parsing port number: {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s", portStr)
}

// initialize internal variables
return &Server{
host: host,
port: port,

hasCurrencies: false,

mutex: &sync.Mutex{},
webhooks: make(map[string]webhook),
}, nil
}

И самая важная функция запуска сервера Run — после запуска сервера возвращает ошибки из http.ListenAndServe:

func (s *Server) Run() (err error) {
log.Printf("Starting server on {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s:{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d\n", s.host, s.port)

// запускает подзадачу обновления валют
s.startCurrencyUpdating()
return http.ListenAndServe(fmt.Sprintf("{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s:{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d", s.host, s.port), s)
}

Рассмотрим пару важных методов работы с данными — startCurrencyUpdating() и parseCurrencyData() в файле data.go:

func (s *Server) startCurrencyUpdating() {
log.Println("Starting currency fetching...")
go func() {
for {
log.Println("Starting new currency fetch...")

// устанавливаем время паузы
napTime := successSleepTime

if data, err := fetchCurrencyData(); err == nil {
if ts, curr, err2 := parseCurrencyData(data); err2 == nil {
// всё хорошо - обновляем данные по валютам
// заблокируем, когда закончим
s.mutex.Lock()
s.hasCurrencies, s.lastUpdateTime, s.currencies = true, ts, curr
s.mutex.Unlock()

log.Println("Currencies updated.")

// вызываем веб-хуки
go s.callWebhooks()
} else {
// ошибка - запишем в лог и уменьшим время до след. запуска
log.Println("Error parsing currency data:", err)
napTime = errorSleepTime
}
} else {
// ошибка - запишем в лог и уменьшим время до след. запуска
log.Println("Error fetching currency data:", err)
napTime = errorSleepTime
}

// всё сделали - пауза
log.Println("Sleeping", napTime)
time.Sleep(napTime)
}
}()
}

Здесь мы запускаем подпрограмму Go — goroutine с передачей ей структуры сервера, которая в бесконечном цикле (с настроенными перерывами — time.Sleep(napTime) ) получает данные с ECB.

и
func parseCurrencyData(data []byte) (ts time.Time, currencies map[string]float64, err error) {
// читаем файл, возврат при ошибке
var c currencyEnvelope
err = xml.Unmarshal(data, &c)
if err != nil {
return time.Time{}, nil, err
}

// читаем ещё раз для получения штампа времени, возврат при ошибке
var t timeEnvelope
err = xml.Unmarshal(data, &t)
if err != nil {
return time.Time{}, nil, err
}

// разбираем время, возврат при ошибке
ts, err = time.Parse(currencyDateFormat, t.Time.Time)
if err != nil {
return time.Time{}, nil, err
}

currencies = make(map[string]float64)

// вручную вставляем EUR как "1"
currencies[eur] = 1

// добавляем все курсы
for _, currency := range c.Cube {
currencies[currency.Name] = currency.Rate
}

return ts, currencies, nil
}

Ещё немного поясню — здесь мы читаем «сырые» данные от ECB, возвращаем время обновления курсов и map с курсами.

И немного про обработчик URL нашего приложения. Рассмотрим метод ServeHTTP() в файле handlers.go — с которого всё начинается. Он разбирает все запросы к серверу и вызывает соответствующий метод для каждого запроса:

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("[{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s] {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s\n", r.Method, r.RequestURI)

// если нет валют, вернём ошибку
if !s.hasCurrencies {
log.Println("No currencies, returning error!")
http.Error(w, "No currencies", http.StatusServiceUnavailable)
return
}

// выбираем нужный обработчик, и возвращаем ошибку на незнакомый URI
switch r.RequestURI {
case "/currencies":
s.currenciesHandler(w, r)
case "/convert":
s.convertHandler(w, r)
case "/webhook":
s.webhookHandler(w, r)
default:
http.NotFound(w, r)
}
}

В главном файле проекта, main.go, мы импортируем наш серверный модуль и создаём объект сервера: s, err := server.New(), а затем, если нет ошибок, запускаем его: err = s.Run().

Что дальше?

Прочитайте код, загрузите его себе и запустите!

Продолжение

Leave a Comment