- PVSM.RU - https://www.pvsm.ru -

Язык Go для начинающих

Gopher

Цель этой статьи — рассказать о языке программирования 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 [1]. Он добавляет легкую прослойку для быстрого и удобного создания веб-приложений. Обратите внимание как импортируется этот пакет. Нужно указать путь к репозиторию откуда он был взят. А чтобы его поулчить, достаточно в консоли набрать команду 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 [2]» Марка Саммерфильда. И, конечно, больше практиковаться.

Автор: alehano

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/58955

Ссылки в тексте:

[1] Martini: https://github.com/go-martini/martini

[2] Программирование на языке Go: http://dmkpress.com/catalog/computer/programming/978-5-94074-854-0/

[3] Источник: http://habrahabr.ru/post/219459/