Это вторая часть материала. Первая часть.
Сегодня мы начнём разработку бэкенда нашего приложения. Но перед этим мы должны определиться с языком программирования.
Выполнение задач
При разработке серверного приложения на 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().
Что дальше?
Прочитайте код, загрузите его себе и запустите!
Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.
Пишите: @ighar. Buy me a coffee, please :).