Показаны сообщения с ярлыком Веб-приложение на Go. Показать все сообщения
Показаны сообщения с ярлыком Веб-приложение на Go. Показать все сообщения

суббота, 11 сентября 2021 г.

Разработка RESTful API с помощью Go и Gin

В этом посте представлены основы написания API RESTful веб-сервиса с помощью Go и Gin Web Framework (Gin).

Вы получите максимальную отдачу от этого руководства, если у вас будут базовые знания Go и его инструментов. Если это ваше первое знакомство с Go, см. Начало работы с Go для быстрого ознакомления.

Gin упрощает многие задачи кодирования, связанные с созданием веб-приложений, включая веб-сервисы. В этом руководстве вы будете использовать Gin для маршрутизации запросов, получения сведений о запросах и маршалинга JSON для ответов.

В этом руководстве вы создадите RESTful API сервер с двумя конечными точками. Ваш примерный проект будет репозиторием данных о винтажных джазовых записях.

Предпосылки

1. Установка Go 1.16 или новее. Инструкции по установке см. Установка Go.

2. Инструмент для редактирования вашего кода. Любой текстовый редактор, который у вас есть, будет работать нормально.

3. Командный терминал. Go хорошо работает с любым терминалом в Linux и Mac, а также с PowerShell или cmd в Windows.

4. Инструмент curl. В Linux и Mac это уже должно быть установлено. В Windows он входит в состав Windows 10 . Для более ранних версий Windows может потребоваться его установка.

Проектирование конечных точек API

Вы создадите API, открывающий доступ к магазину, где продаются винтажные записи на виниле. Поэтому вам нужно будет предоставить конечные точки, через которые клиент может получать и добавлять альбомы для пользователей.

При разработке API вы обычно начинаете с проектирования конечных точек. Пользователи вашего API добьются большего успеха, если конечные точки будут простыми для понимания.

Вот конечные точки, которые вы создадите в этом руководстве.

/albums

  • GET - получить список всех альбомов в формате JSON.
  • POST - добавить новый альбом из данных запроса, отправленных в формате JSON.

/albums/:id

  • GET - получить альбом по его идентификатору, вернув данные альбома в формате JSON.

Далее вы создадите папку для своего кода.

Создайте папку для вашего кода

Для начала создайте проект для кода, который вы напишете.

1. Откройте командную строку и перейдите в свой домашний каталог.

В Linux или Mac:

$ cd

В Windows:

C:\> cd %HOMEPATH%

2. Используя командную строку, создайте каталог для вашего кода с именем web-service-gin.

$ mkdir web-service-gin
$ cd web-service-gin

3. Создайте модуль, в котором вы сможете управлять зависимостями.

Запустите команду go mod init, указав ей путь к модулю, в котором будет находиться ваш код.

$ go mod init example/web-service-gin
go: creating new go.mod: module example/web-service-gin

Эта команда создает файл go.mod, в котором будут перечислены добавленные вами зависимости для отслеживания. Дополнительные сведения об именовании модуля с помощью пути к модулю см. в посте Управление зависимостями.

Далее вы создадите структуры данных для обработки данных.

Создайте данные

Чтобы упростить работу с руководством, вы будете хранить данные в памяти. Более типичный API будет взаимодействовать с базой данных.

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

Напишите код

Используя текстовый редактор, создайте файл с именем main.go в каталоге web-service-gin. Вы напишете свой код Go в этом файле.

В main.go в верхней части файла вставьте следующее объявление пакета.

package main

Автономная программа (в отличие от библиотеки) всегда находится в пакете main.

Под объявлением пакета вставьте следующее объявление структуры album. Вы будете использовать ее для хранения данных альбома в памяти.

Теги структуры, такие как json:"artist", определяют, каким должно быть имя поля, когда содержимое структуры сериализуется в JSON. Без них JSON использовал бы имена полей структуры с заглавной буквы - стиль, который не так распространен в JSON.

// album представляет данные об альбоме.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

Под только что добавленным объявлением структуры вставьте следующий срез album структур, содержащий данные, которые вы будете использовать для начала.

// Срез albums для заполнения данных об альбомах.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

Далее вы напишете код для реализации вашей первой конечной точки.

Напишите обработчик для возврата всех товаров

Когда клиент делает запрос в GET /albums, вы хотите вернуть все альбомы в формате JSON.

Для этого вам нужно написать следующее:

  • Логика для подготовки ответа
  • Код для сопоставления пути запроса с вашей логикой

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

Напишите код

Под кодом структур, который вы добавили в предыдущем разделе, вставьте следующий код, чтобы получить список альбомов.

Эта функция getAlbums создает JSON из среза album структур, записывая JSON в ответ.

// getAlbums возвращает список всех альбомов в формате JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

В этом коде вы:

  • Напишете функцию getAlbums, которая принимает параметр gin.Context. Обратите внимание, что вы могли дать этой функции любое имя - ни Gin, ни Go не требуют определенного формата имени функции.
    gin.Context - самая важная часть Gin. Он передает детали запроса, проверяет и сериализует JSON и многое другое. (Несмотря на похожее название, он отличается от встроенного в Go пакета context.)
  • Вызовете Context.IndentedJSON, чтобы сериализовать структуру в JSON и добавить ее в ответ.
    Первый аргумент функции - это код состояния HTTP, который вы хотите отправить клиенту. Здесь вы передаете константу StatusOK из пакета net/http, чтобы указать 200 OK.
    Обратите внимание, что вы можете заменить Context.IndentedJSON вызовом Context.JSON для отправки более компактного JSON. На практике с формой с отступом намного легче работать при отладке, и разница в размере обычно небольшая.

В верхней части main.go, сразу под объявлением среза albums, вставьте приведенный ниже код, чтобы назначить функцию-обработчик пути к конечной точке.

Это устанавливает связь, в которой getAlbums обрабатывает запросы к пути конечной точки /albums.

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)

    router.Run("localhost:8080")
}

В этом коде вы:

  • Инициализируете роутер Gin, используя Default.
  • Используете функцию GET, чтобы связать метод GET HTTP и путь /albums с функцией обработчика.
    Обратите внимание, что вы передаете имя функции getAlbums. Это отличается от передачи результата функции, которую вы бы сделали, передав getAlbums() (обратите внимание на круглые скобки).
  • Используйте функцию Run, чтобы подключить маршрутизатор к http.Server и запустить сервер.

В верхней части main.go, сразу под объявлением пакета, импортируйте пакеты, которые вам понадобятся для поддержки кода, который вы только что написали.

Первые строки кода должны выглядеть так:

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

Сохраните main.go.

Запустите код

Начните отслеживать модуль Gin как зависимость.

В командной строке используйте go get, чтобы добавить модуль github.com/gin-gonic/gin в качестве зависимости для вашего модуля. Используйте аргумент с точкой для обозначения "получить зависимости для кода в текущем каталоге".

$ go get .
go get: added github.com/gin-gonic/gin v1.7.2

Go разрешил и загрузил эту зависимость, чтобы удовлетворить декларацию импорта, которую вы добавили на предыдущем шаге.

Из командной строки в каталоге, содержащем main.go, запустите код. Используйте аргумент с точкой для обозначения "запустить код в текущем каталоге".

$ go run .

После запуска кода у вас есть работающий HTTP-сервер, на который вы можете отправлять запросы.

В новом окне командной строки используйте curl, чтобы сделать запрос к работающему веб-сервису.

$ curl http://localhost:8080/albums

Команда должна отображать данные, которыми вы заполнили сервис.

[
        {
                "id": "1",
                "title": "Blue Train",
                "artist": "John Coltrane",
                "price": 56.99
        },
        {
                "id": "2",
                "title": "Jeru",
                "artist": "Gerry Mulligan",
                "price": 17.99
        },
        {
                "id": "3",
                "title": "Sarah Vaughan and Clifford Brown",
                "artist": "Sarah Vaughan",
                "price": 39.99
        }
]

Вы запустили API! В следующем разделе вы создадите еще одну конечную точку с кодом для обработки POST запроса для добавления элемента.

Напишите обработчик для добавления нового элемента

Когда клиент отправляет запрос POST в /albums, вы хотите добавить альбом, описанный в теле запроса, к существующим данным альбомов.

Для этого вам нужно написать следующее:

  • Логика добавления нового альбома в существующий список.
  • Немного кода для маршрутизации запроса POST к вашей логике.

Напишите код

Добавьте код для добавления данных альбомов в список альбомов.

Где-нибудь после операторов импорта вставьте следующий код. (Конец файла - хорошее место для этого кода, но Go не обеспечивает соблюдение порядка, в котором вы объявляете функции.)

// postAlbums добавляет альбом из JSON, 
// полученного в теле запроса.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Вызов BindJSON для привязки 
    // полученного JSON к newAlbum
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Добавляем в срез новый альбом.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

В этом коде вы:

  • Используйте Context.BindJSON для привязки тела запроса к newAlbum.
  • Добавляете структуру album, инициализированную из JSON, в срез albums.
  • Добавляете в ответ код состояния 201 вместе с JSON, представляющим добавленный вами альбом.

Измените main функцию так, чтобы она включала функцию router.POST, как показано ниже.

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

В этом коде вы:

  • Связываете метод POST по пути /albums с функцией postAlbums.
    С помощью Gin вы можете связать обработчик с комбинацией HTTP-метода и пути. Таким образом, вы можете отдельно маршрутизировать запросы, отправленные по одному пути, в зависимости от метода, который использует клиент.

Запустите код

Если сервер все еще работает с последнего раздела, остановите его.

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .

Из другого окна командной строки используйте curl, чтобы сделать запрос к работающему веб-сервису.

$ curl http://localhost:8080/albums \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'

Команда должна отображать заголовки и JSON для добавленного альбома.

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 02 Jun 2021 00:34:12 GMT
Content-Length: 116

{
    "id": "4",
    "title": "The Modern Sound of Betty Carter",
    "artist": "Betty Carter",
    "price": 49.99
}

Как и в предыдущем разделе, используйте curl для получения полного списка альбомов, который вы можете использовать для подтверждения того, что новый альбом был добавлен.

$ curl http://localhost:8080/albums \
    --header "Content-Type: application/json" \
    --request "GET"

Команда должна отобразить список альбомов.

[
        {
                "id": "1",
                "title": "Blue Train",
                "artist": "John Coltrane",
                "price": 56.99
        },
        {
                "id": "2",
                "title": "Jeru",
                "artist": "Gerry Mulligan",
                "price": 17.99
        },
        {
                "id": "3",
                "title": "Sarah Vaughan and Clifford Brown",
                "artist": "Sarah Vaughan",
                "price": 39.99
        },
        {
                "id": "4",
                "title": "The Modern Sound of Betty Carter",
                "artist": "Betty Carter",
                "price": 49.99
        }
]

В следующем разделе вы добавите код для обработки GET для определенного элемента.

Напишите обработчик для получения определенного элемента

Когда клиент делает запрос к GET /album/[id], вы хотите вернуть альбом, идентификатор которого совпадает с параметром пути id.

Для этого вы:

  • Добавите логику для получения запрошенного альбома.
  • Укажите путь к логике.

Напишите код

Под функцией postAlbums, которую вы добавили в предыдущем разделе, вставьте следующий код для получения определенного альбома.

Эта функция getAlbumByID извлечет идентификатор из пути запроса, а затем найдет соответствующий альбом.

// getAlbumByID находит альбом, 
// значение идентификатора которого совпадает с 
// параметром id, отправленным клиентом,
// затем возвращает этот альбом в качестве ответа
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Перебираем список альбомов в поисках альбома,
    // значение идентификатора которого соответствует параметру.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

В этом коде вы:

  • Используете Context.Param, чтобы получить параметр пути id из URL-адреса. Когда вы сопоставляете этот обработчик с путем, вы включаете в путь заполнитель для параметра.
  • Проходите в цикле по структурам album в срезе, ища ту, значение поля ID которой совпадает со значением параметра id. Если он найден, вы сериализуете эту album структуру в JSON и возвращаете ее в качестве ответа с HTTP-кодом 200 OK.
    Как упоминалось выше, реальный сервис, скорее всего, будет использовать запрос к базе данных для выполнения этого поиска.
  • Вернуть ошибку HTTP 404 с http.StatusNotFound, если альбом не найден.

Наконец, измените свой main так, чтобы он включал новый вызов router.GET, где теперь путь - /albums/:id, как показано в следующем примере.

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

В этом коде вы:

  • Связываете путь /albums/:id с функцией getAlbumByID. В Gin двоеточие перед элементом в пути означает, что этот элемент является параметром пути.

Запустите код

Если сервер все еще работает с последнего раздела, остановите его.

Из командной строки в каталоге, содержащем main.go, запустите код, чтобы запустить сервер.

$ go run .

Из другого окна командной строки используйте curl, чтобы сделать запрос к работающему веб-сервису.

$ curl http://localhost:8080/albums/2

Команда должна отображать JSON для альбома, идентификатор которого вы использовали. Если альбом не найден, вы получите JSON с сообщением об ошибке.

{
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
}

Заключение

Вы только что использовали Go и Gin для написания простого RESTful веб-сервиса.

Итоговый код

В этом разделе содержится код приложения, которое вы создаете с помощью этого руководства.

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// album представляет данные об альбоме.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// Срез albums для заполнения данных об альбомах.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

// getAlbums возвращает список всех альбомов в формате JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums добавляет альбом из JSON, полученный в теле запроса.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Вызов BindJSON для привязки 
    // полученного JSON к newAlbum
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Добавляем в срез новый альбом.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID находит альбом, 
// значение идентификатора которого совпадает с 
// параметром id, отправленным клиентом,
// затем возвращает этот альбом в качестве ответа
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Перебираем список альбомов в поисках альбома,
    // значение идентификатора которого соответствует параметру.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}


Читайте также:


суббота, 23 марта 2019 г.

Пакет net/http, краткий обзор

Пакет http предоставляет реализации HTTP клиента и сервера.

Get, Head, Post, и PostForm выполняют HTTP (или HTTPS) запросы:

resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
  url.Values{"key": {"Value"}, "id": {"123"}})

Клиент должен закрывать тело ответа когда закончил с ним работать:

resp, err := http.Get("http://example.com/")
if err != nil {
  // handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...

Для контроля над клиентскими HTTP заголовками, политикой перенаправлений, и другими настройками создавайте Client:

client := &http.Client{
  CheckRedirect: redirectPolicyFunc,
}

resp, err := client.Get("http://example.com")
// ...

req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", W/"wyzzy")
resp, err := client.Do(req)
// ...

Для контроля над прокси, TLS конфигурацией, сохранения открытого соединения (keep-alive), сжатием, и другими настройками, создавайте Transport:

tr := &http.Transport{
  MaxIdleConns:       10,
  IdleConnTimeout:    30 * time.Second,
  DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")

Client и Transport безопасны для конкурентного использования несколькими go-процедурами (goroutines) и для эффективности должны быть созданы только однажды и повторно использоваться.

ListenAndServe стартует HTTP-сервер с заданным адресом и обработчиком (handler). Обработчик (handler) обычно равен nil, что означает использовать DefaultServeMux. Handle и HandleFunc добавляют обработчики к DefaultServeMux:

http.Handle("/foo", fooHandler)

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

log.Fatal(http.ListenAndServe(":8080", nil))

Больше контроля над поведением сервера доступно посредством создания пользовательского Server:

s := &http.Server{
  Addr:           ":8080",
  Handler:        myHandler,
  ReadTimeout:    10 * time.Second,
  WriteTimeout:   10 * time.Second,
  MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())

Начиная с Go 1.6, http пакет имеет прозрачную поддержку HTTP/2 протокола при использовании HTTPS. Программы которые должны отключить HTTP/2 могут сделать это посредством настройки Transport.TLSNextProto (для клиентов) или Server.TLSNextProto (для серверов) к не-nil, пустой map. В качестве альтернативы на данный момент поддерживаются следующие переменные окружения GODEBUG:

GODEBUG=http2client=0  # отключаем клиентскую поддержку HTTP/2
GODEBUG=http2server=0  # отключаем серверную поддержку HTTP/2
GODEBUG=http2debug=1   # включаем подробные HTTP/2 отладочные логи
GODEBUG=http2debug=2   # ... еще более подробные, с дампами фреймов

В пакете http Transport и Server оба автоматически включают HTTP/2 поддержку в простых конфигурациях. Для включения HTTP/2 для более сложных конфигураций, для использования низкоуровневых HTTP/2 возможностей, или для использования новой версии Go http2 пакета выполняйте import "golang.org/x/net/http2" напрямую и используйте его ConfigureTransport и/или ConfigureServer функции. Ручное конфигурирование HTTP/2 посредством golang.org/x/net/http2 пакета имеет первенство над встроенной поддержкой HTTP/2 в net/http пакете.


Читайте также:


воскресенье, 17 марта 2019 г.

Go примеры: Hello, gopher!

Сегодня я представляю вам пример простой веб-программы на Go. Программа принимает параметр name переданный в GET запросе и приветствует пользователя. В программе представлен пример обслуживания http-запроса с помощью пакета net/http. Также в программе использован пакет flag для обработки параметров командной строки, передаваемых программе при запуске - в нашем случае программа обрабатывает флаг port и прослушивает запросы по указанному порту.

package main

import (
 "flag"
 "fmt"
 "log"
 "net/http"
)

var PORT = flag.Int("port", 15001, "listen port")

func Hello(w http.ResponseWriter, r *http.Request) {

 keys, ok := r.URL.Query()["name"]

 if !ok || len(keys[0]) < 1 {
  log.Println("Url параметр 'name' пропущен")
  return
 }

 // Query()["key"] будет возвращать массив элементов,
 // нам нужен только один элемент.
 key := keys[0]

 log.Println("Url параметр 'name': " + string(key))

 message := "Hello, " + string(key) + "!"
 head := "" + message + ""
 response := "" + head + "" + message + ""

 w.Header().Set("Content-Type", "text/html")
 w.Write([]byte(response))
}

func main() {
 flag.Parse()
 addr := fmt.Sprintf(":%d", *PORT)
 log.Println("Программа запущена на порту " + addr)
 http.HandleFunc("/hello", Hello)
 err := http.ListenAndServe(addr, nil)
 if err != nil {
  log.Fatal("ListenAndServe: ", err)
 }
}

Пример сборки и запуска (из папки где находится файл hello.go с исходным кодом программы):

go build
./hello // для Linux

go build
hello.exe // для Windows

Пример запроса:


Читайте также:


среда, 6 февраля 2019 г.

Веб-приложение на Go: готовое приложение

Вот окончательный код всего нашего вики веб-приложения. Приложение состоит из трех файлов: wiki.go, edit.html. view.html.

Файл wiki.go

package main

import (
  "html/template"
  "io/ioutil"
  "log"
  "net/http"
  "regexp"
)

type Page struct {
  Title string
  Body  []byte
}

func (p *Page) save() error {
  filename := p.Title + ".txt"
  return ioutil.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
  filename := title + ".txt"
  body, err := ioutil.ReadFile(filename)
  if err != nil {
    return nil, err
  }
  return &Page{Title: title, Body: body}, nil
}

func viewHandler(w http.ResponseWriter, 
                 r *http.Request, title string) {
  p, err := loadPage(title)
  if err != nil {
    http.Redirect(w, r, "/edit/"+title, http.StatusFound)
    return
  }
  renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, 
                 r *http.Request, title string) {
  p, err := loadPage(title)
  if err != nil {
    p = &Page{Title: title}
  }
  renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, 
                 r *http.Request, title string) {
  body := r.FormValue("body")
  p := &Page{Title: title, Body: []byte(body)}
  err := p.save()
  if err != nil {
    http.Error(w, err.Error(), 
               http.StatusInternalServerError)
    return
  }
  http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

var templates = template.Must(
                   template.ParseFiles("edit.html", 
                                       "view.html"))

func renderTemplate(w http.ResponseWriter, 
                    tmpl string, p *Page) {
  err := templates.ExecuteTemplate(w, tmpl+".html", p)
  if err != nil {
    http.Error(w, err.Error(), 
               http.StatusInternalServerError)
  }
}

var validPath = regexp.MustCompile(
                   "^/(edit|save|view)/([a-zA-Z0-9]+)$")

func makeHandler(fn func(http.ResponseWriter, 
                 *http.Request, string)) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
      http.NotFound(w, r)
      return
    }
    fn(w, r, m[2])
  }
}

func main() {
  http.HandleFunc("/view/", makeHandler(viewHandler))
  http.HandleFunc("/edit/", makeHandler(editHandler))
  http.HandleFunc("/save/", makeHandler(saveHandler))

  log.Fatal(http.ListenAndServe(":8080", nil))
}

Файл edit.html

<h1>Editing {{.Title}}</h1>

<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">
{{printf "%s" .Body}}
</textarea></div>
<div><input type="submit" value="Save"></div>
</form>

Файл view.html

<h1>{{.Title}}</h1>

<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{printf "%s" .Body}}</div>

Скомпилируйте код и запустите приложение:

$ go build wiki.go
$ ./wiki

При открытии http://localhost:8080/view/ANewPage должна быть показана форма редактирования страницы.

Далее вводим текст, после нажатия «Сохранить» должно быть выполнено перенаправление на вновь созданную страницу.

Код приложения также доступен на Github https://github.com/A1esandr/golang-blog/tree/master/src/wiki


Читайте также:


Веб-приложение на Go: литералы функций и замыкания

Перехват условия ошибки в каждом обработчике вводит много повторяющегося кода. Что если бы мы могли обернуть каждый из обработчиков в функцию, которая делает валидацию и проверку ошибок? Литералы Go функций предоставляют мощные средства абстрагирования функциональности и это может помочь нам здесь.

Сначала мы переписываем определение функции каждого из обработчиков, чтобы принимать строку заголовка:

func viewHandler(w http.ResponseWriter, 
                 r *http.Request, title string)
func editHandler(w http.ResponseWriter, 
                 r *http.Request, title string)
func saveHandler(w http.ResponseWriter, 
                 r *http.Request, title string)

Теперь давайте определим функцию-обертку, которая принимает функцию верхнего типа и возвращает функцию типа http.HandlerFunc (подходит для передачи в функцию http.HandleFunc):

func makeHandler(fn func (http.ResponseWriter, 
                 *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Здесь мы извлечем заголовок страницы из запроса,
        // и вызываем предоставленный обработчик 'fn'
    }
}

Возвращенная функция называется замыканием, потому что она содержит значения, определенные вне функции. В этом случае переменная fn (единственный аргумент makeHandler) заключен в замыкание. Переменная fn будет одним из наших обработчиков сохранения, редактирования или просмотра.

Теперь мы можем взять код из getTitle и использовать его здесь (с некоторыми незначительными изменениями):

func makeHandler(fn func(http.ResponseWriter, 
                 *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        m := validPath.FindStringSubmatch(r.URL.Path)
        if m == nil {
            http.NotFound(w, r)
            return
        }
        fn(w, r, m[2])
    }
}

Замыкание, возвращаемое makeHandler, является функцией, которая принимает http.ResponseWriter и http.Request (другими словами, http.HandlerFunc). Замыкание извлекает title из пути запроса и проверяет его с помощью TitleValidator regexp. Если title недействителен, ошибка будет записана в ResponseWriter с помощью функции http.NotFound. Если title допустим, вложенная функция-обработчик fn будет вызываться с помощью ResponseWriter, Request и title в качестве аргументов.

Теперь мы можем обернуть функции-обработчики с помощью makeHandler в main, прежде чем они будут зарегистрированы в http пакете:

func main() {
    http.HandleFunc("/view/", makeHandler(viewHandler))
    http.HandleFunc("/edit/", makeHandler(editHandler))
    http.HandleFunc("/save/", makeHandler(saveHandler))

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Наконец, мы удаляем вызовы getTitle из функций обработчиков, делая их намного проще:

func viewHandler(w http.ResponseWriter, 
                 r *http.Request, title string) {
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, 
                      http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, 
                 r *http.Request, title string) {
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, 
                 r *http.Request, title string) {
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err := p.save()
    if err != nil {
        http.Error(w, err.Error(), 
                   http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, 
                  http.StatusFound)
}


Читайте также:


вторник, 5 февраля 2019 г.

Веб-приложение на Go: валидация

Как вы могли заметить, у нашего приложения есть серьезный недостаток безопасности: пользователь может предоставить произвольный путь для чтения/записи на сервере. Для снижения вероятности некорректного действия мы можем написать функцию для проверки заголовка с помощью регулярного выражения.

Сначала добавьте "regexp" в список import. Затем мы можем создать глобальную переменную для хранения нашей проверочного выражения:

var validPath = regexp.MustCompile(
                   "^/(edit|save|view)/([a-zA-Z0-9]+)$")

Функция regexp.MustCompile проанализирует и скомпилирует регулярное выражение и вернет regexp.Regexp. MustCompile отличается от Compile тем, что он вызывает panic, если компиляция выражения не удается, а Compile возвращает error в качестве второго параметра.

Теперь давайте напишем функцию, которая использует validPath выражение для проверки пути и извлечения заголовка страницы:

func getTitle(w http.ResponseWriter, 
              r *http.Request) (string, error) {
    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return "", errors.New("Invalid Page Title")
    }
    return m[2], nil // title это второе подвыражение.
}

Если заголовок действителен, он будет возвращен вместе с ошибкой со значением nil. Если заголовок недействителен, функция напишет ошибку «404 Not Found» для HTTP-соединения и вернет ошибку обработчику. Чтобы создать новую ошибку, мы должны импортировать errors пакет.

Давайте поместим вызов getTitle в каждом из обработчиков:

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, 
             "/edit/"+title, http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err = p.save()
    if err != nil {
        http.Error(w, err.Error(), 
                   http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, 
         "/view/"+title, http.StatusFound)
}


Читайте также:


Веб-приложение на Go: кэширование шаблонов

Код нашего приложения неэффективен: renderTemplate вызывает ParseFiles каждый раз, когда страница отображается. Лучшим подходом будет вызвать ParseFiles один раз в инициализации программы, сложить все шаблоны в один *Template. Тогда мы можем использовать метод ExecuteTemplate для визуализации конкретного шаблона.

Сначала мы создаем глобальную переменную с именем templates и инициализируем ее с ParseFiles.

var templates = template.Must(
                        template.ParseFiles("edit.html", 
                                            "view.html"))

Функция template.Must - это удобная оболочка, которая паникует когда передано ненулевое значение error, а в противном случае возвращает *Template без изменений. Здесь уместна паника; если шаблоны не могут быть загружены, единственное разумное, что нужно сделать, это выйти из программы.

Функция ParseFiles принимает любое количество строковых аргументов, которые идентифицируют наши файлы шаблонов и анализируют эти файлы в шаблоны, которые названы в честь базового имени файла. Если бы мы добавили больше шаблонов в нашу программу, мы бы добавили их имена к аргументам вызова ParseFiles.

Затем мы модифицируем функцию renderTemplate для вызова метода templates.ExecuteTemplate с именем соответствующего шаблона:

func renderTemplate(w http.ResponseWriter, 
                    tmpl string, p *Page) {
    err := templates.ExecuteTemplate(w, tmpl+".html", p)
    if err != nil {
        http.Error(w, err.Error(), 
                   http.StatusInternalServerError)
    }
}

Обратите внимание, что имя шаблона является именем файла шаблона, поэтому мы должны добавить ".html" к аргументу tmpl.


Читайте также:


Веб-приложение на Go: обработка ошибок

В нашей программе есть несколько мест, где ошибки игнорируются. Это плохая практика, не в последнюю очередь потому, что, когда ошибка происходит, программа будет иметь непреднамеренное поведение. Лучшее решение - обработать ошибки и вернуть сообщение об ошибке пользователю. Таким образом, если что-то пойдет не так, сервер будет функционировать именно так, как мы хотим, и пользователь может быть уведомлен.

Сначала давайте обработаем ошибки в renderTemplate:

func renderTemplate(w http.ResponseWriter, 
                    tmpl string, p *Page) {
    t, err := template.ParseFiles(tmpl + ".html")
    if err != nil {
        http.Error(w, err.Error(), 
                   http.StatusInternalServerError)
        return
    }
    err = t.Execute(w, p)
    if err != nil {
        http.Error(w, err.Error(), 
                   http.StatusInternalServerError)
    }
}

Функция http.Error отправляет указанный код HTTP ответа (в данном случае "Internal Server Error") и сообщение об ошибке. Решение о том, чтобы поместить обработку шаблонов в отдельную функцию, уже окупается.

Теперь давайте исправим saveHandler:

func saveHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/save/"):]
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err := p.save()
    if err != nil {
        http.Error(w, err.Error(), 
                   http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

О любых ошибках, возникающих во время p.save(), будет сообщено пользователю.


Читайте также:


Веб-приложение на Go: обработка несуществующих страниц, сохранение страниц

Обработка несуществующих страниц

Что если вы посетите /view/APageThatDoesntExist? Вы увидите страницу, содержащую HTML. Это потому, что он игнорирует возвращаемое значение ошибки из loadPage и продолжает пытаться заполнить шаблон без данных. Вместо этого, если запрошенная страница не существует, она должна перенаправлять клиента на страницу редактирования, чтобы создать контент:

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, 
                      http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}

Функция http.Redirect добавляет код статуса HTTP http.StatusFound(302) и Location заголовок к HTTP ответу.

Сохранение страниц

Функция saveHandler будет обрабатывать отправку форм, которые находятся на страницах редактирования. После раскомментирования соответствующей строки в main, давайте реализуем обработчик:

func saveHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/save/"):]
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    p.save()
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

Заголовок страницы (указан в URL) и единственное поле формы, Body хранятся на новой Page. Затем вызывается метод save() для записи данных в файл, и клиент перенаправляется на страницу /view/.

Значение, возвращаемое FormValue, имеет тип string. Мы должны преобразовать это значение в []byte, прежде чем оно уместится в структуре Page. Мы используем []byte(body) для выполнения преобразования.


Читайте также:


понедельник, 4 февраля 2019 г.

Веб-приложение на Go: редактирование страниц, пакет html/template

Вики это не настоящий вики, если нет возможности редактировать страницы. Давайте создадим два новых обработчика: один с именем editHandler для отображения формы редактирования страницы, и другой с именем saveHandler для сохранения данных, введенных через форму.

Сначала мы добавим их в main():

func main() {
    http.HandleFunc("/view/", viewHandler)
    http.HandleFunc("/edit/", editHandler)
    http.HandleFunc("/save/", saveHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Функция editHandler загружает страницу (или, если он не существует, создает пустую структуру Page), и отображает HTML форму.

func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    fmt.Fprintf(w, "<h1>Editing %s</h1>"+
        "<form action=\"/save/%s\" method=\"POST\">"+
        "<textarea name=\"body\">%s</textarea><br>"+
        "<input type=\"submit\" value=\"Save\">"+
        "</form>",
        p.Title, p.Title, p.Body)
}

Эта функция будет хорошо работать, но весь этот жестко закодированный HTML ужасен. Конечно, есть лучший способ.

Пакет html/template

Пакет html/template является частью стандартной библиотеки Go. Мы можем использовать html/template, чтобы хранить HTML в отдельном файле, что позволяет нам изменить макет нашей страницы редактирования без изменения основного Go кода.

Во-первых, мы должны добавить html/template в список импорта. Мы также не будем больше использовать fmt, поэтому мы должны удалить его.

import (
    "html/template"
    "io/ioutil"
    "net/http"
)

Давайте создадим файл шаблона, содержащий HTML форму. Откройте новый файл с именем edit.htmlи добавьте следующие строки:

<h1>Editing {{.Title}}</h1>

<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">
{{printf "%s" .Body}}
</textarea></div>
<div><input type="submit" value="Save"></div>
</form>

Измените editHandler, чтобы использовать шаблон вместо жестко заданного HTML кода:

func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    t, _ := template.ParseFiles("edit.html")
    t.Execute(w, p)
}

Функция template.ParseFiles будет читать содержимое edit.html и возвращать *template.Template.

Метод t.Execute выполняет шаблон, записывая сгенерированный HTML для http.ResponseWriter. Точечные идентификаторы .Title и .Body относятся к p.Title и p.Body.

Шаблонные директивы заключены в двойные фигурные скобки. Инструкция printf "%s" .Body является вызовом функции, которая выводит .Body в виде строки вместо потока байтов, такой же, как вызов fmt.Printf. Пакет html/template помогает гарантировать, что только безопасный и правильно выглядящий HTML генерируется действиями шаблона. Например, он автоматически экранирует знак «больше» (>), заменяя его с помощью &gt;, чтобы убедиться, что данные пользователя не повреждают HTML форму.

Поскольку сейчас мы работаем с шаблонами, давайте создадим шаблон для нашего viewHandler называемый view.html:

<h1>{{.Title}}</h1>

<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{printf "%s" .Body}}</div>

Измените viewHandler соответственно:

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    t, _ := template.ParseFiles("view.html")
    t.Execute(w, p)
}

Обратите внимание, что мы использовали почти одинаковый шаблонный код в обоих обработчиках. Давайте удалим это дублирование, переместив шаблонный код в свою собственную функцию:

func renderTemplate(w http.ResponseWriter, 
                    tmpl string, p *Page) {
    t, _ := template.ParseFiles(tmpl + ".html")
    t.Execute(w, p)
}

И измените обработчики для использования этой функции:

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}

Если мы закомментируем регистрацию нашего невыполненного обработчика сохранения в main, мы можем еще раз собрать и протестировать нашу программу. Взглянем на весь код, который мы уже написали.

package main

import (
  "html/template"
  "io/ioutil"
  "log"
  "net/http"
)

type Page struct {
  Title string
  Body  []byte
}

func (p *Page) save() error {
  filename := p.Title + ".txt"
  return ioutil.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
  filename := title + ".txt"
  body, err := ioutil.ReadFile(filename)
  if err != nil {
    return nil, err
  }
  return &Page{Title: title, Body: body}, nil
}

func renderTemplate(w http.ResponseWriter, 
                    tmpl string, p *Page) {
  t, _ := template.ParseFiles(tmpl + ".html")
  t.Execute(w, p)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
  title := r.URL.Path[len("/view/"):]
  p, _ := loadPage(title)
  renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
  title := r.URL.Path[len("/edit/"):]
  p, err := loadPage(title)
  if err != nil {
    p = &Page{Title: title}
  }
  renderTemplate(w, "edit", p)
}

func main() {
  http.HandleFunc("/view/", viewHandler)
  http.HandleFunc("/edit/", editHandler)
  //http.HandleFunc("/save/", saveHandler)
  log.Fatal(http.ListenAndServe(":8080", nil))
}


Читайте также:


воскресенье, 3 февраля 2019 г.

Веб-приложение на Go: использование net/http для обслуживания вики-страниц

Чтобы использовать пакет net/http, его необходимо импортировать:

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

Давайте создадим обработчик viewHandler, который позволит пользователям просмотреть страницу вики. Он будет обрабатывать URL с префиксом "/view/".

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", 
                p.Title, p.Body)
}

Опять же, обратите внимание на использование _ для игнорирования error, при возвращении значения из loadPage. Это сделано здесь для простоты и вообще считается плохой практикой. Мы займемся этим позже.

Во-первых, эта функция извлекает заголовок страницы из r.URL.Path, компонента пути URL запроса. Path повторно разрезается с помощью [len("/view/"):] для удаления ведущего компонента "/view/" пути запроса. Это связано с тем, что путь всегда начинается с "/view/", который не является частью заголовка страницы.

Затем функция загружает данные страницы, форматирует страницу со строкой простого HTML и записывает его в w, http.ResponseWriter.

Чтобы использовать этот обработчик, мы переписываем нашу функцию main, чтобы инициализировать http, используя viewHandler для обработки любых запросов по пути /view/.

func main() {
    http.HandleFunc("/view/", viewHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Взглянем на весь код веб-приложения, который мы уже написали.

package main

import (
  "fmt"
  "io/ioutil"
  "log"
  "net/http"
)

type Page struct {
  Title string
  Body  []byte
}

func (p *Page) save() error {
  filename := p.Title + ".txt"
  return ioutil.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
  filename := title + ".txt"
  body, err := ioutil.ReadFile(filename)
  if err != nil {
    return nil, err
  }
  return &Page{Title: title, Body: body}, nil
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
  title := r.URL.Path[len("/view/"):]
  p, _ := loadPage(title)
  fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", 
              p.Title, p.Body)
}

func main() {
  http.HandleFunc("/view/", viewHandler)
  log.Fatal(http.ListenAndServe(":8080", nil))
}

Давайте создадим некоторые данные страницы (как test.txt), скомпилируем наш код и попробуем открыть страницу вики.

Откройте файл test.txt в своем редакторе и сохраните строку "Hello world" (без кавычек) в нем.

$ go build wiki.go
$ ./wiki

(Если вы используете Windows, вы должны ввести wiki без ./ для запуска программы.)

При работающем веб-сервере при обращении к http://localhost:8080/view/test должна отобразиться страница с названием «test», содержащая слова «Hello world».


Читайте также:


Веб-приложение на Go: введение в пакет net/http, пример веб-сервера на Go

В качестве введения в пакет net/http рассмотрим полностью рабочий пример простого веб-сервера:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Функция main начинается с вызова http.HandleFunc, который сообщает пакету http обрабатывать все корневые веб запросы ("/") с помощью handler.

Затем он вызывает http.ListenAndServe, указывая, что он должен прослушивать порт 8080 на любом интерфейсе (":8080"). (Не беспокойтесь о втором параметре, nil, пока.) Эта функция будет блокироваться до завершения программы.

ListenAndServe всегда возвращает ошибку, поскольку она возвращается только тогда, когда случилась неожиданная ошибка. Чтобы записать эту ошибку в лог, мы заключаем вызов функции в log.Fatal.

Функция handler имеет тип http.HandlerFunc. Он принимает http.ResponseWriter и http.Request как его аргументы.

Значение http.ResponseWriter собирает ответ HTTP-сервера; написав в него, мы отправляем данные HTTP-клиенту.

http.Request - это структура данных, которая представляет клиентский HTTP-запрос. r.URL.Path является компонентом пути URL запроса. Конечный [1:] означает "создать под-срез Path от 1-го символа до конца." Это удаляет ведущий "/" из имени пути.

Если вы запустите эту программу и обратитесь к URL:

http://localhost:8080/monkeys

программа представит страницу, содержащую:

Hi there, I love monkeys!


Читайте также:


суббота, 2 февраля 2019 г.

Веб-приложение на Go: структуры данных, сохранение и загрузка страницы

Давайте начнем с определения структур данных. Вики (а мы собираемся реализовать как раз вики-приложение) состоит из серии взаимосвязанных страниц, каждая из которых имеет заголовок и тело (содержимое страницы). Здесь мы определяем Page как структуру с двумя полями, представляющими название и тело.

type Page struct {
    Title string
    Body  []byte
}

Тип []byte означает «byte срез». (См. Срезы) Элемент Body является []byte, а не string, потому что это тип, ожидаемый io библиотекой, которую мы будем использовать, как вы увидите далее.

Структура Page описывает, как данные страницы будут храниться в памяти. Но как насчет постоянного хранения? Мы можем решить эту проблему, создав метод save на Page:

func (p *Page) save() error {
    filename := p.Title + ".txt"
    return ioutil.WriteFile(filename, p.Body, 0600)
}

Сигнатура этого метода гласит: "Это метод с именем save, который принимает в качестве получателя p указатель на Page. Не принимает параметров и возвращает значение типа error."

Этот метод сохраняет Body структуры Page в текстовом файле. Для простоты мы будем использовать Title в качестве имени файла.

Метод save возвращает значение error, потому что это тип возвращаемого значения WriteFile (стандартная функция библиотеки, которая записывает срез байтов в файл). Метод save возвращает значение ошибки, чтобы позволить приложению обрабатывать ее, если что-то пойдет не так при записи файла. Если все пойдет хорошо, Page.save() вернет nil (нулевое значение для указателей, интерфейсов и некоторых других типов).

Восьмеричное целое число 0600, переданное в качестве третьего параметра WriteFile, указывает, что файл должен быть создан с разрешением на чтение и запись только для текущего пользователя.

Помимо сохранения страниц, мы также хотим загружать страницы:

func loadPage(title string) *Page {
    filename := title + ".txt"
    body, _ := ioutil.ReadFile(filename)
    return &Page{Title: title, Body: body}
}

Функция loadPage создает имя файла из параметра заголовка, читает содержимое файла в новую переменную body и возвращает указатель на Page, созданный с соотвествующим значением заголовка и тела.

Функции могут возвращать несколько значений. Стандартная функция библиотеки io.ReadFile возвращает []byte и error. В loadPage ошибка еще не обрабатывается; «пустой идентификатор» представленный символом подчеркивания (_) используется для удаления возвращаемого значения ошибки (по сути, присвоение значения ничему).

Но что произойдет, если ReadFile обнаружит ошибку? Например, файл может не существовать. Мы не должны игнорировать такие ошибки. Давайте изменим функцию для возврата *Page и error.

func loadPage(title string) (*Page, error) {
    filename := title + ".txt"
    body, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return &Page{Title: title, Body: body}, nil
}

Вызывающие эту функцию теперь могут проверять второй параметр; если это nil, то он успешно загрузил страницу. Если нет, то это будет error, которая может быть обработана вызывающей стороной.

На данный момент у нас есть простая структура данных и возможность сохранять и загружать из файла. Давайте напишем функцию main, чтобы проверить, то что мы написали:

func main() {
    p1 := &Page{Title: "TestPage", 
                Body: []byte("This is a sample Page.")}
    p1.save()
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))
}

После компиляции и выполнения этого кода файл с именем TestPage.txt будет создан, содержащий содержимое p1. Затем файл будет считан в структуру p2 и ее элемент Body будет выведен на экран.

Вы можете скомпилировать и запустить программу следующим образом:

$ go build wiki.go
$ ./wiki
This is a sample Page.

(Если вы используете Windows, вы должны ввести «wiki» без «./» для запуска программы.)

Взглянем на весь код, который мы уже написали.

package main

import (
  "fmt"
  "io/ioutil"
)

type Page struct {
  Title string
  Body  []byte
}

func (p *Page) save() error {
  filename := p.Title + ".txt"
  return ioutil.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
  filename := title + ".txt"
  body, err := ioutil.ReadFile(filename)
  if err != nil {
    return nil, err
  }
  return &Page{Title: title, Body: body}, nil
}

func main() {
  p1 := &Page{Title: "TestPage", 
              Body: []byte("This is a sample Page.")}
  p1.save()
  p2, _ := loadPage("TestPage")
  fmt.Println(string(p2.Body))
}


Читайте также:


Веб-приложение на Go: введение

В серии постов веб-приложение на Go мы осветим следующие разделы:

  • Создание структуры данных с использованием методов load и save
  • Использование пакета net/http для создания веб-приложений
  • Использование пакета html/template для обработки HTML шаблонов
  • Использование пакета regexp для проверки ввода пользователя
  • Использование замыканий

Начало работы

Вам потребуется компьютер с ОС Linux, OS X или Windows для запуска Go. Мы будем использовать $ для представления командной строки.

Установите Go.

Создайте новый каталог для нового приложения внутри GOPATH и перейдите к нему:

$ mkdir gowiki
$ cd gowiki

Создайте файл с именем wiki.go, откройте его в своем предпочитаемом редакторе и добавьте следующие строки:

package main

import (
    "fmt"
    "io/ioutil"
)

Мы импортируем пакеты fmt и ioutil из стандартной библиотеки Go. Позже, когда мы реализуем дополнительную функциональность, мы будем добавим больше пакетов в это объявление import.


Читайте также: