# Первые шаги на Go — Построение простого веб приложения с Neo4j

### Цель:
Цель этого поста создать простое веб приложение которое снимает структуру вашего сайта получая все ссылки которые содержит и сохраняет их в neo4j базу данных.
Идея проста - и в ней следующие шаги:
* Делаем запрос на URL
* парсим ответ
* Извлекаем ссылки из ответа
* Сохраняем извлеченные ссылки в  neo4j
* Повторяем 1 шаг с полученными ссылками пока не исследуем весь сайт
* Наконец используем Neo4j веб интерфейс чтобы посмотреть на структуру.
### Требования:
Эта статья подойдет начинающим. Будут приведены ссылки, каждый раз, когда будет преставленна новая идея. Для Neo4j, базовое знание графово ориентированной базы данных будет к месту. Предполагается что Go  и  Neo4j уже установленны на машинет.
## Создаем ползунок:
Теперь, когда у нас все есть. Начнем.
### Получение одной страницы из интеренета:
Время написать сожный код, которы поможет нам получить определенную страницу из интернета.
```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

type responseWriter struct{}

func main() {

	resp, err := http.Get("http://www.sfeir.com")

	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}

	rw := responseWriter{}
	io.Copy(rw, resp.Body)
}

func (responseWriter) Write(bs []byte) (int, error) {
	fmt.Printf(string(bs))

	return len(bs), nil
}
```

Мы начали с объявления главного пакета и импорта требуемых пакетов. Дальше, мы объявили структуру которая будет реализовывать `Writer` интерфейс. В `main` функции, мы собираемся присвоить множествую переменных значения. В основном. `http.Get` будет возвращать значения с  ответом и некоторой ошибкой, если что-то пойдет не так. Это общий способ обработки ошибок в Go программах.

Если вы посмотрите на документацию, вы найдете `Writer` интерфейс с одной функцией. Для того, чтобы реализовать этот интерфейс, нам нужно добавить получателя функции к нашей `responseWriter` структуре которая совпадает с функцией `Writer`.  Если вы пришли из Java вы должны ожидать синтаксис типа `реалзиация Writer`. Ну чтож, это не тот случай для Go, так как тут реализация происходит неявно.

Наконец, мы используем `io.Copy` для записи тела ответа в нашу переменную ответа. Следующий шаг это модификация нашего кода для извлечения ссылок из данного адреса страницы. После некоторого рефакторинга, у нас будет два фала.
Это `main.go`:
```go
package main

import (
	"fmt"
	"os"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	url := os.Args[1]

	retreive(url)
}
```

И этот `retreiver.go`:
```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

type responseWriter struct{}

func (responseWriter) Write(bs []byte) (int, error) {
	fmt.Printf(string(bs))

	return len(bs), nil
}

func retreive(uri string) {
	resp, err := http.Get(uri)

	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}

	rw := responseWriter{}
	io.Copy(rw, resp.Body)
}
```

Мы можем запустить это для небольшого вебсайта:
```bash
go run main.go retreiver.go http://www.sfeir.com
```
Теперь мы сделали наш первый шаг для создание ползунка. Есть возможность загрузить и спарсить данный адрес, открыть подключение прям к удлённому хосту, и получить html содержание.


### Создадим все гиперссыылки для одной страницы
Теперь начинается часть где нам нужно извлечь все ссылки из  html  документа. К сожалению, нет доступного для этого помошника для обработки  HTML в Go API. Поэтому мы должны посмотреть на  стороннюю  API. Давайте рассмотрим  `goquery`. Как вы можете догадаться, она  похоже на `jquery` только для Go. 

Пакет `goquery`  легко получается командой:
```bash
go get github.com/PuerkitoBio/goquery
```

```go
package main

import (
	"fmt"
	"os"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	url := os.Args[1]

	links, err := retrieve(url)

	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}

	for _, link := range links {
		fmt.Println(link)
	}
}
```

Я изменил нашу `retrieve` функцию, таким образом, чтобы она возвращала ссылки данные на странице.
```go
package main

import (
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

func retrieve(uri string) ([]string, error) {
	resp, err := http.Get(uri)
	if err != nil {
		fmt.Println("Error:", err)
		return nil, err
	}

	doc, readerErr := goquery.NewDocumentFromReader(resp.Body)
	if readerErr != nil {
		fmt.Println("Error:", readerErr)
		return nil, readerErr
	}
	u, parseErr := url.Parse(uri)
	if parseErr != nil {
		fmt.Println("Error:", parseErr)
		return nil, parseErr
	}
	host := u.Host

	links := []string{}
	doc.Find("a[href]").Each(func(index int, item *goquery.Selection) {
		href, _ := item.Attr("href")
		lu, err := url.Parse(href)
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		if isInternalURL(host, lu) {
			links = append(links, u.ResolveReference(lu).String())
		}

	})

	return unique(links), nil
}

// insures that the link is internal
func isInternalURL(host string, lu *url.URL) bool {

	if lu.IsAbs() {
		return strings.EqualFold(host, lu.Host)
	}
	return len(lu.Host) == 0
}

// insures that there is no repetition
func unique(s []string) []string {
	keys := make(map[string]bool)
	list := []string{}
	for _, entry := range s {
		if _, value := keys[entry]; !value {
			keys[entry] = true
			list = append(list, entry)
		}
	}
	return list
}
```
Как можно увидеть, наша `retrieve` функция стала существенно улучшена. Я убрал  `responseWriter` стуктуру  так как она больше не нужна из-за `goqeury` имеет свою реализацию интерфейса `Writer`. 
Я так же добавил две функции помошников. Первая - определяет  URL указывающий на внутреннюю страницу. Вторая - проверяет, что список не содержит дубликатов ссылок.

Вновь запустим программу для простого сайта:
```bash
go run main.go retreiver.go http://www.sfeir.com
```

### Получаем все гиперссылки для всего сайта.
Ура! Мы сделали большую работу. Следующее, мы собираемся посмотре, как улучшить `retrieve` функцию для того, чтобы получить ссылки с других страниц, в том числе. Я предлагю рассмотреть использование рекурсии. Мы создадим другую функцию под названием `crawl` и эта функция будет вызывать себя рекурсивно, с каждой следующей полученой ссылкой. Так же, нам нужно отслеживать посещенные страницы, чтоби избежать повторных переходов по ссылкам.
Прроверим:
```go
// part of retreiver.go
var visited = make(map[string]bool)

func crawl(uri string) {

	links, _ := retrieve(uri)

	for _, l := range links {
		if !visited[l] {
			fmt.Println("Fetching", l)
			visited[uri] = true
			crawl(l)
		}
	}
}
```
Теперь можно вызывать  `crawl` вместо `retrieve` функции в  `main.go`. Код будет следующим.
```go
package main

import (
	"fmt"
	"os"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	url := os.Args[1]

	crawl(url)

}
```
Запустим нашу программу:
```bash
go run main.go retreiver.go http://www.sfeir.com
```
### Реализуем слушателя событий через каналы. 
В прошлой части, мы увидели как полученные URL отображаются внутри `crawl` функции. Это не лучшее решение особенно когда вам нужно делать больеш чем просто вывод на экране. Чтобы это исправить, в основном, нам нужно реализовать слушателя событий для получения URL через каналы. 
Давайте посмотрим на это:
```go
// same imports
type link struct {
	source string
	target string
}

type retriever struct {
	events  map[string][]chan link
	visited map[string]bool
}

func (b *retriever) addEvent(e string, ch chan link) {
	if b.events == nil {
		b.events = make(map[string][]chan link)
	}
	if _, ok := b.events[e]; ok {
		b.events[e] = append(b.events[e], ch)
	} else {
		b.events[e] = []chan link{ch}
	}
}

func (b *retriever) removeEvent(e string, ch chan link) {
	if _, ok := b.events[e]; ok {
		for i := range b.events[e] {
			if b.events[e][i] == ch {
				b.events[e] = append(b.events[e][:i], b.events[e][i+1:]...)
				break
			}
		}
	}
}

func (b *retriever) emit(e string, response link) {
	if _, ok := b.events[e]; ok {
		for _, handler := range b.events[e] {
			go func(handler chan link) {
				handler <- response
			}(handler)
		}
	}
}

func (b *retriever) crawl(uri string) {

	links, _ := b.retrieve(uri)

	for _, l := range links {
		if !b.visited[l] {
			b.emit("newLink", link{
				source: uri,
				target: l,
			})
			b.visited[uri] = true
			b.crawl(l)
		}
	}
}

func (b *retriever) retrieve(uri string) ([]string, error) {
	resp, err := http.Get(uri)
	if err != nil {
		fmt.Println("Error:", err)
		return nil, err
	}

	doc, readerErr := goquery.NewDocumentFromReader(resp.Body)
	if readerErr != nil {
		fmt.Println("Error:", readerErr)
		return nil, readerErr
	}
	u, parseErr := url.Parse(uri)
	if parseErr != nil {
		fmt.Println("Error:", parseErr)
		return nil, parseErr
	}
	host := u.Host

	links := []string{}
	doc.Find("a[href]").Each(func(index int, item *goquery.Selection) {
		href, _ := item.Attr("href")
		lu, err := url.Parse(href)
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		if isInternalURL(host, lu) {
			links = append(links, u.ResolveReference(lu).String())
		}

	})

	return unique(links), nil
}

// same helper functions
```
Как можно увидеть, у нас есть дополнительные функции, для управлеия событиями для данного `retriever`. Для этого кода я использовал  `go` ключевое слово. В основном, написание `go foo()` запустит функцию `foo` запуститься асинхронно. В нашем случае мы использовали `go` которая является анонимной функцией, чтобы послать парамметр события(ссылку) всем слушателям через канал.
	Я указал тип канала данных как `link`, который содержит источник и целевую страницу. 

Теперь давайте взглянем на `main` функцию:
```go
package main

import (
	"fmt"
	"os"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	url := os.Args[1]
	ev := make(chan link)
	r := retriever{visited: make(map[string]bool)}
	r.addEvent("newLink", ev)

	go func() {
		for {
			l := <-ev
			fmt.Println(l.source + " -> " + l.target)
		}
	}()

	r.crawl(url)

}
```
Вновь используя `go` ключевое слово, в этот раз чтобы получать параметры события отправленные  `crawl` функцией. Если мы запустим нашу программу, теперь мы должны увидеть все внутренние ссылки для данного сайта.

Этого достаточно для ползунка.

## Интеграция с  Neo4j
Мы закончили с ползунком, давайте перейдем к части с Neo4j. Первая вещь, которую  мы собираемся сделать это установить драйвер.
```bash
go get github.com/neo4j/neo4j-go-driver/neo4j
```
После установки драйвера, нам нужно создать некую базовую функцию которая позволит нам работать с  Neo4j.
Создадим файл под названием  `neo4j.go`:
```go
package main

import (
	"github.com/neo4j/neo4j-go-driver/neo4j"
)

func connectToNeo4j() (neo4j.Driver, neo4j.Session, error) {

	configForNeo4j40 := func(conf *neo4j.Config) { conf.Encrypted = false }
	
	driver, err := neo4j.NewDriver("bolt://localhost:7687", neo4j.BasicAuth(
		"neo4j", "alice!in!wonderland", ""), configForNeo4j40)

	if err != nil {
		return nil, nil, err
	}

	sessionConfig := neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}
	session, err := driver.NewSession(sessionConfig)
	if err != nil {
		return nil, nil, err
	}

	return driver, session, nil
}

func createNode(session *neo4j.Session, l *link) (neo4j.Result, error) {
	r, err := (*session).Run("CREATE (:WebLink{source: $source, target: $target}) ", map[string]interface{}{
		"source": l.source,
		"target": l.target,
	})

	if err != nil {
		return nil, err
	}

	return r, err
}

func createNodesRelationship(session *neo4j.Session) (neo4j.Result, error) {
	r, err := (*session).Run("MATCH (a:WebLink),(b:WebLink) WHERE a.target = b.source CREATE (a)-[r:point_to]->(b)", map[string]interface{}{})

	if err != nil {
		return nil, err
	}

	return r, err
}
```
В основе, мы имеет три функции отвечающие за инициализацию подключения  Neo4j с базовым запросом. 
	Вам нужно поменять Neo4j конфигурацию для работы с локальной установкой.
    
Чтобы создать `WebLink` ноду нам просто нужно запустить следующий запрос:
```sql
CREATE (:WebLink{source: "http://www.sfeir.com/", target: "http://www.sfeir.com/en/services"})
```
Как только нода будет создана, нам нужно создать отношение между ними запустив следующий запрос:
```sql
MATCH (a:WebLink),(b:WebLink) 
WHERE a.target = b.source 
CREATE (a)-[r:point_to]->(b)
```
Давайте обновим нашу `main` функцию.
```go
package main

import (
	"fmt"
	"os"

	"github.com/neo4j/neo4j-go-driver/neo4j"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	driver, session, connErr := connectToNeo4j()

	if connErr != nil {
		fmt.Println("Error connecting to Database:", connErr)
		os.Exit(1)
	}

	defer driver.Close()

	defer session.Close()

	url := os.Args[1]
	ev := make(chan link)
	r := retriever{visited: make(map[string]bool)}
	r.addEvent("newLink", ev)

	go func(session *neo4j.Session) {
		for {
			l := <-ev
			fmt.Println(l.source + " -> " + l.target)
			_, err := createNode(session, &l)

			if err != nil {
				fmt.Println("Failed to create node:", err)
			}

		}
	}(&session)

	r.crawl(url)

	fmt.Println("Creation of relationship between nodes.. ")
	_, qErr := createNodesRelationship(&session)

	if qErr == nil {
		fmt.Println("Nodes updated")
	} else {
		fmt.Println("Error while updating nodes:", qErr)
	}

}
```
Использую три функциюю объявленные в `neo4j.go` наша программа создаст подключение к neo4j, подпишется на `newLink` событие для вставки нод и наконец обновит связи нод.
Я использовал `defer` ключево слово, чтобы сослаться на исполнение функции до тех пор пока не завершиштся `main` функция.
Давайте запустим в последний раз:
```bash
go run main.go retreiver.go neo4j.go http://www.sfeir.com
```
Чтобы проверить результат в  Neo4j вы можете запустить следующий запрос в вашем  Neo4j браузере.
```sql
MATCH (n:WebLink) RETURN count(n) AS count
```
Или этот запрос отобразит все ноды:
```sql
MATCH (n:WebLink) RETURN n
```

### Выводы
В этом посте, мы изучили множество свойств языка Golang включая множественные присвоения переменным, реализацию интерфейсов и каналов и горутин. Так же мы использовали стандартную библиотеку. Спасибо за чтение.