Создаём укорачивалку URL на Golang с Couchbase NoSQL

Создаём укорачивалку URL на Golang с Couchbase NoSQL
Разработка сервиса по укорачиванию ссылок (такого как TinyURL или Bitly) на Go, думаю, будет очень крутым примером для начинающих. Итак, приступим!

Подготовка

Для начала проверьте, что у вас установлен Go версии не ниже 1.7 и установите Couchbase Server 4.1+

Наше приложение будет использовать запросы N1QL — SQL запросы к базе данных Couchbase NoSQL.

Подготовка базы данных, создание модели данных

Для хранения информации о длинных и коротких URL нам нужна база данных. Для нашей несложной задачи лучшим вариантом будет выбор NoSQL базы, поэтому остановимся на БД с открытым исходным кодом Couchbase.

Скачайте и установите нужную версию для вашей операционной системы. Во время установки необходимо включить службу запросов (query service).

Для работы нам необходимо создать и настроить хранилище данных в Couchbase.


Так как мы будем использовать запросы N1QL, нам понадобится как минимум один индекс в хранилище. Его можно создать несколькими способами: с помощью Couchbase Query Workbench или через оболочку CBQ. Запрос создания индекса будет примерно таким:

CREATE PRIMARY INDEX ON `bucket-name` USING GSI;
Для больших приложений можно создать несколько индексных полей.


Перейдём к модели данных. Наше приложение будет принимать длинный URL и отдавать соответствующий короткий URL. Оба URL будут хранится в базе данных. Вот как примерно может выглядеть модель данных:

{
"id": "5Qp8oLmWX",
"longUrl": "https://www.thepolyglotdeveloper.com/2016/08/using-couchbase-server-golang-web-application/",
"shortUrl": "http://localhost:3000/5Qp8oLmWX"
}

id — это уникальный короткий хэш, привязанный к конкретному URL, он будет выдаваться автоматически для любого адреса.

Теперь начнём разработку приложения.

Создание RESTful приложения на Golang

Мы создадим RESTful API, но перед этим нужно определиться с логикой каждой конечной точки, а также позаботиться о надёжной работе приложения.

Создадим новый проект Go. Я назову его просто main.go и он будет расположен в $GOPATH/src/github.com/nraboy/shorturl. Добавьте следующий код в файл $GOPATH/src/github.com/nraboy/shorturl/main.go:

package main

import (
"net/http"
"github.com/couchbase/gocb"
"github.com/gorilla/mux"
)

var bucket *gocb.Bucket
var bucketName string

func ExpandEndpoint(w http.ResponseWriter, req *http.Request) { }

func CreateEndpoint(w http.ResponseWriter, req *http.Request) { }

func RootEndpoint(w http.ResponseWriter, req *http.Request) { }

func main() {
router := mux.NewRouter()
cluster, _ := gocb.Connect("couchbase://localhost")
bucketName = "example"
bucket, _ = cluster.OpenBucket(bucketName, "")
router.HandleFunc("/{id}", RootEndpoint).Methods("GET")
router.HandleFunc("/expand/", ExpandEndpoint).Methods("GET")
router.HandleFunc("/create", CreateEndpoint).Methods("PUT")
log.Fatal(http.ListenAndServe(":12345", router))
}

Рассмотрим подробно что мы сделали здесь. Мы импортировали Couchbase Go SDK и утилиту Mux, с помощью которой так легко создавать RESTful API. Установить эти пакеты можно так:

go get github.com/couchbase/gocb
go get github.com/gorilla/mux

Затем нам нужны две переменных, которые будут доступны во всём файле main.go, в них мы будем хранить копию открытого хранилища и название этого хранилища.

В методе main мы настраиваем роутер, соединяемся с локальным кластером Couchbase и открываем наше хранилище. В нашем случае открывается хранилище example, которое уже есть в кластере.

Далее мы создаём три роута, представляющие конечные точки API. Роут /create принимает длинный URL и отдаёт короткий. /expand делает обратное преобразование. И, наконец, роут /root принимает хэш и перекидывает на нужную страницу.

Логика API

Перед тем как создать логику, определим модель данных, это будет структура данных Go:

type MyUrl struct {
ID string `json:"id,omitempty"`
LongUrl string `json:"longUrl,omitempty"`
ShortUrl string `json:"shortUrl,omitempty"`
}

В структуре MyUrl есть три поля, представляющие свойством JSON.

Добавим самую сложную конечную точку /create:

func CreateEndpoint(w http.ResponseWriter, req *http.Request) {
var url MyUrl
_ = json.NewDecoder(req.Body).Decode(&url)
var n1qlParams []interface{}
n1qlParams = append(n1qlParams, url.LongUrl)
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE longUrl = $1")
rows, err := bucket.ExecuteN1qlQuery(query, n1qlParams)
if err != nil {
w.WriteHeader(401)
w.Write([]byte(err.Error()))
return
}
var row MyUrl
rows.One(&row)
if row == (MyUrl{}) {
hd := hashids.NewData()
h := hashids.NewWithData(hd)
now := time.Now()
url.ID, _ = h.Encode([]int{int(now.Unix())})
url.ShortUrl = "http://localhost:12345/" + url.ID
bucket.Insert(url.ID, url, 0)
} else {
url = row
}
json.NewEncoder(w).Encode(url)
}

Роут /create будет доступен через запрос PUT. В этом запросе будет указан длинный URL в формате JSON. Для удобства мы будем хранить весь JSON объект в объекте MyUrl.
Также необходимо убедится, что мы храним только уникальные длинные URL, а это значит, что каждый короткий URL должен быть также уникальным. Поэтому вначале мы проверяем базу данных на существование такого URL:

var n1qlParams []interface{}
n1qlParams = append(n1qlParams, url.LongUrl)
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE longUrl = $1")
rows, err := bucket.ExecuteN1qlQuery(query, n1qlParams)

Здесь мы используем параметризованный запрос N1QL для проверки. При ошибках в запросе мы выведем их на экран.

Если ошибок не будет, мы получим результат запроса и увидим, не пустой ли он. Если он пуст, значит нам нужно укоротить URL и сохранить в базе.

Можно разработать собственный алгоритм хэширования, но я предпочитаю использовать пакет Hashids.

Перед использованием, установим его:

go get github.com/speps/go-hashids
Для получения уникального короткого URL мы будем делать хэш из текущего времени:

hd := hashids.NewData()
h := hashids.NewWithData(hd)
now := time.Now()
url.ID, _ = h.Encode([]int{int(now.Unix())})

После получения уникального хэша сохраним его в MyUrl вместе с коротким URL. А длинный URL уже хранится в ней.

Перейдём к /expand, вот код для роута:

func ExpandEndpoint(w http.ResponseWriter, req *http.Request) {
var n1qlParams []interface{}
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE shortUrl = $1")
params := req.URL.Query()
n1qlParams = append(n1qlParams, params.Get("shortUrl"))
rows, _ := bucket.ExecuteN1qlQuery(query, n1qlParams)
var row MyUrl
rows.One(&row)
json.NewEncoder(w).Encode(row)
}

Здесь почти такой же код, как и у /create, но есть отличия: вместо N1QL запроса, мы передаём короткий URL и передаём параметры в запросе вместо того, чтобы передавать полный запрос.

Теперь остался роут root. Мы можем рассматривать все запросы к точке root как короткие URL:

func RootEndpoint(w http.ResponseWriter, req *http.Request) {
params := mux.Vars(req)
var url MyUrl
bucket.Get(params["id"], &url)
http.Redirect(w, req, url.LongUrl, 301)
}

После поиска по id, будет сделан 301 редирект на длинный URL.

Полный код проекта

package main

import (
"encoding/json"
"log"
"net/http"
"time"

"github.com/couchbase/gocb"
"github.com/gorilla/mux"
"github.com/speps/go-hashids"
)

type MyUrl struct {
ID string `json:"id,omitempty"`
LongUrl string `json:"longUrl,omitempty"`
ShortUrl string `json:"shortUrl,omitempty"`
}

var bucket *gocb.Bucket
var bucketName string

func ExpandEndpoint(w http.ResponseWriter, req *http.Request) {
var n1qlParams []interface{}
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE shortUrl = $1")
params := req.URL.Query()
n1qlParams = append(n1qlParams, params.Get("shortUrl"))
rows, _ := bucket.ExecuteN1qlQuery(query, n1qlParams)
var row MyUrl
rows.One(&row)
json.NewEncoder(w).Encode(row)
}

func CreateEndpoint(w http.ResponseWriter, req *http.Request) {
var url MyUrl
_ = json.NewDecoder(req.Body).Decode(&url)
var n1qlParams []interface{}
n1qlParams = append(n1qlParams, url.LongUrl)
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE longUrl = $1")
rows, err := bucket.ExecuteN1qlQuery(query, n1qlParams)
if err != nil {
w.WriteHeader(401)
w.Write([]byte(err.Error()))
return
}
var row MyUrl
rows.One(&row)
if row == (MyUrl{}) {
hd := hashids.NewData()
h := hashids.NewWithData(hd)
now := time.Now()
url.ID, _ = h.Encode([]int{int(now.Unix())})
url.ShortUrl = "http://localhost:12345/" + url.ID
bucket.Insert(url.ID, url, 0)
} else {
url = row
}
json.NewEncoder(w).Encode(url)
}

func RootEndpoint(w http.ResponseWriter, req *http.Request) {
params := mux.Vars(req)
var url MyUrl
bucket.Get(params["id"], &url)
http.Redirect(w, req, url.LongUrl, 301)
}

func main() {
router := mux.NewRouter()
cluster, _ := gocb.Connect("couchbase://localhost")
bucketName = "example"
bucket, _ = cluster.OpenBucket(bucketName, "")
router.HandleFunc("/{id}", RootEndpoint).Methods("GET")
router.HandleFunc("/expand/", ExpandEndpoint).Methods("GET")
router.HandleFunc("/create", CreateEndpoint).Methods("PUT")
log.Fatal(http.ListenAndServe(":12345", router))
}

После запуска приложения, оно будет принимать запросы на http://localhost:12345

Этот же урок в видео (на английском): https://youtu.be/OVBvOuxbpHA

По материалам: «Create A URL Shortener With Golang And Couchbase NoSQL» by Nic Raboy

Leave a Comment