Go - это просто. Создаем HelloWorld веб-сервер
По многочисленным просьбам читателей DOU публикую первую статью про Go.
Ниже будут раскрыты следующие темы:
- как создать на Go простой веб-сервер, которому не нужны Apache с nginx’ом;
 - как добавить поддержку https без использования OpenSSL;
 - как перестать платить за TLS-сертификаты и беспокоиться об их своевременном обновлении.
 

Создаем HelloWorld веб-сервер на Go
Опустим разделы про установку и настройку Go. При желании можете почитать сами. Приступим сразу к делу :)
Создаем файл server.go и сохраняем в нем следующий код:
// Объявляем название пакета.
// Все *.go - файлы должны начинаться с названия пакета.
// Пакет с названием "main" имеет специальное назначение - он указывает
// компилятору go, что из этого пакета нужно собрать самодостаточный
// исполняемый файл. Это бинарник, которому для запуска не нужны дополнительные
// зависимости - его достаточно скопировать на нужный компьютер и запустить.
package main
// Импортируем пакеты, используемые в данном файле.
//
// Документацию по стандартным и сторонним пакетам легко найти по адресу
// https://godoc.org/<package_path>.
// Например,
//
//   * https://godoc.org/flag
//   * https://godoc.org/github.com/valyala/fasthttp
import (
	"flag"
	"github.com/valyala/fasthttp"
	"log"
)
// Объявляем глобальную переменную addr, куда будет записано значение параметра
// -addr при запуске программы.
//
// Например, параметр addr станет равным ":80" для следующей строки
// запуска:
//	./server -addr=:80
//
// Пропущенный IP в TCP адресе говорит о том, чтобы сервер "слушал"
// на всех доступных IP-адресах.
//
// flag.String указывает на то, что значение -addr - строка.
// flag.String принимает три аргумента:
//
//   * Название аргумента, который нужно распарсить. "addr" в данном случае.
//   * Значение аргумента по умолчанию. "127.0.0.1:8080" в данном случае.
//   * Описание аргумента, которое выводится при вызове программы
//     с параметром -help.
//
// flag.String возвращает указатель на строку, где хранится значение -addr.
var addr = flag.String("addr", "127.0.0.1:8080",
	"TCP address to listen to for incoming connections")
// main - функция, с которой начинается выполнение программы.
// Эта функцию должна находиться в package main.
func main() {
	// Парсим параметры, указанные в строке запуска программы.
	flag.Parse()
	// Конфигурируем http сервер.
	//
	// См. возможные параметры конфигурации
	// в https://godoc.org/github.com/valyala/fasthttp#Server
	s := fasthttp.Server{
		// Hanlder - функция-обработчик входящих http запросов.
		// См. код функции handler ниже.
		Handler: handler,
	}
	// Запускаем сервер.
	//
	// ListenAndServe принимает TCP адрес, где будет запущен сервер.
	// ListenAndServe возвращает результат только в двух случаях:
	//
	//   * Если во во время запуска сервера произошла ошибка.
	//     Например, указанный адрес уже занят другим сервером.
	//     Тогда соответствующая ошибка попадет в err.
	//   * Если сервер был остановлен. Тогда err будет равно nil.
	err := s.ListenAndServe(*addr)
	if err != nil {
		log.Fatalf("error in ListenAndServe: %s", err)
	}
}
// handler обрабатывает входящие запросы.
func handler(ctx *fasthttp.RequestCtx) {
	ctx.WriteString("Hello, world!\n")
}
Этот файл использует сторонний пакет — github.com/valyala/fasthttp , который нужно установить перед компиляцией. Сделаем это:
$ go get -u github.com/valyala/fasthttp
Исходники всех сторонних пакетов, полученные с помощью go get, сохраняются в папку $GOPATH/src/. Про $GOPATH можно почитать в официальной документации.
Теперь скомпилируем наш веб-сервер:
$ go build ./server.go
В текущем каталоге должен появиться исполняемый файл с именем server. Убедимся в этом:
$ ls -l | grep server -rwxrwxr-x 1 aliaksandr aliaksandr 6140200 May 7 19:39 server -rw-rw-r-- 1 aliaksandr aliaksandr 4020 May 7 19:31 server.go
Проверим, какие параметры он принимает:
$ ./server -help Usage of ./server: -addr string TCP address to listen to for incoming connections (default "127.0.0.1:8080")
Запустим его:
$ ./server
В отдельном окне убедимся с помощью nc, что сервер работает:
$ nc 127.0.0.1 8080 GET / HTTP/1.0 HTTP/1.1 200 OK Server: fasthttp Date: Sun, 07 May 2017 17:43:40 GMT Content-Type: text/plain; charset=utf-8 Content-Length: 14 Connection: close Hello, world!
Также можно открыть 127.0.0.1:8080 в браузере и убедиться, что сервер работает.
Проверим скорость его работы с помощью wrk.
Через одно подключение:
$ wrk -t 1 -c 1 http://127.0.0.1:8080/ Running 10s test @ http://127.0.0.1:8080/ 1 threads and 1 connections Thread Stats Avg Stdev Max +/- Stdev Latency 13.25us 24.26us 1.70ms 99.74% Req/Sec 76.71k 4.43k 81.43k 91.09% 771609 requests in 10.10s, 109.64MB read Requests/sec: 76399.78 Transfer/sec: 10.86MB
Через 1000 одновременных подключений:
$ wrk -t 2 -c 1000 http://127.0.0.1:8080/ Running 10s test @ http://127.0.0.1:8080/ 2 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 5.24ms 3.44ms 150.36ms 89.22% Req/Sec 82.85k 11.32k 110.93k 68.18% 1643377 requests in 10.01s, 233.52MB read Requests/sec: 164183.83 Transfer/sec: 23.33MB
Через 100 одновременных подключений, в каждом по 32 pipelined запроса:
$ wrk -t 2 -c 100 -s pipeline.lua http://127.0.0.1:8080 -- / 32 Running 10s test @ http://127.0.0.1:8080 2 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.40ms 1.46ms 39.22ms 90.68% Req/Sec 824.20k 75.28k 0.99M 78.50% 16469856 requests in 10.05s, 2.29GB read Requests/sec: 1638496.88 Transfer/sec: 232.83MB
Как видите, простой веб-сервер из пары десятков строчек на Go не требует ни nginx, ни Apache, и может обрабатывать более 1,6 млн запросов в секунду на обычном ноуте трехлетней давности.
Добавляем поддержку https
В стандартную поставку Go входит пакет crypto/tls, с помощью которого можно настраивать https на любой вкус и цвет. За разработку данного пакета отвечает Adam Langley, автор BoringSSL. Несколько фактов про crypto/tls:
- он написан на Go с чистого листа, без использования OpenSSL либо других сторонних TLS-библиотек;
 - в crypto/tls находится на порядок меньше дыр по сравнению с количеством уязвимостей в OpenSSL;
 - этот пакет активно используется в CloudFlare, которая регулярно вносит в него различные оптимизации и улучшения.
 
Если у вас уже есть TLS-сертификат и вы хотите побыстрее включить поддержку https, то просто замените следующую строку в server.go:
err := s.ListenAndServe(*addr)
На
err := s.ListenAndServeTLS(*addr, certFile, keyFile)
Где certFile и keyFile — пути к файлам сертификата и соответствующего ключа.
Если нужна дополнительная настройка https, например, как описано в статье Exposing Go on the Internet, то нужно немного повозиться:
package main
import (
	"crypto/tls"
	"flag"
	"github.com/valyala/fasthttp"
	"log"
	"net"
)
var (
	addr        = flag.String("addr", "127.0.0.1:8080", "TCP address to listen to for http")
	tlsAddr     = flag.String("tlsAddr", "", "TCP address to listen to for https")
	tlsCertFile = flag.String("tlsCertFile", "", "Path to TLS certificate file")
	tlsKeyFile  = flag.String("tlsKeyFile", "", "Path to TLS key file")
)
func main() {
	flag.Parse()
	// Пытаемся запустить https сервер
	startTLS()
	// Запускаем http сервер
	log.Printf("Serving http on -addr=%q", *addr)
	err := fasthttp.ListenAndServe(*addr, handler)
	if err != nil {
		log.Fatalf("error in ListenAndServe: %s", err)
	}
}
func startTLS() {
	if len(*tlsAddr) == 0 {
		log.Printf("-tlsAddr is empty, so skip serving https")
		return
	}
	// Читаем TLS сертификат из файла
	cert, err := tls.LoadX509KeyPair(*tlsCertFile, *tlsKeyFile)
	if err != nil {
		log.Fatalf("cannot load cert for -tlsCertFile=%q, -tlsKeyFile=%q: %s",
		*tlsCertFile, *tlsKeyFile, err)
	}
	// Создаем net.Listener'а, который принимает подключения по -tlsAddr.
	ln, err := net.Listen("tcp4", *tlsAddr)
	if err != nil {
		log.Fatalf("cannot listen for -tlsAddr=%q: %s", *tlsAddr, err)
	}
	// Создаем требуемую конфигурацию tls.
	// См. https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/ .
	tlsConfig := tls.Config{
		PreferServerCipherSuites: true,
		CurvePreferences: []tls.CurveID{
			tls.CurveP256,
			tls.X25519,
		},
		MinVersion: tls.VersionTLS12,
		CipherSuites: []uint16{
			tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
			tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
			tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
		},
		Certificates: []tls.Certificate{cert},
	}
	// Создаем net.Listener'а для tls подключений поверх созданного
	// выше net.Listener'а
	tlsLn := tls.NewListener(ln, &tlsConfig)
	// запускаем https сервер в отдельном потоке
	log.Printf("Serving https on -tlsAddr=%q", *tlsAddr)
	go fasthttp.Serve(tlsLn, handler)
}
func handler(ctx *fasthttp.RequestCtx) {
	ctx.WriteString("Hello, world!\n")
}
Теперь при указании параметров -tlsAddr, -tlsCertFile и -tlsKeyFile сервер будет принимать https-запросы на -tlsAddr дополнительно к http-запросам на -addr. И снова никаких nginx’ов c apache’ами и openssl’ами не нужно. Скорость обработки https-трафика сервером на Go сравнима со скоростью nginx, поэтому перед ним не нужно ставить TLS termination proxy.
Автоматизируем бесплатное получение и обновление TLS-сертификатов
Многие уже слышали про прекрасный сервис letsencrypt.org, который выдает всем желающим бесплатные TLS-сертификаты. И эти сертификаты признаются всеми современными браузерами. Ниже показано, насколько просто добавить поддержку автоматического получения и обновления TLS-сертификатов letsencrypt.org в наш сервер на Go:
package main
import (
	"crypto/tls"
	"flag"
	"github.com/valyala/fasthttp"
	"golang.org/x/crypto/acme/autocert"
	"log"
	"net"
)
var (
	addr        = flag.String("addr", "127.0.0.1:8080", "TCP address to listen to for http")
	tlsAddr     = flag.String("tlsAddr", "", "TCP address to listen to for https")
	tlsCertFile = flag.String("tlsCertFile", "", "Path to TLS certificate file. "+
		"The certificate is automatically generated and put "+
		"to -autocertCacheDir if empty")
	tlsKeyFile = flag.String("tlsKeyFile", "", "Path to TLS key file. "+
		"The key is automatically generated and put "+
		"to -autocertCacheDir if empty")
	autocertCacheDir = flag.String("autocertCacheDir", "autocert-cache",
		"Path to the directory where letsencrypt certs are cached")
)
func main() {
	flag.Parse()
	// Пытаемся запустить https сервер
	startTLS()
	// Запускаем http сервер
	log.Printf("Serving http on -addr=%q", *addr)
	err := fasthttp.ListenAndServe(*addr, handler)
	if err != nil {
		log.Fatalf("error in ListenAndServe: %s", err)
	}
}
func startTLS() {
	if len(*tlsAddr) == 0 {
		log.Printf("-tlsAddr is empty, so skip serving https")
		return
	}
	// Создаем net.Listener'а, который принимает подключения по -tlsAddr.
	ln, err := net.Listen("tcp4", *tlsAddr)
	if err != nil {
		log.Fatalf("cannot listen for -tlsAddr=%q: %s", *tlsAddr, err)
	}
	// Создаем требуемую конфигурацию tls.
	// См. https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/ .
	tlsConfig := tls.Config{
		PreferServerCipherSuites: true,
		CurvePreferences: []tls.CurveID{
			tls.CurveP256,
			tls.X25519,
		},
	}
	if len(*tlsCertFile) > 0 {
		// Читаем TLS сертификат из файла
		cert, err := tls.LoadX509KeyPair(*tlsCertFile, *tlsKeyFile)
		if err != nil {
			log.Fatalf("cannot load cert for -tlsCertFile=%q, -tlsKeyFile=%q: %s", *tlsCertFile, *tlsKeyFile, err)
		}
		tlsConfig.Certificates = []tls.Certificate{cert}
	} else {
		// Настраиваем автоматическое создание и обновление сертификатов.
		m := autocert.Manager{
			Prompt: autocert.AcceptTOS,
			// Сертификаты будут кэшироваться в -autocertCacheDir,
			// чтобы при рестарте сервера не приходилось
			// пересоздавать их снова.
			Cache: autocert.DirCache(*autocertCacheDir),
		}
		tlsConfig.GetCertificate = m.GetCertificate
	}
	// Создаем net.Listener'а для tls подключений поверх созданного
	// выше net.Listener'а
	tlsLn := tls.NewListener(ln, &tlsConfig)
	// запускаем https сервер в отдельном потоке
	log.Printf("Serving https on -tlsAddr=%q", *tlsAddr)
	go fasthttp.Serve(tlsLn, handler)
}
func handler(ctx *fasthttp.RequestCtx) {
	ctx.WriteString("Hello, world!\n")
}
Перед компиляцией данного кода понадобится скачать еще один сторонний пакет — golang.org/x/crypto/acme/autocert, который отвечает за автоматическое создание и обновление TLS-сертификатов:
$ go get -u golang.org/x/crypto/acme/autocert
Теперь сервер будет автоматически создавать и обновлять TLS-сертификаты для всех hostname’ов, запрошенных по https адресу -tlsAddr, если не указан -tlsCertFile. Выписанные сертификаты будут кэшироваться в каталоге -autocertCacheDir.
Заключение
Как вы могли убедиться, на Go можно легко и непринужденно создавать самодостаточные высокопроизводительные http- и https-серверы, которым не нужны никакие зависимости, включая Apache, nginx и OpenSSL. Код получается лаконичным и простым, без лишних абстракций и 
В статье рассмотрен простейший http-сервер, выдающий «Hello, world!». На Go можно создавать сервера и прокси с намного более сложной логикой. В качестве примера рекомендую оценить простой в использовании http-прокси, балансировщик нагрузки и TLS termination прокси, который также умеет экономить трафик — httptp.
Предлагайте в комментариях темы по Go для следующих статей.