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 мы задаём константами параметры запуска нашего сервиса — адрес и порт, при этом устанавливаем их по-умолчанию на адрес 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: %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 %s:%d\n", s.host, s.port)

	// запускает подзадачу обновления валют
	s.startCurrencyUpdating()
	return http.ListenAndServe(fmt.Sprintf("%s:%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("[%s] %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().

Что дальше?

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

Продолжение
0 комментариев
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.