Создаём укорачивалку 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.

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

CREATE PRIMARY INDEX ON `bucket-name` USING GSI;

Для больших приложений можно создать несколько индексных полей.

Разработка: couchbase-create-primary-index.gif
Перейдём к модели данных. Наше приложение будет принимать длинный 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))
}

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

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

По материалам: «Create A URL Shortener With Golang And Couchbase NoSQL» by Nic Raboy
0 комментариев
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.