- Создавать модуль через
go mod initи разбивать код на пакеты - Пользоваться стандартными пакетами
strings,strconv,os,timeи другими - Писать и запускать табличные тесты через
go test - Строить HTTP-сервер, работающий с JSON, на
net/httpиencoding/json
Теперь ты знаешь почти все основные возможности Go. Соберём их вместе в реальном проекте: создадим собственный модуль, разобьём код на пакеты, напишем тесты и построим небольшой REST API сервер со списком задач. Причём для всего этого хватит одной стандартной библиотеки Go.
Модули и пакеты
Модуль — это проект с файлом go.mod. Команда go mod init example.com/tasks создаёт модуль, а example.com/tasks — это путь модуля, обычно адрес, где хранится код, например github.com/aysel/tasks. Внутри модуля каждая папка — это пакет, который импортируют как путь модуля + имя папки: example.com/tasks/store. Из другого пакета видны только имена, начинающиеся с заглавной буквы.
tasks/
├── go.mod
├── main.go
└── store/
├── store.go
└── store_test.gomodule example.com/tasks
go 1.22go указывает минимальную требуемую версию; у тебя может стоять более новая.| Команда | Что делает |
|---|---|
go mod init example.com/tasks | создаёт новый модуль |
go get golang.org/x/text | добавляет сторонний пакет и записывает его в go.mod |
go mod tidy | добавляет недостающие зависимости и удаляет лишние |
go build ./... | собирает все пакеты модуля (./... — эта папка и все вложенные) |
go vet ./... | ищет подозрительный код: неверные спецификаторы, копии мьютексов |
Стандартная библиотека
Стандартная библиотека Go очень богата: текст, числа, файлы, время, JSON, HTTP-сервер и клиент, криптография, тестирование — всё это поставляется вместе с языком. Поэтому многие небольшие и средние проекты пишутся вообще без сторонних зависимостей.
| Пакет | Для чего |
|---|---|
fmt | форматированный вывод и сборка строк |
strings, strconv | работа со строками, преобразование строка ↔ число |
os | файлы, переменные окружения, аргументы |
time | даты, время и длительности |
encoding/json | кодирование значений в JSON и декодирование JSON |
net/http | HTTP-сервер и клиент |
testing | автоматические тесты |
package main
import (
"fmt"
"strconv"
"strings"
"time"
)
func main() {
fmt.Println(strings.Split("go,java,sql", ","))
fmt.Println(strings.Repeat("ab", 3), strings.HasPrefix("golang", "go"))
n, err := strconv.ParseFloat("2.5", 64)
fmt.Println(n*2, err)
d := 90 * time.Minute
fmt.Println(d, d.Hours())
day := time.Date(2026, time.September, 27, 0, 0, 0, 0, time.UTC)
fmt.Println(day.Weekday(), day.Format("02.01.2006"))
}[go java sql] ababab true 5 <nil> 1h30m0s 1.5 Sunday 27.09.2026
Проект: пакет store и тесты
Логику хранения задач выносим в отдельный пакет store. Теги вроде json:"id" у полей Task сообщают пакету encoding/json, какие имена использовать в JSON. Нулевое значение Store сразу готово к работе — конструктор не нужен. Это любимый принцип Go: «сделай нулевое значение полезным».
package store
import (
"errors"
"strings"
"sync"
)
var ErrEmptyTitle = errors.New("empty title")
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
}
type Store struct {
mu sync.Mutex
tasks []Task
}
func (s *Store) Add(title string) (Task, error) {
title = strings.TrimSpace(title)
if title == "" {
return Task{}, ErrEmptyTitle
}
s.mu.Lock()
defer s.mu.Unlock()
t := Task{ID: len(s.tasks) + 1, Title: title}
s.tasks = append(s.tasks, t)
return t, nil
}
func (s *Store) All() []Task {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]Task, len(s.tasks))
copy(out, s.tasks)
return out
}All возвращает не сам внутренний срез, а его копию, поэтому вызывающий код не сможет менять список в обход мьютекса. Имя тестового файла оканчивается на _test.go, и он лежит в той же папке, что и тестируемый пакет. Тестовая функция начинается со слова Test и принимает *testing.T: t.Errorf фиксирует ошибку и продолжает работу, а t.Fatalf сразу останавливает тест. В Go чаще всего пишут табличные тесты (table-driven tests): проверки перечисляются в срезе структур, а один цикл запускает каждую из них как отдельный подтест через t.Run.
package store
import (
"errors"
"testing"
)
func TestAdd(t *testing.T) {
tests := []struct {
name string
title string
want string
wantErr error
}{
{"simple", "Buy bread", "Buy bread", nil},
{"trims spaces", " Learn Go ", "Learn Go", nil},
{"empty", " ", "", ErrEmptyTitle},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var s Store
got, err := s.Add(tt.title)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("Add(%q) error = %v, want %v", tt.title, err, tt.wantErr)
}
if got.Title != tt.want {
t.Errorf("Add(%q).Title = %q, want %q", tt.title, got.Title, tt.want)
}
})
}
}$ go test ./...
? example.com/tasks [no test files]
ok example.com/tasks/store 0.004sgo test -v ./store показывает каждый подтест (TestAdd/trims_spaces — пробелы заменяются подчёркиваниями), а go test -run TestAdd/empty ./store запускает только один случай.if got.Title != "Learn Go" {
t.Error("wrong title")
}if got.Title != tt.want {
t.Errorf("Add(%q).Title = %q, want %q",
tt.title, got.Title, tt.want)
}got X, want Y: когда тест падает, причина видна без открытия кода.HTTP-сервер: net/http
В main.go функция http.NewServeMux() создаёт маршрутизатор. Начиная с Go 1.22 HTTP-метод можно указать прямо в маршруте: "GET /tasks" и "POST /tasks". Каждый обработчик (handler) принимает два параметра: http.ResponseWriter для записи ответа и входящий запрос *http.Request. json.NewEncoder(w).Encode(...) превращает значение в JSON и пишет его в ответ, а json.NewDecoder(r.Body).Decode(&in) читает тело запроса. Порт берётся из переменной окружения PORT (os.Getenv), а если её нет, используется 8080.
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"example.com/tasks/store"
)
func main() {
var tasks store.Store
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks.All())
})
mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {
var in struct {
Title string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
task, err := tasks.Add(in.Title)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(task)
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Println("listening on :" + port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}Запусти сервер командой go run . и отправляй запросы из другого терминала с помощью curl. POST создаёт новую задачу и возвращает JSON со статусом 201 Created, пустой заголовок даёт 400 Bad Request, а GET возвращает весь список. Если отправить запрос с методом, для которого нет маршрута (например, DELETE /tasks), ServeMux сам ответит 405 Method Not Allowed.
$ curl -X POST localhost:8080/tasks -d '{"title":"Learn Go"}'
{"id":1,"title":"Learn Go"}
$ curl -X POST localhost:8080/tasks -d '{"title":" "}'
empty title
$ curl localhost:8080/tasks
[{"id":1,"title":"Learn Go"}]Главное
go mod initсоздаёт модуль; каждая папка — пакет, который импортируют какпуть модуля/папка.- Стандартная библиотека (
fmt,strings,strconv,os,time,encoding/json,net/http) закрывает большинство задач без сторонних зависимостей. - Тесты — это функции
TestXxx(t *testing.T)в файлах_test.go; табличные тесты сt.Run— стиль Go. - Начиная с Go 1.22
ServeMuxподдерживает маршруты с методами ("GET /tasks") и параметры пути ({id}). - Обработчики работают одновременно, поэтому защищай общее состояние мьютексом.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
example.com/tasks, а пакет находится в папке store. Как его импортировать?