Всем привет! Мы продолжаем серию статей, посвященных языку программирования Golang, которую наш внештатный автор написал в преддверии старта курса.
В прошлый раз мы посмотрели как Golang чувствует себя в рамках функциональной парадигмы — и надо сказать, чувствовал он себя не слишком уверенно (большую часть возможностей для использования функциональной парадигмы пришлось эмулировать). Сегодня мы продолжим программировать на Go, только теперь попробуем его в том, в чем он по идее должен быть хорош — в создании REST API сервиса, в котором можно получать данные по определенному запросу. В первой части я постараюсь объяснить более простую часть, в которой мы отправим данные, записанные в нашем Golang файле, в котором мы передадим обычный объект, например, статьи с заголовком, описанием и контентом. Во второй части статьи мы усложним данный процесс: запустим базу данных MySQL, сконнектимся с ней, получим данные, трансформируем их в JSON и успешно отдадим по GET — запросу.
Итак, что такое REST API? Если коротко — то это набор удаленных стандартных методов, которые возвращают различного рода данные (и не обязательно это будет именно приложение на фронте, в приложениях на Android эта технология применяется достаточно широко). Однако REST, как и БЭМ на фронте, нужен отчасти для того, что бы каждый член команды, даже если он первый день на работе, сразу понимал, как именно устроены у вас запросы, а не изобретал свой велосипед. Иными словам, REST — архитектура, которая выступает как некий стандарт, который неслучайно упомянут в таком количестве офферов от компаний.
Не хочу приводить дальнейшие определения REST в нашей статье, они и так просто везде, так что для быстрого введения можете прочитать тут. А так же мне очень понравилось вот это введение . Здесь все кратко, по делу и с множеством примеров. Ну а мы с вами начинаем делать наше приложение.
Итак, главный файл, в который мы можем складывать другие импорты, всегда называется
main.go
. Остальные должны туда только экспортироваться и вызываться. Итого, в любой директории мы создаем main.go
(я очень надеюсь, что golang вы уже скачали и установили вот отсюда ) и там начинаем творить наше приложение:package main
import (
"encoding/json" // Для работы с json
"fmt" // библиотека для вывода данных в терминале
"log" // для логирования
"net/http" // и для обработки http запросов
"github.com/gorilla/mux" // для упрощения работы с маршрутизацией
)
Итак, немного подробностей про каждый из пакетов.
encoding/json
— пакет из стандартной библиотеки, в котором можно кодировать и декодировать данные в JSON формате по стандарту RFC7159. fmt
— простая библиотека для ввода/вывода(I/O). log
— библиотека для логирования, она необходима для простого логирования ошибок и сообщений в нашем веб-приложении. net\http
— встроенный пакет для работы с протоколом http
. Gorilla вне сомнения, один из самых популярных фреймворков для веб-приложений на Go. Он позволит нам куда проще описать наши запросы, и методы http
, по которым они будут отправляться.Если вы только совсем недавно поставили Go, то будьте поаккуратнее с сочетаниями клавиш ctrl + s в VS Code — в данном случае форматер go любит удалять пакеты и переменные при отсутствии их использования.
Итак, в данном языке нет классов. Но мы можем с вами создать
struct
, в котором опишем, как в паттерне проектирования Конструктор, как именно должен выглядеть наш объект статьи:type Article struct {
Title string `json:"Title"`
ID int `json: id` //поля нашего struct неслучайно с большой буквы. Инача бы они не экспортировались
Desc string `json:"Desc"`
Content string `json:"Content"`
}
type Articles []Article // здесь у нас создается массив наших статей.
Дальше мы создадим функцию, которая будет выдавать, например, домашнюю страницу по запросу. В дальнейшем, признаюсь, от нее особого толка не будет — но она будет прекрасной иллюстрацией стандартной функции, которая обрабатывает request — response запросы.
func homePage(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "<h1>Элвис покинул здание </h1>") // я надеюсь флаги w и r уже знакомы нашему читателю
}
Окей, здорово. Но нам нужна функция, которая будет вызывать эту функцию на запрос и запускать сервер. Кроме того, нам был не помешал еще пакет с функциями диспетчера, который сделает за нас всю грязную работу… Хорошо, что у нас уже есть
gorilla/mux
:func handleRequests() {
myRouter := mux.NewRouter().StrictSlash(true) // ура роутинг
myRouter.HandleFunc("/", homePage) // ура роутер за нас все сделает и из коробки любые методы
Println(http.ListenAndServe(":8801", myRouter)) // запускаем наш сервер
}
Что же нам осталось сделать, что бы минимальный костяк сервера у нас заработал? Правильно, в Go без функции
main
никуда:func main() {
handleRequests()
}
Если вас прям срочно нужно поднять этот сверхмощный сервер и посмотреть, все ли работает, вводим в консоль:
go run main.go
Наш сервер должен запуститься, и по адресу
http://localhost:8810/
вы можете увидеть:Окей, но пока ничего такого интересного. Пришло время для функции, которая будет отдавать наши статейки в
json
:func allArticles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") //без заголовков данные посылать конечно можно, но читать иx тогда никто не будет
w.Header().Set("Access-Control-Allow-Origin", "*") // разрешаем чтение отовсюду
articles := Articles{
Article{Title: "О дельфинах", ID: 1, Desc: "дельфинчики клевые", Content: "Дельфи?ны — водные млекопитающие отряда китообразных."},
Article{Title: "О пингвинах", ID: 2, Desc: "Пингвины классные", Content: "Пингви?новые, или пингви?ны, — семейство нелетающих морских птиц, единственное современное в отряде пингвинообра?зных."},
} // какая статья на хабре обходится без экскурса в живую природу. А если по делу, делаем новый объекты Article
fmt.Println("Endpoint Hit:All articles") // подтверждение что у нас все работает.
json.NewEncoder(w).Encode(articles) // кодируем наш контент в JSON
}
Здорово, статьи с нами! Осталось их только отдать по запросу. Добавляем в наш
handleRequests()
новую строчку:myRouter.HandleFunc("/articles", allArticles).Methods("GET") // специализируем метод чтения
Теперь, если мы перезапустим наш сервер, то мы сможем получать наши данные, например, зайдя на страницу
http://localhost:8810/articles
. Однако было бы классно, например, сразу отправить данные Например, можно сделать запрос из нашего веб-приложения, которое создает страницу на JS, и получить данные:async function f() {
let response = await fetch('http://localhost:8801/articles');
if (response.ok) {
let json = await response.json();
let value = JSON.parse(json);
console.log(typeof(value));
console.log(value.length); //отладочные радости, что бы убедиться, что в каком виде пришло.
for (let i = 1; i < value.length; i++) {
draw(value[i]); // draw это функция которая отрисовывает наши статьи. Не думаю, что вы не сможете такое написать( и лучше конечно сразу на React)
}
} else {
console.log("Произошла ошибка HTTP: " + response.status);
console.warn("не стоит тестировать это на проде")
}
}
f(); // press F
Конечно, нормальный человек скачал бы Postman и посмотрел бы, как работает его запрос. Но в живом приложении, конечно, в сто раз интереснее.
Окей. Однако сейчас наше приложение поразительно уныло, и никакого толка от него нет — мы так все эти статьи могли в fixtures какие-нить записать с тем же успехом, и никакого сервера бы не понадобилось. Пора связать нашу красоту с базами данных. В качестве базы данных выступит MySQL. Мне показалось достаточно полезной данная книженция , хотя лучше всего, мне кажется, сразу сверяться с документацией пакета , который отвечает за коннект с базой данных. Итак, добавляем пакет и драйвер в наши импорты:
import (
"database/sql" // основной плагин для использования sql
_ "github.com/go-sql-driver/mysql" //драйвер для работы нашего sql
)
Кстати, забыл отметить. Пакеты не из стандартной библиотеки сначала нужно установить глобально, с помощью команды go get -u " название пакета".
Окей, и конечно же наша функция, которая отвечает за передачу всех статей, у нас сейчас изменится:
func allArticles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
sql := "SELECT * from articles" // выбираем все колонки из нашей таблицы articles
articles := rows
json.NewEncoder(w).Encode(articles)
}
В предыдущем отрывке кода для получения json мы использовали магическую функцию getJSON. Вы точно хотите узнать, что у нее внутри? Если что, вы сами попросили, я тут ни причем:
func getJSON(sqlString string) (string, error) {
db, err := sql.Open("mysql", "pavel:@tcp(127.0.0.1:3306)/testdb") //без пароля, имя пользователя - pavel, название базы данных - testdb
if err != nil {
log.Println(err.Error())
}
rows, err := db.Query(sqlString) // осуществляем запрос, он приходит аргументом
if err != nil {
return "", err
}
columns, err := rows.Columns()
if err != nil {
return "", err
}
count := len(columns)
tableData := make([]map[string]interface{}, 0) // да, мы читаем в пустые интерфейсы. И так делать не стоит, это bad practice. Но наш метод универсален, не универсальный лучше подсмотреть <a href="https://4gophers.ru/articles/go-i-sql-bazy-dannyh/#.Xdm5GZP7QUs">здесь</a>
values := make([]interface{}, count)
valuePtrs := make([]interface{}, count)
for rows.Next() {
for i := 0; i < count; i++ {
valuePtrs[i] = &values[i]
}
rows.Scan(valuePtrs...)
entry := make(map[string]interface{})
for i, col := range columns {
var v interface{}
val := values[i]
b, ok := val.([]byte)
if ok {
v = string(b)
} else {
v = val
}
entry[col] = v
}
tableData = append(tableData, entry) // там много всяких интерфейсов, поэтому просто скажу, что мы делаем таблицу, в которую потом переведем в JSON
}
jsonData, err := json.Marshal(tableData)
if err != nil {
return "", err
}
return string(jsonData), nil
}
Все кул, теперь осталось только специализировать наш запрос в
handleRequest()
:myRouter.HandleFunc("/articles", allArticles).Methods("GET")
Отлично! Теперь просто запустите, например, свой
MySQLWorkBranch
(да да, я пользуюсь интерфейсами, а не консолью) и создайте там схему testdb
, где можете создать таблицу articles
со столбцами id
, Title
, Desc
, Content
— и после наполнения их каким-то контентом (ну это элементарщину давайте вы сами найдете) вы сможете увидеть, как ваша система заработает. Я сознательно немного упростил запрос, чтобы материал стал проще. Но теперь вы можете поменять таблицу данных на более реальную, и посмотреть как работает ваш REST API c другими запросами. Как напишите и скомпилируете с помощью go build и задеплоите — все например, хвалят Google Cloud (это не реклама) за весьма небольшую плату (есть и бесплатная квота), или heroku
(там тоже есть бесплатный тариф, но по моему не настолько удобный).На этом все. Надеюсь, несмотря на пугающую часть обработки базы данных со множеством интерфейсов, язык немного приподнялся в ваших глазах. И несмотря на некую лексическую бедность, ждем GO 2 и надеемся, что мы в итоге получим компилируемый, быстрый и удобный, лексически богатый язык программирования со множеством сфер применения.
Итак, по традиции, полезные ссылочки:
- Тур по Golang. Вне сомнения, один из лучших способов сейчас выучить Go, если вы еще не заказали Head First Go на английском
- Go на примерах. Очень полезный справочник, если хотите что-то попробовать из возможностей языка — например, работу с JSON, корутины, и тому подобное
- Часть официальной документации, которая проведет вас через все необходимые шаги к работающему go приложению на Heroku
- Неплохой гайд, как настроить выдачу кастомных ошибок в Go