Передо мной стояла задача разработать бэкэнд к мобильному сервису. Суть сервиса довольно проста. Мобильное приложение, которое показывает посты пользователей, находящихся рядом с текущим местоположением. На посты пользователи могут оставлять свои комментарии, которые тоже, в свою очередь, можно комментировать. Получается своеобразный гео-форум.Давно хотел попробовать применить язык 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, main.Буду приводить неполное содержание файлов, а только минимально необходимое для понимания.Пакет 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))
}
}
m.Get("/", controllers.Home)
m.Get("/api/v1/users", controllers.GetUsers)
m.Get("/api/v1/users/:id", controllers.GetUserById)
m.Post("/api/v1/users", controllers.CreateUser)
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"
)
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 NewPost() *Post {
return new(Post)
}
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) Create() (id string, err *conf.ApiError) {
switch {
case p.UserId == "":
err = conf.ErrUserIdEmpty
case p.Body == "":
err = conf.ErrPostBodyEmpty
case utf8.RuneCountInString(p.Body) > conf.MAX_POST_CHARS:
err = conf.ErrPostMaxSize
case p.Geo.Coordinates[0] == 0.0 || p.Geo.Coordinates[1] == 0.0:
err = conf.ErrPostLocationEmpty
}
if err != nil {
return
}
p.Id = bson.NewObjectId()
p.Geo.Type = "Point"
p.Enabled = true
p.Date = time.Now()
session := utils.NewDbSession()
defer session.Close()
c := session.Col("posts")
errDb := c.Insert(p)
if errDb != nil {
return "", conf.NewApiError(errDb)
}
return p.Id.Hex(), 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» Марка Саммерфильда. И, конечно, больше практиковаться.UPD: Tour Go на русском.