Разработка сервиса по укорачиванию ссылок (такого как 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
Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.
Пишите: @ighar. Buy me a coffee, please :).