В go есть стандартный пакет net/http, который позволяет написать свой собственный веб-сервер.
Начнем с написания простейшего примера: сервер будет обслуживать всего одну статическую страницу,
на которой будет подсчитываться и выводиться счетчик ссылок:
package main
import (
"fmt"
"net/http"
)
type webCounter struct {
count chan int
}
func NewCounter() *webCounter {
counter := new(webCounter)
counter.count = make(chan int, 1)
go func() {
for i:=1 ;; i++ { counter.count <- i }
}()
return counter
}
func (w *webCounter) ServeHTTP(r http.ResponseWriter, rq *http.Request) {
if rq.URL.Path != "/" {
r.WriteHeader(http.StatusNotFound)
return
}
fmt.Fprintf(r, "You are visitor %d", <-w.count)
}
func main() {
err := http.ListenAndServe(":8000", NewCounter())
if err != nil {
fmt.Printf("Server failed: ", err.Error())
}
}
Компилируем:
go build
Запускаем откомпилированный бинарник, открываем броузер и набираем адрес:
http://localhost:8000/
и обновляем несколько раз страницу.
При каждом обновлении страницы будет срабатывать метод ServeHTTP(), и для обработки запроса
каждый раз будет генериться goroutine. Все они будут обслуживаться одним и тем же каналом
до окончания работы веб-сервера, поэтому счетчик не теряет свое значение.
Напишем веб-клиента, который будем делать запрос к нашему серверу и выводить содержимое заглавной страницы:
Структура http.Client содержит метод Get(url), которая получает содержимое этой страницы.
Стандартный пакет "text/template" позволяет использовать технологию шаблонов при генерации HTML.
В следующем примере имеется шаблон для главной страницы:
<html>
<head>
<title>Go Web Counter</title>
</head>
<body>
<h1>A Simple Example</h1>
<p>You are visitor: {{.Counter}}</p>
</body>
</html>
Соответственно меняется код самого сервера:
package main
import "fmt"
import "net/http"
import "text/template"
type webCounter struct {
count chan int
template *template.Template
}
func NewCounter() *webCounter {
counter := new(webCounter)
counter.count = make(chan int, 1)
go func() {
for i:=1 ;; i++ { counter.count <- i }
}()
counter.template, _ = template.ParseFiles("counter.html")
return counter
}
func (w *webCounter) ServeHTTP(r http.ResponseWriter, rq *http.Request) {
if rq.URL.Path != "/" {
r.WriteHeader(http.StatusNotFound)
return
}
w.template.Execute(r, struct{Counter int}{<-w.count})
}
func main() {
err := http.ListenAndServe(":8000", NewCounter())
if err != nil {
fmt.Printf("Server failed: ", err.Error())
}
}
Создается анонимная структура counter, хранящая счетчик. Она читает шаблон с диска и заполняет в нем {{.Counter}}
значением счетчика.
В следующем примере показано, как работают вложенные шаблоны. Имеется базовый шаблон base.html,
в который могут быть вложены два других - index.html либо about.html.
Исходники можно найти на гитхабе.
В одном каталоге с кодом сервера нужно создать подкаталог templates, куда положить 3 шаблона - base.html, index.html,
about.html. Код сервера:
В следующем примере дана реализация аутентификации на основе сессии с использованием куки.
Сервер будет обслуживать две страницы - корневую по умолчанию, и вторую - internal - куда пользователь попадает
после того, как набирает логин и пароль. Для этого примера нужно на гитхабе забрать тулкит под названием
горилла. Из этого тулкита понадобится два пакета -
mux и securecookie. Их можно установить с помощью команд:
go get github.com/gorilla/mux
go get github.com/gorilla/securecookie
после чего дать ссылку на установленные компоненты в виде
Можно пойти другим путем: в локальном каталоге, в котором будет лежать текст, приведенный ниже,
создать каталог gorilla и скопировать туда два подкаталога mux и securecookie вместе с файлами.
В примере регистрируются 4 хэндлера. Для логина и лог-аута разрешен пост. Функция loginHandler читает логин и пароль,
которые приходят из формы. Имя пишется в сессию и происходит редирект на internal страницу.
Если логин и пароль пусты, возвращаемся на главную. logoutHandle удаляет сессию и редиректит на главную.
setSession ложит логин и пароль в сессию, зашифрованное значение сессии сохраняется в куку.
getUserName читает куку. В примере используется т.н. client-side сессия. Другой вариант хранения сессии - в базе.
В следующем примере мы напишем веб-сервис и клиента к нему.
Материал взят отсюда.
Прежде всего нам понадобится пакет net/http, поскольку наш сервис будет работать по протоколу http.
Также нам понадобится пакет мартини.
Сервис будет выполнять основные прототипы функций для интерфейса гостевой книги: он позволяет добавлять записи в гостевую
книгу и читать их после добавления. Записи хранятся в памяти. Запись в гостевой книге представлена структурой:
type GuestBookEntry struct {
Id int
Email string
Title string
Content string
}
Сама гостевая книга представлена структурой, реализованной в форме коллекции:
type GuestBook struct {
guestBookData []*GuestBookEntry
}
Добавление записи в гостевую книгу:
func (g *GuestBook) AddEntry(email, title, content string) int
Как вы уже знаете, в go методу можно передать в качестве параметра интерфейс. В этом интерфейсе можно реализовать
целый список других методов и произвольный набор типов. Реализация интерфейса WebService,
в котором определены 4 метода, в частности методы для добавления, чтения и удаления записи в гостевой книге:
type WebService interface {
GetPath() string
// если параметр отсутсвует, удяляются все записи
WebDelete(params martini.Params) (int, string)
// реализация http-метода GET
// если параметр отсутствует, возвращаются все записи
WebGet(params martini.Params) (int, string)
// реализация http-метода POST method.
WebPost(params martini.Params, req *http.Request) (int, string)
}
Функция, регистрирующая вебсервис, в котором инициализируются эти 4 метода:
Здесь мы создаем 2 обьекта - мартини и гостевую книгу - и передаем эти обьекты в качестве параметров
для регистрации сервиса. Исходные коды примера лежат тут .
Распаковав архив, в корневом каталоге собираем сервер и клиента:
go build server.go
go build client.go
Запускаем сервер:
server
Открываем второй терминал и запускаем клиента со следующими параметрами, выполняя пост:
package main
import (
"fmt"
"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)
http.ListenAndServe(":8000", nil)
}
Функция handler импортирует из базового пакета http два обьекта - http.ResponseWriter и указатель на http.Request.
Функция http.HandleFunc будет перенаправлять все запросы для корня на хэндлер. Затем мы включаем прослушивание порта 8000,
не используя хэндлер. Системный пакет net/http написан все на том же go. Если глянуть на его исходники, то мы увидим,
что ResponseWriter - это интерфейс, в котором определены 3 функии:
type ResponseWriter interface {
// заголовок возвращает коллекцию типа map
Header() Header
// пишет данные для клиента после вызова WriteHeader()
Write([]byte) (int, error)
// отсылает клиенту заголовок вместе с кодом статуса
WriteHeader(int)
}
Хэндлер можно было бы расшифровать и переписать на более низком уровне так:
Иногда возникает необходимость после того, как респонс отдан клиенту, дописать в конец этого респонса что-то еще.
Для этого нужно вызвать функцию Write() этого самого респонса. Нужно создать структуру,
в которой будет одно поле - хэндлер, создать ссылку на обьект этой структуры, присвоив ее хэндлеру нужное значение,
и эту ссылку передать в нужный обработчик:
package main
import (
"net/http"
)
type AppendMiddleware struct {
handler http.Handler
}
func (a *AppendMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.handler.ServeHTTP(w, r)
w.Write([]byte(""))
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Success!"))
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("About !"))
}
func main() {
rmd := &AppendMiddleware{http.HandlerFunc(rootHandler)}
amd := &AppendMiddleware{http.HandlerFunc(aboutHandler)}
http.Handle("/", rmd)
http.Handle("/about/", amd)
http.ListenAndServe(":8000", nil)
}
А что делать, если нужно что-то добавить не в конец, а в начало респонса ?
Нам нужен буфер респонса, который можно модифицировать и потом передать в хэндлер.
Функция ResponseRecorder хранит этот буфер, и кроме этого хранит заголовки:
package main
import (
"net/http"
"net/http/httptest"
"strconv"
)
type ModifierMiddleware struct {
handler http.Handler
}
func (m *ModifierMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rec := httptest.NewRecorder()
// подменяем респонс на ResponseRecorder
m.handler.ServeHTTP(rec, r)
// копируем хидер респонса
for k, v := range rec.Header() {
w.Header()[k] = v
}
// добавляем свой собственный хидер
w.Header().Set("X-We-Modified-This", "Yup")
// status code
w.WriteHeader(418)
// вставляем в начало респонса нужный текст
data := []byte("Middleware says hello again. ")
// у заголовка изменился Content-Length
// нужно его пересчитать
clen, _ := strconv.Atoi(r.Header.Get("Content-Length"))
clen += len(data)
r.Header.Set("Content-Length", strconv.Itoa(clen))
// пишем то, что хотели добавить в начало
w.Write(data)
// пишем все остальное
w.Write(rec.Body.Bytes())
}
func myHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Success!"))
}
func main() {
mid := &ModifierMiddleware{http.HandlerFunc(myHandler)}
println("Listening on port 8000")
http.ListenAndServe(":8000", mid)
}
Обьект http.Request представляет из себя структуру,
в которой содержатся параметры клиентского запроса, данные и т.д:
type Request struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
}
Системная функция http.HandleFunc регистрирует хэндлер:
ServeMux - это http-мультиплексор или переключатель.
Он берет урл из каждого запроса и вызывает тот хэндлер, который ему соответствует. Урл может быть простым и составным.
Исходный код функции http.ListenAndServe -
она устанавливает tcp-коннект с keep-alive таймаутом,
создавая для каждого входящего коннекта новую goroutine:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
var tempDelay time.Duration // how long to sleep on accept failure
for {
rw, e := l.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c, err := srv.newConn(rw)
if err != nil {
continue
}
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve()
}
}
Пакет http имеет несколько встроенных хэндлеров, таких, как FileServer, NotFoundHandler, RedirectHandler.
Последний можно использовать для редиректа - в следующем примере мы создаем два новых обьекта - мультиплексор
и редирект-хэндлер, регистрируем мультиплексор и передаем его в качестве параметра - в результате при загрузке
корневой страницы сразу произойдет редирект:
В следующем примере мы рассмотрим, как делать вложенные хэндлеры. Пусть у нас имеются два хэндлера,
в каждом засекается время его выполнения, после чего это время логируется:
func aboutHandler(w http.ResponseWriter, r *http.Request) {
t1 := time.Now()
fmt.Fprintf(w, "You are on the about page.")
t2 := time.Now()
log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
t1 := time.Now()
fmt.Fprintf(w, "Welcome!")
t2 := time.Now()
log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
}
func main() {
http.HandleFunc("/about", aboutHandler)
http.HandleFunc("/", indexHandler)
http.ListenAndServe(":8080", nil)
}
Здесь мы видим повторное использование кода, от которого надо избавиться.
Надо написать хэндлер, который будет в качестве параметра принимать другой хэндлер,
чтобы это выглядело как-то так:
loggingHandler(indexHandler)
Для функции логирования мы создадим отдельный хэндлер, который в качестве параметра принимает другой хэндлер:
package main
import (
"net/http"
"time"
"log"
"fmt"
)
type Constructor func(http.Handler) http.Handler
// Chain - immutable коллекция хэндлеров
type Chain struct {
constructors []Constructor
}
// создает коллекцию Chain
func New(constructors ...Constructor) Chain {
c := Chain{}
c.constructors = append(c.constructors, constructors...)
return c
}
// функция возвращает из коллекции нужный хэндлер
// берет в качестве параметра хэндлер
// может вызываться несколько раз подряд,
// т.е. уровень вложенности хэндлеров может быть больше двух
func (c Chain) Then(h http.Handler) http.Handler {
var final http.Handler
if h != nil {
final = h
} else {
final = http.DefaultServeMux
}
for i := len(c.constructors) - 1; i >= 0; i-- {
final = c.constructors[i](final)
}
return final
}
// эта функция является оберткой для предыдущей функции
// берет в качестве параметра хэндлер-функцию
func (c Chain) ThenFunc(fn http.HandlerFunc) http.Handler {
if fn == nil {
return c.Then(nil)
}
return c.Then(http.HandlerFunc(fn))
}
func loggingHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
t1 := time.Now()
next.ServeHTTP(w, r)
t2 := time.Now()
log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
}
return http.HandlerFunc(fn)
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "You are on the about page.")
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome!")
}
func main() {
commonHandlers := New(loggingHandler)
http.Handle("/about/", commonHandlers.ThenFunc(aboutHandler))
http.Handle("/", commonHandlers.ThenFunc(indexHandler))
http.ListenAndServe(":8000", nil)
}
Загружаем веб-сервер и смотрим лог, при этом все логируется:
Если теперь в каком-то хэндлере случится непредвиденная серверная ошибка, она будет обработана новым хэндлером
и сервер не упадет, а будет работать дальше, только в логе появится сообщение.