Цель этой статьи — рассказать о языке программирования Go (Golang) тем разработчикам, которые смотрят в сторону этого языка, но еще не решились взяться за его изучение. Рассказ будет вестись на примере реального приложения, которое представляет из себя RESTful API веб-сервис.
Передо мной стояла задача разработать бэкэнд к мобильному сервису. Суть сервиса довольно проста. Мобильное приложение, которое показывает посты пользователей, находящихся рядом с текущим местоположением. На посты пользователи могут оставлять свои комментарии, которые тоже, в свою очередь, можно комментировать. Получается своеобразный гео-форум.
Давно хотел попробовать применить язык Go для сколь нибудь серьезных проектов. Выбор был очевиден, благо что этот язык как нельзя лучше подходит для подобных задач.
Основные преимущества языка Go:
- Простой и понятный синтаксис. Это делает написание кода приятным занятием.
- Статическая типизация. Позволяет избежать ошибок, допущенных по невнимательности, упрощает чтение и понимание кода, делает код однозначным.
- Скорость и компиляция. Скорость у Go в десятки раз быстрее, чем у скриптовых языков, при меньшем потреблении памяти. При этом, компиляция практически мгновенна. Весь проект компилируется в один бинарный файл, без зависимостей. Как говорится, «просто добавь воды».
- Отход от ООП. В языке нет классов, но есть структуры данных с методами. Наследование заменяется механизмом встраивания. Полиморфизм реализуется интерфейсами, которые не нужно явно имплементировать, а лишь достаточно реализовать методы интерфейса.
- Параллелизм. Параллельные вычисления в языке делаются просто, изящно и без головной боли. Горутины (что-то типа потоков) легковесны, потребляют мало памяти.
- Богатая стандартная библиотека. Язык создавался с прицелом на веб-разработку, так что все необходимое есть уже из коробки. Количество сторонних библиотек постоянно растет. Кроме того, есть возможность использовать библиотеки C и C++.
- Возможность писать в функциональном стиле. В языке есть замыкания (closures) и анонимные функции. Функции являются объектами первого порядка, их можно передавать в качестве аргументов и использовать в качестве типов данных.
- Авторитетные отцы-основатели и сильное комьюнити. Роб Пайк, Кен Томпсон, Роберт Гризмер стояли у истоков. Сейчас у языка более 300 контрибьюторов. Язык имеет сильное сообщество и постоянно развивается.
- Open Source
- Обаятельный талисман
Все эти, и многие другие особенности позволяют выделить язык среди остальных. Это достойный кандидат на изучение, к тому же, освоить язык довольно просто.
Итак, вернемся к нашей задаче. Хоть язык и не накладывает ограничений на структуру проекта, данное приложение я решил организовать по модели MVC. Правда View реализовывается на стороне клиента. В моем случае это был AngularJS, в перспективе — нативное мобильное приложение. Здесь я расскажу лишь об API на стороне сервиса.
Структура проекта получилась следующая:
/project/
/conf/
errors.go
settings.go
/controllers/
posts.go
users.go
/models/
posts.go
users.go
/utils/
helpers.go
loctalk.go
Программа в Go разделяется на пакеты (package), что указывается в начале каждого файла. Имя пакета должно соответствовать директории в которой находятся файлы, входящие в пакет. Так же, должен быть главный пакет main с функцией main(). Он у меня находится в корневом файле приложения loctalk.go. Таким образом, у меня получилось 5 пакетов: conf, controllers, models, utils, mian.
Буду приводить неполное содержание файлов, а только минимально необходимое для понимания.
Пакет conf содержит константы и настройки сайта.
package conf
import (
"os"
)
const (
SITE_NAME string = "LocTalk"
DEFAULT_LIMIT int = 10
MAX_LIMIT int = 1000
MAX_POST_CHARS int = 1000
)
func init() {
mode := os.Getenv("MARTINI_ENV")
switch mode {
case "production":
SiteUrl = "http://loctalk.net"
AbsolutePath = "/path/to/project/"
default:
SiteUrl = "http://127.0.0.1"
AbsolutePath = "/path/to/project/"
}
}
Думаю, комментировать тут нечего. Функция init() вызывается в каждом пакете до вызова main(). Их может быть несколько в разных файлах.
Пакет main.
package main
import (
"github.com/go-martini/martini"
"net/http"
"loctalk/conf"
"loctalk/controllers"
"loctalk/models"
"loctalk/utils"
)
func main() {
m := martini.Classic()
m.Use(func(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
})
m.Map(new(utils.MarshUnmarsh))
Auth := func(mu *utils.MarshUnmarsh, req *http.Request, rw http.ResponseWriter) {
reqUserId := req.Header.Get("X-Auth-User")
reqToken := req.Header.Get("X-Auth-Token")
if !models.CheckToken(reqUserId, reqToken) {
rw.WriteHeader(http.StatusUnauthorized)
rw.Write(mu.Marshal(conf.ErrUserAccessDenied))
}
}
// ROUTES
m.Get("/", controllers.Home)
// users
m.Get("/api/v1/users", controllers.GetUsers)
m.Get("/api/v1/users/:id", controllers.GetUserById)
m.Post("/api/v1/users", controllers.CreateUser)
// …
// posts
m.Get("/api/v1/posts", controllers.GetRootPosts)
m.Get("/api/v1/posts/:id", controllers.GetPostById)
m.Post("/api/v1/posts", Auth, controllers.CreatePost)
// ...
m.Run()
}
В самом верху определяется имя пакета. Далее идет список импортируемых пакетов. Мы будем использовать пакет Martini. Он добавляет легкую прослойку для быстрого и удобного создания веб-приложений. Обратите внимание как импортируется этот пакет. Нужно указать путь к репозиторию откуда он был взят. А чтобы его поулчить, достаточно в консоли набрать команду go get github.com/go-martini/martini
Далее мы создаем экземпляр Martini, настраиваем и запускаем его. Обратите внимание на знак « := ». Это сокращенный синтаксис, он означает: создать переменную соответствующего типа и инициализировать ее. Например, написав a := «hello», мы создадим переменную a типа string и присвоим ей строку «hello».
Переменная m в нашем случае имеет тип *ClassicMartini, именно это возвращает martini.Classic(). * означает указатель, т. е. передается не само значение, а лишь указатель на него. В метод m.Use() мы передаем функцию-обработчик. Этот Middleware позволяет Martini делать определенные действия над каждым запросом. В данном случае, мы определяем Content-Type для каждого запроса. Метод m.Map() же позволяет привязать нашу структуру и использовать ее затем в контроллерах при необходимости (механизм dependency injection). В данном случае, я создал обертку для кодирования структуры данных в формат json.
Тут же мы создаем внутреннюю функцию Auth, которая проверяет авторизацию пользователя. Ее можно вставить в наши роуты и она будет вызываться до вызова контроллера. Эти вещи возможны благодаря Martini. С использованием стандартной библиотеки код получился бы немного другой.
Взглянем на файл errors.go пакета conf.
package conf
import (
"fmt"
"net/http"
)
type ApiError struct {
Code int `json:"errorCode"`
HttpCode int `json:"-"`
Message string `json:"errorMsg"`
Info string `json:"errorInfo"`
}
func (e *ApiError) Error() string {
return e.Message
}
func NewApiError(err error) *ApiError {
return &ApiError{0, http.StatusInternalServerError, err.Error(), ""}
}
var ErrUserPassEmpty = &ApiError{110, http.StatusBadRequest, "Password is empty", ""}
var ErrUserNotFound = &ApiError{123, http.StatusNotFound, "User not found", ""}
var ErrUserIdEmpty = &ApiError{130, http.StatusBadRequest, "Empty User Id", ""}
var ErrUserIdWrong = &ApiError{131, http.StatusBadRequest, "Wrong User Id", ""}
// … и т. д.
Язык поддерживает возврат нескольких значений. Вместо механизма try-catch, очень часто используется прием, когда вторым аргументом возвращается ошибка. И при ее наличии, она обрабатывается. Есть встроенный тип error, который представляет из себя интерфейс:
type error interface {
Error() string
}
Таким образом, чтобы реализовать этот интерфейс, достаточно иметь метод Error() string. Я создал свой тип для ошибок ApiError, который более специфичен для моих задач, однако совместим со встроенным типом error.
Обратите внимание на — type ApiError struct. Это определение структуры, модели данных, которую вы будете использовать постоянно в своей работе. Она состоит из полей определенных типов (надеюсь, вы успели заметить, что тип данных пишется после имени переменной). Кстати, полями могут быть другие структуры, наследуя все методы и поля. В одинарных кавычках `` указаны теги. Их указывать не обязательно. В данном случае они используются пакетом encoding/json для указания имени в выводе json (знак минус «-» вообще исключает поле из вывода).
Обратите внимание, что поля структуры написаны с заглавной буквы. Это означает, что они имеют область видимости за пределами пакета. Если написать их с прописной буквы, они экспортироваться не будут, а будут доступны только в пределах пакета. Это же относится и к функциям и методам. Вот такой простой механизм инкапсуляции.
Двигаемся дальше. Определение func (e *ApiError) Error() string означает ни что иное, как метод данной структуры. Переменная e — это указатель на структуру, своего рода self/this. Соответственно вызвав метод .Error() на структуре, мы получим ее поле Message.
Далее мы определяем предустановленные ошибки и заполняем их поля. Поля вида http.StatusBadRequest — это значения типа int в пакете http для стандартных кодов ответа, своего рода алиасы. Мы используем сокращенный синтаксис объявления структуры &ApiError{} с инициализацией. По другому можно было бы написать так:
MyError := new(ApiError)
MyError.Code = 110
// …
Символ & означает получить указатель на данную структуру. Оператор new() так же возвращает указатель, а не значение. По-началу возникает небольшая путаница с указателями, но, со временем, вы привыкните.
Перейдем к нашим моделям. Приведу урезанную версию модели постов:
package models
import (
"labix.org/v2/mgo/bson"
"loctalk/conf"
"loctalk/utils"
"time"
"unicode/utf8"
"log"
)
// GeoJSON format
type Geo struct {
Type string `json:"-"`
Coordinates [2]float64 `json:"coordinates"`
}
type Post struct {
Id bson.ObjectId `json:"id" bson:"_id,omitempty"`
UserId bson.ObjectId `json:"userId"`
UserName string `json:"userName"`
ThumbUrl string `json:"thumbUrl"`
ParentId bson.ObjectId `json:"parentId,omitempty" bson:",omitempty"`
Enabled bool `json:"-"`
Body string `json:"body"`
Geo Geo `json:"geo"`
Date time.Time `json:"date" bson:",omitempty"`
}
func (p *Post) LoadById(id string) *conf.ApiError {
if !bson.IsObjectIdHex(id) {
return conf.ErrPostIdWrong
}
session := utils.NewDbSession()
defer session.Close()
c := session.Col("posts")
err := c.Find(bson.M{"_id": bson.ObjectIdHex(id), "enabled": true}).One(p)
if p.Id == "" {
return conf.ErrPostNotFound
}
if err != nil {
return conf.NewApiError(err)
}
return nil
}
func (p *Post) Update() *conf.ApiError {
session := utils.NewDbSession()
defer session.Close()
c := session.Col("posts")
err := c.UpdateId(p.Id, p)
if err != nil {
return conf.NewApiError(err)
}
return nil
}
func (p *Post) Disable() *conf.ApiError {
session := utils.NewDbSession()
defer session.Close()
p.Enabled = false
c := session.Col("posts")
err := c.UpdateId(p.Id, p)
if err != nil {
return conf.NewApiError(err)
}
return nil
}
// …
Здесь мы используем замечательный драйвер для MongoDb — mgo, чтобы сохранять данные. Для удобства, я создал небольшую обертку над api mgo — utils.NewDbSession. Логика работы с данными: сначала мы создаем объект во внутренней структуре языка, а затем, с помощью метода этой структуры, сохраняем его в базу данных.
Обратите внимание, что в этих методах мы везде используем наш тип ошибки conf.ApiError. Стандартные ошибки мы конвертируем в наши с помощью conf.NewApiError(err). Так же, важен оператор defer. Он исполняется в самом конце выполнения метода. В данном случае, закрывает соединение с БД.
Что ж, осталось взглянуть на контроллер, который обрабатывает запросы и выводит json в ответ.
package controllers
import (
"encoding/json"
"fmt"
"github.com/go-martini/martini"
"labix.org/v2/mgo/bson"
"loctalk/conf"
"loctalk/models"
"loctalk/utils"
"net/http"
)
func GetPostById(mu *utils.MarshUnmarsh, params martini.Params) (int, []byte) {
id := params["id"]
post := models.NewPost()
err := post.LoadById(id)
if err != nil {
return err.HttpCode, mu.Marshal(err)
}
return http.StatusOK, mu.Marshal(post)
}
// ...
Здесь мы получаем из URL id запрашиваемого поста, создаем новый экземпляр нашей структуры и вызываем на ней метод LoadById(id) для загрузки данных из БД и заполнения данной структуры. Которую мы и выводим в HTTP ответ, предварительно преобразовав в json нашим методом mu.Marshal(post).
Обратите внимание на сигнатуру фукнции:
func GetPostById(mu *utils.MarshUnmarsh, params martini.Params) (int, []byte)
Входные параметры нам предоставляет Martini с помощью мехнихма внедрения зависимостей (dependency injection). И мы возвращаем два параметра (int, []byte) — число (статус ответа) и массив байт.
Итак, мы разобрали основные компоненты и подходы, используя которые, вы сможете сделать эффективный RESTful API интерфейс в короткие сроки. Надеюсь, статья была полезна и вдохновит некоторых из вас заняться изучением замечательного языка Go. Уверен, за ним будущее.
Для изучения могу порекомендовать хорошую книгу на русском «Программирование на языке Go» Марка Саммерфильда. И, конечно, больше практиковаться.
Автор: alehano