GO
Язык Go может использоваться для создания любых продуктов, будь то консольные программы или сложные многопоточный приложения.
Go (или Golang)
представляет собой компилируемый многопоточный язык программирования, который имеет открытый исходный код. Его часто используют в веб-сервисах и клиент-серверных приложениях. К концу 2021 года Go вошёл в список самых востребованных языков.
1 - Полный гайд по установке и настройке Go для работы с базами данных (OpenSearch, PostgreSQL, SQLite)
Полный гайд по установке и настройке Go для работы с базами данных (OpenSearch, PostgreSQL, SQLite) на локальном компьютере
1. Установка Go на локальный компьютер
Linux (Ubuntu/Debian)
# Удаление старых версий (если есть)
sudo rm -rf /usr/local/go
# Скачивание и установка
wget https://go.dev/dl/go1.21.4.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.4.linux-amd64.tar.gz
# Добавление в PATH
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
# Проверка
go version
2. Настройка рабочего окружения
-
Создайте рабочую директорию:
mkdir -p ~/go/src/myproject
cd ~/go/src/myproject
-
Инициализация модуля:
-
Установка зависимостей (драйверы для баз данных):
go get github.com/opensearch-project/opensearch-go
go get github.com/lib/pq
go get github.com/mattn/go-sqlite3
3. Подключение к базам данных
OpenSearch
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"github.com/opensearch-project/opensearch-go"
)
func main() {
client, err := opensearch.NewClient(opensearch.Config{
Addresses: []string{"https://localhost:9200"},
Username: "admin",
Password: "admin",
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
})
if err != nil {
log.Fatal(err)
}
// Проверка подключения
res, err := client.Ping()
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected to OpenSearch:", res.Status())
}
PostgreSQL
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
func main() {
connStr := "user=postgres password=1234 dbname=test sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверка подключения
err = db.Ping()
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected to PostgreSQL")
}
SQLite
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверка подключения
err = db.Ping()
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected to SQLite")
}
4. Пример CRUD-операций
PostgreSQL (создание таблицы и вставка данных)
func main() {
db, err := sql.Open("postgres", "user=postgres password=1234 dbname=test sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Создание таблицы
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT,
email TEXT UNIQUE
)
`)
if err != nil {
log.Fatal(err)
}
// Вставка данных
_, err = db.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", "Alice", "alice@example.com")
if err != nil {
log.Fatal(err)
}
fmt.Println("Data inserted")
}
SQLite (выборка данных)
func main() {
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
err = rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s\n", id, name)
}
}
5. Управление зависимостями
-
Инициализация модуля (если еще не сделано):
-
Добавление новых зависимостей:
go get github.com/new/package
-
Обновление зависимостей:
6. Сборка и запуск приложения
-
Запуск в development-режиме:
-
Сборка бинарного файла:
-
Запуск собранного приложения:
7. Дополнительные инструменты
Установка утилит для разработки
go install golang.org/x/tools/cmd/godoc@latest # Документация
go install github.com/go-delve/delve/cmd/dlv@latest # Отладчик
Настройка IDE (VS Code)
- Установите расширение Go от Microsoft
- Нажмите
Ctrl+Shift+P → Go: Install/Update Tools
- Выберите все инструменты и нажмите OK
8. Решение частых проблем
Ошибка: “sql: unknown driver”
Убедитесь, что импортирован драйвер с _ перед пакетом:
import _ "github.com/lib/pq"
Ошибка подключения к OpenSearch
Проверьте:
- Работает ли сервер OpenSearch (
curl -X GET https://localhost:9200 -u 'admin:admin' --insecure)
- Правильно ли указаны логин/пароль
Ошибка cgo при работе с SQLite
На Linux установите:
Теперь можем работать:
- Работать с OpenSearch, PostgreSQL и SQLite из Go-приложений
- Выполнять CRUD-операции
- Собирать и запускать приложения
Официальные справочники:
2 - Работа с JSON в Go
Полное руководство по работе с JSON в Go: файлы, API и базы данных
1. Основы работы с JSON в Go
Структуры для маршалинга/анмаршалинга
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Roles []string `json:"roles"`
IsActive bool `json:"is_active"`
}
Преобразование структур в JSON
user := User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
Roles: []string{"admin", "user"},
IsActive: true,
}
jsonData, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonData))
// Output: {"id":1,"name":"Alice","email":"alice@example.com","roles":["admin","user"],"is_active":true}
Чтение JSON в структуру
var decodedUser User
err = json.Unmarshal(jsonData, &decodedUser)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", decodedUser)
2. Работа с JSON-файлами
Запись JSON в файл
func writeJSON(filename string, data interface{}) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ") // Форматирование с отступами
return encoder.Encode(data)
}
// Использование
err := writeJSON("user.json", user)
Чтение JSON из файла
func readJSON(filename string, target interface{}) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
return json.NewDecoder(file).Decode(target)
}
// Использование
var userFromFile User
err := readJSON("user.json", &userFromFile)
3. Обработка директорий с JSON-файлами
Чтение всех JSON-файлов в директории
func readAllJSONFiles(dirPath string) ([]User, error) {
var users []User
files, err := os.ReadDir(dirPath)
if err != nil {
return nil, err
}
for _, file := range files {
if !strings.HasSuffix(file.Name(), ".json") {
continue
}
var user User
err := readJSON(filepath.Join(dirPath, file.Name()), &user)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
Пример: Сохранение множества файлов
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
for _, user := range users {
filename := fmt.Sprintf("user_%d.json", user.ID)
err := writeJSON(filename, user)
if err != nil {
log.Printf("Error saving %s: %v", filename, err)
}
}
4. Взаимодействие с API
Отправка JSON через HTTP POST
func sendUserToAPI(url string, user User) (*http.Response, error) {
jsonData, err := json.Marshal(user)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token123")
client := &http.Client{}
return client.Do(req)
}
// Использование
resp, err := sendUserToAPI("https://api.example.com/users", user)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
Чтение JSON-ответа от API
func getUsersFromAPI(url string) ([]User, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status: %s", resp.Status)
}
var users []User
err = json.NewDecoder(resp.Body).Decode(&users)
return users, err
}
5. Продвинутые техники
Кастомный парсинг дат в JSON
type CustomTime time.Time
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), `"`)
t, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
*ct = CustomTime(t)
return nil
}
type Event struct {
Name string `json:"name"`
Date CustomTime `json:"date"`
}
Обработка динамических JSON-структур
var data map[string]interface{}
err := json.Unmarshal(jsonData, &data)
if err != nil {
log.Fatal(err)
}
if name, ok := data["name"].(string); ok {
fmt.Println("Name:", name)
}
6. Полный пример: CRUD с JSON-файлами
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
const storageDir = "data/users"
func init() {
os.MkdirAll(storageDir, 0755)
}
func saveUser(user User) error {
filename := filepath.Join(storageDir, fmt.Sprintf("%d.json", user.ID))
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(user)
}
func getUser(id int) (User, error) {
var user User
filename := filepath.Join(storageDir, fmt.Sprintf("%d.json", id))
file, err := os.Open(filename)
if err != nil {
return user, err
}
defer file.Close()
err = json.NewDecoder(file).Decode(&user)
return user, err
}
func main() {
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
// Create
if err := saveUser(user); err != nil {
log.Fatal(err)
}
// Read
savedUser, err := getUser(1)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", savedUser)
}
7. Оптимизация работы с большими JSON
Стриминг при записи
func writeLargeJSON(filename string, data []User) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
for _, item := range data {
if err := encoder.Encode(item); err != nil {
return err
}
}
return nil
}
Построчное чтение больших файлов
func readLargeJSON(filename string) ([]User, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var users []User
decoder := json.NewDecoder(file)
for decoder.More() {
var user User
if err := decoder.Decode(&user); err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
8. Безопасность и валидация
Валидация JSON-схемы
import "github.com/xeipuuv/gojsonschema"
func validateJSON(schemaPath string, jsonData []byte) error {
schemaLoader := gojsonschema.NewReferenceLoader("file://" + schemaPath)
documentLoader := gojsonschema.NewBytesLoader(jsonData)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return err
}
if !result.Valid() {
return fmt.Errorf("validation errors: %v", result.Errors())
}
return nil
}
Защита от переполнения при парсинге
decoder := json.NewDecoder(file)
decoder.DisallowUnknownFields() // Запрет неизвестных полей
Полезные материалы:
- JSON Schema для сложной валидации
- Альтернативные JSON-парсеры: json-iterator
- Бинарные JSON-альтернативы: Protocol Buffers, MessagePack
3 - Работа с OpenSearch в Go для системы управления знаниями
В этом руководстве я покажу, как реализовать систему знаний с таксономиями в OpenSearch с использованием Go, включая сложные запросы и построение дерева знаний.
Подготовка структуры данных
Сначала определим структуру документа для нашей базы знаний:
type KnowledgeDocument struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Category string `json:"category"`
Tags []string `json:"tags"`
Benefits []string `json:"benefits"`
Locations []string `json:"locations"`
LevelUser string `json:"level_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
RelatedDocs []string `json:"related_docs"`
Solutions []struct {
Description string `json:"description"`
Steps []string `json:"steps"`
} `json:"solutions"`
}
Настройка клиента OpenSearch в Go
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/opensearch-project/opensearch-go"
"github.com/opensearch-project/opensearch-go/opensearchapi"
)
func getOpenSearchClient() *opensearch.Client {
cfg := opensearch.Config{
Addresses: []string{"https://localhost:9200"},
Username: "admin",
Password: "admin",
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
client, err := opensearch.NewClient(cfg)
if err != nil {
log.Fatalf("Error creating the client: %s", err)
}
return client
}
Создание индекса с маппингом
func createKnowledgeIndex(client *opensearch.Client) error {
mapping := `
{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"analysis": {
"analyzer": {
"custom_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "stemmer"]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "custom_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"content": {
"type": "text",
"analyzer": "custom_analyzer"
},
"category": {
"type": "keyword"
},
"tags": {
"type": "keyword"
},
"benefits": {
"type": "keyword"
},
"locations": {
"type": "keyword"
},
"level_user": {
"type": "keyword"
},
"created_at": {
"type": "date"
},
"updated_at": {
"type": "date"
},
"related_docs": {
"type": "keyword"
},
"solutions": {
"type": "nested",
"properties": {
"description": {
"type": "text",
"analyzer": "custom_analyzer"
},
"steps": {
"type": "text",
"analyzer": "custom_analyzer"
}
}
}
}
}
}`
req := opensearchapi.IndicesCreateRequest{
Index: "knowledge_base",
Body: strings.NewReader(mapping),
}
res, err := req.Do(context.Background(), client)
if err != nil {
return fmt.Errorf("error creating index: %w", err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("error response: %s", res.String())
}
return nil
}
Добавление документа в базу знаний
func addDocument(client *opensearch.Client, doc KnowledgeDocument) error {
docBytes, err := json.Marshal(doc)
if err != nil {
return fmt.Errorf("error marshaling document: %w", err)
}
req := opensearchapi.IndexRequest{
Index: "knowledge_base",
Body: strings.NewReader(string(docBytes)),
DocumentID: doc.ID,
Refresh: "true",
}
res, err := req.Do(context.Background(), client)
if err != nil {
return fmt.Errorf("error indexing document: %w", err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("error response: %s", res.String())
}
return nil
}
Пример сложного поиска по таксономиям
func searchDocuments(client *opensearch.Client, query string, filters map[string]interface{}) ([]KnowledgeDocument, error) {
var buf bytes.Buffer
searchQuery := map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"must": []map[string]interface{}{
{
"multi_match": map[string]interface{}{
"query": query,
"fields": []string{"title^3", "content", "solutions.description", "solutions.steps"},
},
},
},
"filter": []map[string]interface{}{},
},
},
"size": 10,
}
// Добавляем фильтры по таксономиям
for field, value := range filters {
filter := map[string]interface{}{
"term": map[string]interface{}{
field: value,
},
}
searchQuery["query"].(map[string]interface{})["bool"].(map[string]interface{})["filter"] = append(
searchQuery["query"].(map[string]interface{})["bool"].(map[string]interface{})["filter"].([]map[string]interface{}),
filter,
)
}
if err := json.NewEncoder(&buf).Encode(searchQuery); err != nil {
return nil, fmt.Errorf("error encoding query: %w", err)
}
res, err := client.Search(
client.Search.WithContext(context.Background()),
client.Search.WithIndex("knowledge_base"),
client.Search.WithBody(&buf),
client.Search.WithTrackTotalHits(true),
)
if err != nil {
return nil, fmt.Errorf("error executing search: %w", err)
}
defer res.Body.Close()
if res.IsError() {
return nil, fmt.Errorf("error response: %s", res.String())
}
var result struct {
Hits struct {
Hits []struct {
Source KnowledgeDocument `json:"_source"`
} `json:"hits"`
} `json:"hits"`
}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("error parsing the response body: %w", err)
}
var docs []KnowledgeDocument
for _, hit := range result.Hits.Hits {
docs = append(docs, hit.Source)
}
return docs, nil
}
Решение сложных запросов (например, проблема с принтером)
func troubleshootPrinterIssue(client *opensearch.Client, problemDescription string) ([]KnowledgeDocument, error) {
var buf bytes.Buffer
query := map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"must": []map[string]interface{}{
{
"match": map[string]interface{}{
"category": "printer",
},
},
{
"multi_match": map[string]interface{}{
"query": problemDescription,
"fields": []string{"title^3", "content", "solutions.description", "solutions.steps"},
},
},
},
"should": []map[string]interface{}{
{
"match": map[string]interface{}{
"tags": "streaks",
},
},
{
"match": map[string]interface{}{
"tags": "cartridge",
},
},
{
"match": map[string]interface{}{
"tags": "printing_quality",
},
},
},
"minimum_should_match": 1,
},
},
"size": 5,
}
if err := json.NewEncoder(&buf).Encode(query); err != nil {
return nil, fmt.Errorf("error encoding query: %w", err)
}
res, err := client.Search(
client.Search.WithContext(context.Background()),
client.Search.WithIndex("knowledge_base"),
client.Search.WithBody(&buf),
client.Search.WithTrackTotalHits(true),
)
if err != nil {
return nil, fmt.Errorf("error executing search: %w", err)
}
defer res.Body.Close()
if res.IsError() {
return nil, fmt.Errorf("error response: %s", res.String())
}
var result struct {
Hits struct {
Hits []struct {
Source KnowledgeDocument `json:"_source"`
} `json:"hits"`
} `json:"hits"`
}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("error parsing the response body: %w", err)
}
var docs []KnowledgeDocument
for _, hit := range result.Hits.Hits {
docs = append(docs, hit.Source)
}
return docs, nil
}
Построение дерева знаний
func buildKnowledgeTree(client *opensearch.Client, rootCategory string, depth int) (map[string]interface{}, error) {
var buf bytes.Buffer
query := map[string]interface{}{
"size": 0,
"query": map[string]interface{}{
"term": map[string]interface{}{
"category.keyword": rootCategory,
},
},
"aggs": map[string]interface{}{
"subcategories": map[string]interface{}{
"terms": map[string]interface{}{
"field": "category.keyword",
"size": 10,
},
"aggs": map[string]interface{}{
"related_tags": map[string]interface{}{
"terms": map[string]interface{}{
"field": "tags.keyword",
"size": 5,
},
},
"top_documents": map[string]interface{}{
"top_hits": map[string]interface{}{
"size": 3,
"_source": map[string]interface{}{
"includes": []string{"title", "id"},
},
},
},
},
},
},
}
if err := json.NewEncoder(&buf).Encode(query); err != nil {
return nil, fmt.Errorf("error encoding query: %w", err)
}
res, err := client.Search(
client.Search.WithContext(context.Background()),
client.Search.WithIndex("knowledge_base"),
client.Search.WithBody(&buf),
)
if err != nil {
return nil, fmt.Errorf("error executing search: %w", err)
}
defer res.Body.Close()
if res.IsError() {
return nil, fmt.Errorf("error response: %s", res.String())
}
var result struct {
Aggregations struct {
Subcategories struct {
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
RelatedTags struct {
Buckets []struct {
Key string `json:"key"`
} `json:"buckets"`
} `json:"related_tags"`
TopDocuments struct {
Hits struct {
Hits []struct {
Source struct {
Title string `json:"title"`
ID string `json:"id"`
} `json:"_source"`
} `json:"hits"`
} `json:"hits"`
} `json:"top_documents"`
} `json:"buckets"`
} `json:"subcategories"`
} `json:"aggregations"`
}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("error parsing the response body: %w", err)
}
tree := make(map[string]interface{})
tree["category"] = rootCategory
var subcategories []map[string]interface{}
for _, bucket := range result.Aggregations.Subcategories.Buckets {
subcategory := map[string]interface{}{
"name": bucket.Key,
"count": bucket.DocCount,
"popularTags": []string{},
"documents": []map[string]string{},
}
for _, tag := range bucket.RelatedTags.Buckets {
subcategory["popularTags"] = append(subcategory["popularTags"].([]string), tag.Key)
}
for _, doc := range bucket.TopDocuments.Hits.Hits {
subcategory["documents"] = append(subcategory["documents"].([]map[string]string), map[string]string{
"title": doc.Source.Title,
"id": doc.Source.ID,
})
}
subcategories = append(subcategories, subcategory)
}
tree["subcategories"] = subcategories
return tree, nil
}
Пример использования
func main() {
client := getOpenSearchClient()
// Создаем индекс (один раз)
if err := createKnowledgeIndex(client); err != nil {
log.Fatalf("Error creating index: %v", err)
}
// Добавляем документ о проблемах с принтером
doc := KnowledgeDocument{
ID: "printer_streaks_1",
Title: "Принтер печатает с полосами",
Content: "Если принтер печатает с полосами, даже с новым картриджем, возможные причины...",
Category: "printer",
Tags: []string{"streaks", "printing_quality", "cartridge"},
Benefits: []string{"troubleshooting", "repair"},
Locations: []string{"office", "home"},
LevelUser: "beginner",
Solutions: []struct {
Description string `json:"description"`
Steps []string `json:"steps"`
}{
{
Description: "Очистка печатающей головки",
Steps: []string{
"Зайти в настройки принтера",
"Найти раздел обслуживания",
"Выбрать опцию очистки печатающей головки",
},
},
{
Description: "Проверка выравнивания картриджей",
Steps: []string{
"Заменить картридж",
"Запустить процедуру выравнивания",
},
},
},
}
if err := addDocument(client, doc); err != nil {
log.Fatalf("Error adding document: %v", err)
}
// Поиск решения проблемы
solutions, err := troubleshootPrinterIssue(client, "сломался принтер, печатает с полосой, картридж новый")
if err != nil {
log.Fatalf("Error searching: %v", err)
}
fmt.Println("Найдены решения:")
for _, sol := range solutions {
fmt.Printf("- %s\n", sol.Title)
for _, solution := range sol.Solutions {
fmt.Printf(" %s\n", solution.Description)
for i, step := range solution.Steps {
fmt.Printf(" %d. %s\n", i+1, step)
}
}
}
// Построение дерева знаний
tree, err := buildKnowledgeTree(client, "printer", 2)
if err != nil {
log.Fatalf("Error building knowledge tree: %v", err)
}
fmt.Println("\nДерево знаний:")
fmt.Printf("Категория: %s\n", tree["category"])
for _, subcat := range tree["subcategories"].([]map[string]interface{}) {
fmt.Printf(" Подкатегория: %s (%d документов)\n", subcat["name"], subcat["count"])
fmt.Printf(" Популярные теги: %v\n", subcat["popularTags"])
}
}
4 - Пакеты языка Go
Пакеты и библиотеки языка программирования Go на русском языке
4.1 - Описание пакета html для GO
Пакеты и функции для работы с html в GO
Пакет html предоставляет функции для экранирования и отмены экранирования HTML-текста.
func EscapeString
func EscapeString(s string) string
EscapeString экранирует специальные символы, такие как “<”, превращаясь в “<”. Он экранирует только пять таких символов: <, >, &, ’ и “. UnescapeString(EscapeString(s)) == s всегда выполняется, но обратное не всегда верно.
Пример
package main
import (
"fmt"
"html"
)
func main() {
const s = `"Fran & Freddie's Diner" <tasty@example.com>`
fmt.Println(html.EscapeString(s))
}
Output:
`"Fran & Freddie's Diner" <tasty@example.com>`
func UnescapeString
func UnescapeString(s string) string
UnescapeString отменяет экранирование таких сущностей, как “<”, чтобы они стали “<”. Он отменяет экранирование большего диапазона сущностей, чем EscapeString. Например, “á” отменяет экранирование на “á”, как и “á” и “á”. UnescapeString(EscapeString(s)) == s всегда выполняется, но обратное не всегда верно.
Пример
package main
import (
"fmt"
"html"
)
func main() {
const s = `"Fran & Freddie's Diner" <tasty@example.com>`
fmt.Println(html.UnescapeString(s))
}
Output:
"Fran & Freddie's Diner" <tasty@example.com>
4.1.1 - Описание пакета html/template в Go
Пакет html/template - шаблоны HTML с защитой от инъекций
Пакет html/template реализует шаблоны, управляемые данными, для генерации HTML-вывода, защищённого от внедрения кода. Он предоставляет тот же интерфейс, что и text/template, и должен использоваться вместо него, когда выводом является HTML.
Данная документация фокусируется на функциях безопасности пакета. Информацию о программировании самих шаблонов см. в документации text/template.
Введение
Этот пакет оборачивает text/template, позволяя использовать его API шаблонов для безопасного разбора и выполнения HTML-шаблонов.
Пример:
tmpl, err := template.New("name").Parse(...)
// Проверка ошибок опущена
err = tmpl.Execute(out, data)
Если операция успешна, tmpl будет защищён от инъекций. В противном случае err будет содержать ошибку, как описано в ErrorCode.
HTML-шаблоны обрабатывают значения данных как обычный текст, который должен быть закодирован для безопасного встраивания в HTML-документ. Экранирование является контекстно-зависимым, поэтому действия могут появляться в JavaScript, CSS и URI-контекстах.
Модель безопасности, используемая этим пакетом, предполагает, что авторы шаблонов являются доверенными, а параметр данных Execute - нет. Более подробная информация приведена ниже.
import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
Вывод
Hello, <script>alert('you have been pwned')</script>!
но контекстное автоэкранирование в html/шаблоне
import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
создает безопасный, экранированный HTML-вывод
Hello, <script>alert('you have been pwned')</script>!
Контексты
Этот пакет понимает HTML, CSS, JavaScript и URI. Он добавляет функции очистки к каждому простому конвейеру действий, поэтому, рассмотрим пример
<a href="/search?q={{.}}">{{.}}</a>
Во время разбора каждый {{.}} перезаписывается для добавления экранирующих функций по мере необходимости. В этом случае это становится
<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
где urlescaper, attrescaper и htmlescaper — псевдонимы для внутренних функций экранирования.
Для этих внутренних функций экранирования, если конвейер действий оценивает значение интерфейса nil, он обрабатывается так, как будто это пустая строка.
Namespaced и data-атрибуты
Атрибуты с пространством имен обрабатываются так, как будто у них нет пространства имен. Рассмотрим пример:
Во время анализа атрибут будет обработан так, как если бы это был просто “href”. Таким образом, во время анализа шаблон становится:
<a my:href="{{. | urlescaper | attrescaper}}"></a>
Аналогично атрибутам с пространствами имен, атрибуты с префиксом “data-” обрабатываются так, как если бы у них не было префикса “data-”. Рассмотрим пример:
<a data-href="{{.}}"></a>
Во время анализа это становится
<a data-href="{{. | urlescaper | attrescaper}}"></a>
Если атрибут имеет как пространство имен, так и префикс “data-”, то при определении контекста будет удалено только пространство имен. Например:
<a my:data-href="{{.}}"></a>
Это обрабатывается так, как если бы “my:data-href” было просто “data-href”, а не “href”, как было бы, если бы префикс “data-” тоже игнорировался. Таким образом, во время анализа это становится просто
<a my:data-href="{{. | attrescaper}}"></a>
В качестве особого случая атрибуты с пространством имен “xmlns” всегда рассматриваются как содержащие URL-адреса. Например:
<a xmlns:title="{{.}}"></a>
<a xmlns:href="{{.}}"></a>
<a xmlns:onclick="{{.}}"></a>
Во время анализа они становятся:
<a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
Ошибки
Подробности см. в документации ErrorCode.
Более полная картина
Остальную часть этого комментария к пакету можно пропустить при первом чтении; он включает детали, необходимые для понимания экранирования контекстов и сообщений об ошибках. Большинству пользователей не нужно понимать эти детали.
Контексты
Предположив, что {{.}} — это O'Reilly: How are <i>you</i>?, в таблице ниже показано, как выглядит {{.}} при использовании в контексте слева.
Context {{.}} After
{{.}} O'Reilly: How are <i>you</i>?
<a title='{{.}}'> O'Reilly: How are you?
<a href="/{{.}}"> O'Reilly: How are %3ci%3eyou%3c/i%3e?
<a href="?q={{.}}"> O'Reilly%3a%20How%20are%3ci%3e...%3f
<a onx='f("{{.}}")'> O\x27Reilly: How are \x3ci\x3eyou...?
<a onx='f({{.}})'> "O\x27Reilly: How are \x3ci\x3eyou...?"
<a onx='pattern = /{{.}}/;'> O\x27Reilly: How are \x3ci\x3eyou...\x3f
При использовании в небезопасном контексте значение может быть отфильтровано:
Context {{.}} After
<a href="{{.}}"> #ZgotmplZ
поскольку “O’Reilly:” не является разрешенным протоколом, как “http:”.
Если {{.}} — это безобидное слово left, то оно может появляться более широко,
Context {{.}} After
{{.}} left
<a title='{{.}}'> left
<a href='{{.}}'> left
<a href='/{{.}}'> left
<a href='?dir={{.}}'> left
<a style="border-{{.}}: 4px"> left
<a style="align: {{.}}"> left
<a style="background: '{{.}}'> left
<a style="background: url('{{.}}')> left
<style>p.{{.}} {color:red}</style> left
Нестроковые значения могут использоваться в контекстах JavaScript. Если {{.}} — это
struct{A,B string}{ "foo", "bar" }
в экранированном шаблоне
<script>var pair = {{.}};</script>
то вывод шаблона будет
<script>var pair = {"A": "foo", "B": "bar"};</script>
Ознакомьтесь с пакетом json, чтобы понять, как нестроковый контент маршалируется для встраивания в контексты JavaScript.
Типизированные строки
По умолчанию этот пакет предполагает, что все конвейеры производят простую текстовую строку. Он добавляет этапы экранирования конвейера, необходимые для корректного и безопасного встраивания этой простой текстовой строки в соответствующий контекст.
Если значение данных не является обычным текстом, вы можете убедиться, что оно не экранировано слишком сильно, пометив его разными типами.
Типы HTML, JS, URL и другие из content.go могут содержать безопасный контент, который освобожден от экранирования.
Например шаблон:
может быть вызван с помощью
tmpl.Execute(out, template.HTML(`<b>World</b>`))
вывод
вместо
Hello, <b>World<b>!
который был бы получен, если бы {{.}} был обычной строкой.
Модель безопасности
Определение “безопасности”, используемое в этом пакете, основано на спецификации.
Данный пакет исходит из следующих допущений:
- Авторы шаблонов считаются доверенными
- Данные, передаваемые в
Execute, — ненадёжные
При этом обеспечиваются следующие свойства безопасности при работе с ненадёжными данными:
1. Сохранение структуры (Structure Preservation Property)
Если автор шаблона пишет HTML-тег на безопасном языке шаблонов, браузер всегда будет интерпретировать соответствующую часть вывода именно как тег — независимо от значений ненадёжных данных. То же самое относится к другим структурам, таким как границы атрибутов, строки JavaScript и CSS.
2. Контроль исполнения кода (Code Effect Property)
В результате вставки вывода шаблона на страницу должен выполняться только код, явно указанный автором шаблона. При этом весь код, указанный автором, должен выполняться корректно.
3. Принцип наименьшего удивления (Least Surprise Property)
Разработчик (или ревьюер кода), знакомый с HTML, CSS и JavaScript и знающий о контекстном автоэкранировании, должен быть способен, увидев {{.}}, однозначно определить, какое экранирование будет применено.
Поддержка шаблонных литералов ES6
Ранее шаблонные литералы ECMAScript 6 (${...}) были отключены по умолчанию, и их можно было включить через переменную окружения GODEBUG=jstmpllitinterp=1.
Сейчас шаблонные литералы поддерживаются по умолчанию, а настройка jstmpllitinterp больше не имеет эффекта.
Пример
package main
import (
"html/template"
"log"
"os"
)
func main() {
const tpl = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
</head>
<body>
{{range .Items}}<div>{{ . }}</div>{{else}}<div><strong>no rows</strong></div>{{end}}
</body>
</html>`
check := func(err error) {
if err != nil {
log.Fatal(err)
}
}
t, err := template.New("webpage").Parse(tpl)
check(err)
data := struct {
Title string
Items []string
}{
Title: "My page",
Items: []string{
"My photos",
"My blog",
},
}
err = t.Execute(os.Stdout, data)
check(err)
noItems := struct {
Title string
Items []string
}{
Title: "My another page",
Items: []string{},
}
err = t.Execute(os.Stdout, noItems)
check(err)
}
Output:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My page</title>
</head>
<body>
<div>My photos</div><div>My blog</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My another page</title>
</head>
<body>
<div><strong>no rows</strong></div>
</body>
</html>
Пример Autoescaping
package main
import (
"html/template"
"log"
"os"
)
func main() {
check := func(err error) {
if err != nil {
log.Fatal(err)
}
}
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
check(err)
err = t.ExecuteTemplate(os.Stdout, "T", "<script>alert('you have been pwned')</script>")
check(err)
}
Output:
Hello, <script>alert('you have been pwned')</script>!
Пример Escape
package main
import (
"fmt"
"html/template"
"os"
)
func main() {
const s = `"Fran & Freddie's Diner" <tasty@example.com>`
v := []any{`"Fran & Freddie's Diner"`, ' ', `<tasty@example.com>`}
fmt.Println(template.HTMLEscapeString(s))
template.HTMLEscape(os.Stdout, []byte(s))
fmt.Fprintln(os.Stdout, "")
fmt.Println(template.HTMLEscaper(v...))
fmt.Println(template.JSEscapeString(s))
template.JSEscape(os.Stdout, []byte(s))
fmt.Fprintln(os.Stdout, "")
fmt.Println(template.JSEscaper(v...))
fmt.Println(template.URLQueryEscaper(v...))
}
Output:
"Fran & Freddie's Diner" <tasty@example.com>
"Fran & Freddie's Diner" <tasty@example.com>
"Fran & Freddie's Diner"32<tasty@example.com>
\"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
\"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
\"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
%22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
4.1.1.1 - Шпаргалка по html/template в Go
html/template - это пакет Go для безопасного создания HTML-выхода с автоматическим экранированием.
Инициализация шаблона
import "html/template"
// Создание нового шаблона
tmpl := template.New("templateName")
// Парсинг шаблона из строки
tmpl, err := template.New("test").Parse(`<h1>{{.Title}}</h1>`)
// Парсинг шаблона из файла
tmpl, err := template.ParseFiles("template.html")
// Парсинг нескольких файлов
tmpl, err := template.ParseGlob("templates/*.html")
Основные конструкции в шаблоне
1. Вывод значения
<!-- Простой вывод -->
<p>{{.UserName}}</p>
<!-- Вывод с экранированием HTML -->
<p>{{.UserComment}}</p>
<!-- Вывод без экранирования (осторожно!) -->
<p>{{.SafeHTML | html}}</p>
2. Условия
{{if .User}}
<p>Welcome, {{.User}}!</p>
{{else}}
<p>Please log in.</p>
{{end}}
{{if eq .Role "admin"}}
<p>Admin dashboard</p>
{{end}}
3. Циклы
<ul>
{{range .Items}}
<li>{{.}}</li>
{{end}}
</ul>
<!-- С индексом -->
<ul>
{{range $index, $item := .Items}}
<li>Item #{{$index}}: {{$item}}</li>
{{end}}
</ul>
<!-- Если слайс пуст -->
{{range .Items}}
{{.}}
{{else}}
<p>No items found</p>
{{end}}
4. Определение переменных
{{$name := .UserName}}
{{$count := len .Items}}
<p>Hello, {{$name}}. You have {{$count}} items.</p>
5. Функции и конвейеры
<!-- Вызов встроенных функций -->
<p>{{printf "Hello, %s" .Name}}</p>
<p>{{len .Items}} items</p>
<p>{{index .Items 0}}</p>
<!-- Конвейер -->
<p>{{.Title | upper | truncate 50}}</p>
6. Вложенные шаблоны
// Определение в коде
tmpl := template.Must(template.New("base").Parse(`
{{define "content"}}Default content{{end}}
<html>
<body>
{{template "content" .}}
</body>
</html>
`))
// Переопределение в другом шаблоне
tmpl2 := template.Must(tmpl.Parse(`{{define "content"}}New content{{end}}`))
<!-- В шаблоне -->
{{template "header" .}}
<!-- С передачей других данных -->
{{template "footer" .FooterData}}
7. Пользовательские функции
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"add": func(a, b int) int { return a + b },
}
tmpl := template.New("test").Funcs(funcMap)
tmpl, err := tmpl.Parse(`{{upper .Name}} {{add 1 2}}`)
Пример полного шаблона
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}} - My Site</title>
</head>
<body>
<header>
{{if .User}}
<p>Welcome, {{.User.Name}} ({{.User.Email}})</p>
{{else}}
<a href="/login">Login</a>
{{end}}
</header>
<nav>
<ul>
{{range .Menu}}
<li><a href="{{.URL}}">{{.Title}}</a></li>
{{end}}
</ul>
</nav>
<main>
{{template "content" .}}
</main>
<footer>
<p>© {{.CurrentYear}} My Company</p>
</footer>
</body>
</html>
Выполнение шаблона
// В память
err := tmpl.Execute(os.Stdout, data)
// В строку
var buf bytes.Buffer
err := tmpl.Execute(&buf, data)
result := buf.String()
// Конкретный шаблон из набора
err := tmpl.ExecuteTemplate(os.Stdout, "templateName", data)
Обработка ошибок
tmpl := template.Must(template.New("").Parse("...")) // паника при ошибке
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
Безопасность
- Автоматическое экранирование HTML/JS/CSS
- Для отключения экранирования используйте типы
template.HTML, template.JS, template.CSS:
type Data struct {
SafeContent template.HTML
}
data := Data{SafeContent: template.HTML("<b>Safe HTML</b>")}
4.1.1.2 - Подробное руководство по define, template и block в html/template
В пакете html/template есть три основные директивы для работы с повторно используемыми частями шаблонов: define, template, block
Основные концепции
В пакете html/template есть три основные директивы для работы с повторно используемыми частями шаблонов:
{{define "name"}}...{{end}} - определяет именованный шаблон
{{template "name"}} - вставляет именованный шаблон
{{block "name"}}...{{end}} - комбинация define+template с возможностью переопределения
1. Директива define
Где описывать:
- В любом месте шаблона (но обычно в начале или конце файла)
- В отдельных файлах, которые потом объединяются
Пример определения:
{{define "header"}}
<header>
<h1>{{.Title}}</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
{{end}}
{{define "footer"}}
<footer>
<p>Copyright © {{.Year}} My Company</p>
</footer>
{{end}}
2. Директива template
Где применять:
- В любом месте основного шаблона
- Можно передавать данные (по умолчанию - текущий контекст)
Пример использования:
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
{{template "header" .}} <!-- Передаем текущий контекст -->
<main>
{{.Content}}
</main>
{{template "footer" .}} <!-- Передаем текущий контекст -->
</body>
</html>
Передача другого контекста:
{{template "user_profile" .UserData}}
3. Директива block
Особенности:
- Комбинация
define + template
- Можно переопределять в дочерних шаблонах
- Если не переопределен - используется содержимое по умолчанию
Пример:
<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
{{block "head" .}}
<title>{{.Title}} - Default</title>
{{end}}
</head>
<body>
{{block "content" .}}
<p>Default content</p>
{{end}}
</body>
</html>
Переопределение в дочернем шаблоне:
<!-- home.html -->
{{define "content"}}
<h1>Welcome!</h1>
<p>This is the home page.</p>
{{end}}
{{template "base.html" .}}
Практические схемы организации
1. Многофайловая структура
templates/
├── base.html # Основной каркас
├── header.html # Шапка сайта
├── footer.html # Подвал
├── home.html # Главная страница
└── about.html # Страница "О нас"
base.html:
<!DOCTYPE html>
<html>
<head>
{{template "head" .}}
</head>
<body>
{{template "header" .}}
{{block "content" .}}{{end}}
{{template "footer" .}}
</body>
</html>
home.html:
{{define "content"}}
<h1>Welcome</h1>
<p>Home page content</p>
{{end}}
{{template "base.html" .}}
2. Наследование шаблонов
// Загрузка шаблонов
templates := template.Must(template.ParseFiles(
"base.html",
"header.html",
"footer.html",
"home.html",
))
// Исполнение конкретного шаблона
err := templates.ExecuteTemplate(w, "home.html", data)
3. Вложенные блоки
<!-- base.html -->
{{define "layout"}}
<div class="container">
{{block "sidebar" .}}
<!-- Default sidebar -->
<div class="sidebar">Default sidebar</div>
{{end}}
<div class="content">
{{template "content" .}}
</div>
</div>
{{end}}
<!-- profile.html -->
{{define "sidebar"}}
<div class="profile-sidebar">
<img src="{{.User.Avatar}}">
<h3>{{.User.Name}}</h3>
</div>
{{end}}
{{define "content"}}
<div class="profile-content">
<!-- Контент профиля -->
</div>
{{end}}
{{template "layout" .}}
Особенности работы
- Порядок загрузки: шаблоны, на которые есть ссылки, должны быть загружены до их использования
- Контекст данных: по умолчанию передается текущий контекст, но можно передать другой
- Рекурсия: шаблоны могут вызывать сами себя (но осторожно с бесконечной рекурсией)
- Переопределение: последний загруженный шаблон с тем же именем переопределяет предыдущий
Пример с пользовательскими функциями
func main() {
funcMap := template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02")
},
}
tmpl := template.Must(
template.New("base").Funcs(funcMap).ParseFiles("base.html", "home.html"),
)
err := tmpl.ExecuteTemplate(os.Stdout, "home.html", data)
}
<!-- В шаблоне: -->
<p>Created: {{.CreatedAt | formatDate}}</p>
Лучшие практики
- Разделяйте шаблоны на логические компоненты
- Используйте
block для переопределяемых секций
- Основной каркас выносите в
base.html
- Называйте шаблоны осмысленно (не “template1”, а “user_profile”)
- Документируйте назначение шаблонов в комментариях
4.1.1.3 - Шпаргалка по конвейерам и функциям в html/template (Go)
Конвейеры позволяют последовательно применять функции к данным в шаблонах.
Синтаксис:
{{value | function1 | function2 | ...}}
Примеры:
<!-- Простой конвейер -->
<p>{{.Name | upper}}</p>
<!-- Множественные функции -->
<p>{{.Title | trim | lower | trunc 50}}</p>
<!-- С аргументами -->
<p>{{.Date | format "2006-01-02"}}</p>
<!-- Вложенные конвейеры -->
<p>{{.Description | trunc 100 | html}}</p>
Встроенные функции
1. Строковые функции
{{"hello" | title}} <!-- Hello -->
{{"HELLO" | lower}} <!-- hello -->
{{"hello" | upper}} <!-- HELLO -->
{{" hello " | trim}} <!-- hello -->
{{"hello" | repeat 3}} <!-- hellohellohello -->
2. HTML-функции
{{"<script>" | html}} <!-- Экранирование HTML -->
{{"<b>safe</b>" | html}} <!-- <b>safe</b> -->
{{"<b>safe</b>" | safeHTML}} <!-- <b>safe</b> (без экранирования) -->
3. Функции для коллекций
{{len .Items}} <!-- Длина массива/слайса/мапы -->
{{index .Users 0}} <!-- Первый элемент -->
{{index .Map "key"}} <!-- Значение по ключу -->
{{slice .List 1 3}} <!-- Подслайс -->
4. Логические функции
{{if eq .A .B}}...{{end}} <!-- Равно -->
{{if ne .A .B}}...{{end}} <!-- Не равно -->
{{if lt .A .B}}...{{end}} <!-- Меньше -->
{{if le .A .B}}...{{end}} <!-- Меньше или равно -->
{{if gt .A .B}}...{{end}} <!-- Больше -->
{{if ge .A .B}}...{{end}} <!-- Больше или равно -->
{{if and .A .B}}...{{end}} <!-- Логическое И -->
{{if or .A .B}}...{{end}} <!-- Логическое ИЛИ -->
{{if not .A}}...{{end}} <!-- Логическое НЕ -->
5. Математические функции
{{add 1 2}} <!-- 3 -->
{{sub 5 2}} <!-- 3 -->
{{mul 3 4}} <!-- 12 -->
{{div 10 2}} <!-- 5 -->
{{mod 10 3}} <!-- 1 -->
6. Функции форматирования
{{printf "%s - %d" .Name .Age}} <!-- Форматированная строка -->
{{.Price | printf "$%.2f"}} <!-- Форматирование числа -->
{{.Date | date "2006-01-02"}} <!-- Форматирование даты -->
Пользовательские функции
Регистрация функций:
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"trunc": func(s string, max int) string {
if len(s) > max {
return s[:max] + "..."
}
return s
},
"add": func(a, b int) int { return a + b },
"contains": strings.Contains,
"now": time.Now,
}
tmpl := template.New("").Funcs(funcMap)
Использование в шаблоне:
{{.Title | trunc 50 | upper}}
{{add .Value 10}}
{{if contains .Description "important"}}Important!{{end}}
{{now | date "2006-01-02"}}
Примеры сложных конвейеров
1. Обработка текста
{{.UserComment | trim | trunc 200 | html}}
2. Форматирование данных
{{.CreatedAt | date "02.01.2006" | printf "Created: %s"}}
3. Работа с коллекциями
{{range .Items | sortBy "Name" | limit 10}}
{{.Name | upper}}
{{end}}
4. Условные конвейеры
{{if .Price | gt 100}}
<p class="expensive">{{.Price | printf "$%.2f"}}</p>
{{else}}
<p>{{.Price | printf "$%.2f"}}</p>
{{end}}
Полезные комбинации
Безопасный вывод HTML:
{{.UserContent | sanitizeHTML | safeHTML}}
Форматирование денежных значений:
{{.Amount | div 100 | printf "$%.2f"}}
Сортировка и фильтрация:
{{range .Products | filter "category" "electronics" | sortBy "price"}}
{{.Name}} - {{.Price | printf "$%.2f"}}
{{end}}
Ограничения
- Функции должны возвращать только одно значение (и опционально ошибку)
- Невозможно передавать функции как аргументы
- Нет поддержки сложных операций с каналами
- Автоматическое экранирование применяется к конечному результату
Лучшие практики
- Выносите сложную логику в Go-код, а не в шаблоны
- Используйте конвейеры для читаемости, но не переусердствуйте
- Документируйте пользовательские функции
- Тестируйте шаблоны с разными входными данными
- Избегайте побочных эффектов в функциях
4.1.1.4 - Функции экранирования HTML/JavaScript/URL пакета html/template языка Go
Описание функций пакета html/template языка Go в соответствии с официальной документацией
HTMLEscape
func HTMLEscape(w io.Writer, b []byte)
HTMLEscape записывает в w экранированный HTML-эквивалент текстовых данных b.
HTMLEscapeString
func HTMLEscapeString(s string) string
HTMLEscapeString возвращает экранированный HTML-эквивалент текстовых данных s.
HTMLEscaper
func HTMLEscaper(args ...any) string
HTMLEscaper возвращает экранированный HTML-эквивалент текстового представления своих аргументов.
IsTrue
func IsTrue(val any) (truth, ok bool)
IsTrue определяет, является ли значение “истинным” (не нулевым для своего типа) и имеет ли оно вообще смысл как булево значение. Это определение истинности используется в условных операциях типа if.
JSEscape
func JSEscape(w io.Writer, b []byte)
JSEscape записывает в w экранированный JavaScript-эквивалент текстовых данных b.
JSEscapeString
func JSEscapeString(s string) string
JSEscapeString возвращает экранированный JavaScript-эквивалент текстовых данных s.
JSEscaper
func JSEscaper(args ...any) string
JSEscaper возвращает экранированный JavaScript-эквивалент текстового представления своих аргументов.
URLQueryEscaper
func URLQueryEscaper(args ...any) string
URLQueryEscaper возвращает экранированное значение текстового представления аргументов в форме, пригодной для вставки в URL-запрос.
4.1.1.5 - Типы экранирования HTML/JavaScript/URL пакета html/template языка Go
Описание типов и функций пакета html/template языка Go в соответствии с официальной документацией
type CSS
CSS инкапсулирует заранее проверенное содержимое, соответствующее:
- Производству CSS3 таблиц стилей (напр.
p { color: purple })
- Производству CSS3 правил (напр.
a[href=~"https:"].foo#bar)
- Производству CSS3 деклараций (напр.
color: red; margin: 2px)
- Производству CSS3 значений (напр.
rgba(0, 0, 255, 127))
Внимание: Использование этого типа представляет угрозу безопасности - содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.
type Error
type Error struct {
ErrorCode ErrorCode // Тип ошибки
Node parse.Node // Узел, вызвавший проблему (если известен)
Name string // Имя шаблона с ошибкой
Line int // Номер строки в исходнике шаблона
Description string // Человекочитаемое описание
}
Описывает проблему, возникшую при экранировании шаблона.
Коды ошибок (ErrorCode)
const (
OK ErrorCode = iota // Нет ошибки
// Неоднозначный контекст в URL
ErrAmbigContext
// Некорректный HTML (незакрытые теги/атрибуты)
ErrBadHTML
// Ветки if/range заканчиваются в разных контекстах
ErrBranchEnd
// Шаблон заканчивается в нетекстовом контексте
ErrEndContext
// Отсутствует указанный шаблон
ErrNoSuchTemplate
// Невозможно вычислить контекст вывода
ErrOutputContext
// Незавершённый набор символов в regexp JS
ErrPartialCharset
// Незавершённая escape-последовательность
ErrPartialEscape
// Повторный вход в range с изменением контекста
ErrRangeLoopReentry
// Неоднозначная интерпретация '/' (деление или regexp)
ErrSlashAmbig
// Использование запрещённых экранировщиков
ErrPredefinedEscaper
// Устаревшая ошибка для JS template literals
ErrJSTemplate
)
Особое значение ZgotmplZ
Пример:
где {{.X}} вычисляется в javascript:...
Результат:
Объяснение:
“ZgotmplZ” - специальное значение, указывающее, что небезопасное содержимое достигло CSS или URL контекста во время выполнения. Для доверенных источников используйте типы содержимого для отключения фильтрации: URL(javascript:…).
type FuncMap
type FuncMap = template.FuncMap
FuncMap - это псевдоним для типа template.FuncMap из пакета text/template, представляющий карту функций для использования в шаблонах.
type HTML
HTML инкапсулирует безопасный фрагмент HTML-документа. Не следует использовать для:
- HTML из ненадёжных источников
- HTML с незакрытыми тегами или комментариями
Внимание: Содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.
type HTMLAttr
HTMLAttr инкапсулирует безопасный HTML-атрибут, например dir="ltr".
Внимание: Как и с HTML, содержимое должно быть доверенным, так как включается без экранирования.
type JS
JS инкапсулирует безопасное выражение EcmaScript5, например (x + y * z()).
Особенности:
- Авторы шаблонов должны обеспечивать правильный порядок операций
- Не должно быть неоднозначности между выражениями и инструкциями
Внимание: Не безопасно использовать для непроверенного JSON. Для JSON лучше использовать json.Unmarshal.
type JSStr
JSStr инкапсулирует безопасную строку для JavaScript, которая будет заключена в кавычки.
Требования:
- Допустимые символы или escape-последовательности
- Запрещены переносы строк внутри escape-последовательностей
Пример:
JSStr("foo\\nbar") // допустимо
JSStr("foo\\\nbar") // недопустимо
type Srcset
Srcset инкапсулирует безопасный атрибут srcset для тегов <img> (спецификация HTML5).
type Template
type Template struct {
Tree *parse.Tree // Абстрактное синтаксическое дерево шаблона
// ... скрытые поля
}
Template - это специализированная версия text/template, которая генерирует безопасные фрагменты HTML-документов.
Ключевые особенности:
- Расширяет базовый функционал text/template
- Автоматически применяет контекстно-зависимое экранирование
- Гарантирует безопасность вывода при правильном использовании
Все типы безопасного контента (HTML, JS и др.) требуют, чтобы их содержимое поступало из доверенных источников, так как они включаются в вывод без дополнительного экранирования.
Пример Block
package main
import (
"html/template"
"log"
"os"
"strings"
)
func main() {
const (
master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
overlay = `{{define "list"}} {{join . ", "}}{{end}} `
)
var (
funcs = template.FuncMap{"join": strings.Join}
guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
)
masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
if err != nil {
log.Fatal(err)
}
overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
if err != nil {
log.Fatal(err)
}
if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
log.Fatal(err)
}
if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
log.Fatal(err)
}
}
Output:
Names:
- Gamora
- Groot
- Nebula
- Rocket
- Star-Lord
Names: Gamora, Groot, Nebula, Rocket, Star-Lord
Пример Glob
Здесь мы демонстрируем загрузку набора шаблонов из каталога.
package main
import (
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
name string
contents string
}
func createTestDir(files []templateFile) string {
dir, err := os.MkdirTemp("", "template")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
f, err := os.Create(filepath.Join(dir, file.name))
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = io.WriteString(f, file.contents)
if err != nil {
log.Fatal(err)
}
}
return dir
}
func main() {
// Here we create a temporary directory and populate it with our sample
// template definition files; usually the template files would already
// exist in some location known to the program.
dir := createTestDir([]templateFile{
// T0.tmpl is a plain template file that just invokes T1.
{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
// T1.tmpl defines a template, T1 that invokes T2.
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
// T2.tmpl defines a template T2.
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
})
// Clean up after the test; another quirk of running as an example.
defer os.RemoveAll(dir)
// pattern is the glob pattern used to find all the template files.
pattern := filepath.Join(dir, "*.tmpl")
// Here starts the example proper.
// T0.tmpl is the first name matched, so it becomes the starting template,
// the value returned by ParseGlob.
tmpl := template.Must(template.ParseGlob(pattern))
err := tmpl.Execute(os.Stdout, nil)
if err != nil {
log.Fatalf("template execution: %s", err)
}
}
Output:
T0 invokes T1: (T1 invokes T2: (This is T2))
Пример Helpers
Этот пример демонстрирует один из способов поделиться некоторыми шаблонами и использовать их в разных контекстах. В этом варианте мы вручную добавляем несколько шаблонов драйверов в существующий пакет шаблонов.
package main
import (
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
name string
contents string
}
func createTestDir(files []templateFile) string {
dir, err := os.MkdirTemp("", "template")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
f, err := os.Create(filepath.Join(dir, file.name))
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = io.WriteString(f, file.contents)
if err != nil {
log.Fatal(err)
}
}
return dir
}
func main() {
// Here we create a temporary directory and populate it with our sample
// template definition files; usually the template files would already
// exist in some location known to the program.
dir := createTestDir([]templateFile{
// T1.tmpl defines a template, T1 that invokes T2.
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
// T2.tmpl defines a template T2.
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
})
// Clean up after the test; another quirk of running as an example.
defer os.RemoveAll(dir)
// pattern is the glob pattern used to find all the template files.
pattern := filepath.Join(dir, "*.tmpl")
// Here starts the example proper.
// Load the helpers.
templates := template.Must(template.ParseGlob(pattern))
// Add one driver template to the bunch; we do this with an explicit template definition.
_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
if err != nil {
log.Fatal("parsing driver1: ", err)
}
// Add another driver template.
_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
if err != nil {
log.Fatal("parsing driver2: ", err)
}
// We load all the templates before execution. This package does not require
// that behavior but html/template's escaping does, so it's a good habit.
err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
if err != nil {
log.Fatalf("driver1 execution: %s", err)
}
err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
if err != nil {
log.Fatalf("driver2 execution: %s", err)
}
}
Output:
Driver 1 calls T1: (T1 invokes T2: (This is T2))
Driver 2 calls T2: (This is T2)
Пример Parsefiles
Здесь мы демонстрируем загрузку набора шаблонов из файлов в разных каталогах.
package main
import (
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
name string
contents string
}
func createTestDir(files []templateFile) string {
dir, err := os.MkdirTemp("", "template")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
f, err := os.Create(filepath.Join(dir, file.name))
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = io.WriteString(f, file.contents)
if err != nil {
log.Fatal(err)
}
}
return dir
}
func main() {
// Here we create different temporary directories and populate them with our sample
// template definition files; usually the template files would already
// exist in some location known to the program.
dir1 := createTestDir([]templateFile{
// T1.tmpl is a plain template file that just invokes T2.
{"T1.tmpl", `T1 invokes T2: ({{template "T2"}})`},
})
dir2 := createTestDir([]templateFile{
// T2.tmpl defines a template T2.
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
})
// Clean up after the test; another quirk of running as an example.
defer func(dirs ...string) {
for _, dir := range dirs {
os.RemoveAll(dir)
}
}(dir1, dir2)
// Here starts the example proper.
// Let's just parse only dir1/T0 and dir2/T2
paths := []string{
filepath.Join(dir1, "T1.tmpl"),
filepath.Join(dir2, "T2.tmpl"),
}
tmpl := template.Must(template.ParseFiles(paths...))
err := tmpl.Execute(os.Stdout, nil)
if err != nil {
log.Fatalf("template execution: %s", err)
}
}
Output:
T1 invokes T2: (This is T2)
Пример Share
В этом примере показано, как использовать одну группу шаблонов драйверов с различными наборами вспомогательных шаблонов.
package main
import (
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
name string
contents string
}
func createTestDir(files []templateFile) string {
dir, err := os.MkdirTemp("", "template")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
f, err := os.Create(filepath.Join(dir, file.name))
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = io.WriteString(f, file.contents)
if err != nil {
log.Fatal(err)
}
}
return dir
}
func main() {
// Here we create a temporary directory and populate it with our sample
// template definition files; usually the template files would already
// exist in some location known to the program.
dir := createTestDir([]templateFile{
// T0.tmpl is a plain template file that just invokes T1.
{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
})
// Clean up after the test; another quirk of running as an example.
defer os.RemoveAll(dir)
// pattern is the glob pattern used to find all the template files.
pattern := filepath.Join(dir, "*.tmpl")
// Here starts the example proper.
// Load the drivers.
drivers := template.Must(template.ParseGlob(pattern))
// We must define an implementation of the T2 template. First we clone
// the drivers, then add a definition of T2 to the template name space.
// 1. Clone the helper set to create a new name space from which to run them.
first, err := drivers.Clone()
if err != nil {
log.Fatal("cloning helpers: ", err)
}
// 2. Define T2, version A, and parse it.
_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
if err != nil {
log.Fatal("parsing T2: ", err)
}
// Now repeat the whole thing, using a different version of T2.
// 1. Clone the drivers.
second, err := drivers.Clone()
if err != nil {
log.Fatal("cloning drivers: ", err)
}
// 2. Define T2, version B, and parse it.
_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
if err != nil {
log.Fatal("parsing T2: ", err)
}
// Execute the templates in the reverse order to verify the
// first is unaffected by the second.
err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
if err != nil {
log.Fatalf("second execution: %s", err)
}
err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
if err != nil {
log.Fatalf("first: execution: %s", err)
}
}
Output:
T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))
func Must
func Must(t *Template, err error) *Template
Must - вспомогательная функция, которая оборачивает вызов функции, возвращающей (*Template, error), и вызывает панику, если ошибка не равна nil. Предназначена для инициализации переменных:
Пример:
var t = template.Must(template.New("name").Parse("html"))
func New
func New(name string) *Template
New создает новый HTML-шаблон с указанным именем.
func ParseFS (добавлена в Go 1.16)
func ParseFS(fs fs.FS, patterns ...string) (*Template, error)
ParseFS аналогична ParseFiles и ParseGlob, но читает из файловой системы fs вместо файловой системы хоста. Принимает список glob-шаблонов (большинство имен файлов работают как glob-шаблоны, соответствующие только самим себе).
func ParseFiles
func ParseFiles(filenames ...string) (*Template, error)
ParseFiles создает новый шаблон и парсит определения шаблонов из указанных файлов. Имя возвращаемого шаблона будет соответствовать базовому имени первого файла. Должен быть указан хотя бы один файл.
Особенности:
- При парсинге нескольких файлов с одинаковыми именами будет использован последний указанный файл
- Например,
ParseFiles("a/foo", "b/foo") сохранит “b/foo” как шаблон “foo”, а “a/foo” будет недоступен
func ParseGlob
func ParseGlob(pattern string) (*Template, error)
ParseGlob создает новый шаблон и парсит определения шаблонов из файлов, соответствующих указанному шаблону (согласно семантике filepath.Match). Шаблон должен соответствовать хотя бы одному файлу.
Эквивалентно вызову ParseFiles со списком файлов, соответствующих шаблону. Как и в ParseFiles, при дублировании имен используется последний файл.
func (*Template) AddParseTree
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)
AddParseTree создает новый шаблон с указанным именем и деревом разбора, ассоциируя его с t.
Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены.
func (*Template) Clone
func (t *Template) Clone() (*Template, error)
Clone создает копию шаблона, включая все ассоциированные шаблоны. Последующие вызовы Parse в копии будут добавлять шаблоны только в копию.
Возвращает ошибку, если t уже был выполнен.
func (*Template) DefinedTemplates (добавлена в Go 1.6)
func (t *Template) DefinedTemplates() string
DefinedTemplates возвращает строку с перечислением определенных шаблонов с префиксом “; defined templates are: “. Если шаблонов нет, возвращает пустую строку. Используется для генерации сообщений об ошибках.
func (*Template) Delims
func (t *Template) Delims(left, right string) *Template
Delims устанавливает разделители действий для последующих вызовов Parse, ParseFiles или ParseGlob. Вложенные определения шаблонов наследуют эти настройки. Пустые разделители соответствуют значениям по умолчанию: {{ и }}.
Возвращает сам шаблон, позволяя объединять вызовы в цепочку.
Пример
package main
import (
"html/template"
"log"
"os"
)
func main() {
const text = "<<.Greeting>> {{.Name}}"
data := struct {
Greeting string
Name string
}{
Greeting: "Hello",
Name: "Joe",
}
t := template.Must(template.New("tpl").Delims("<<", ">>").Parse(text))
err := t.Execute(os.Stdout, data)
if err != nil {
log.Fatal(err)
}
}
func (*Template) Execute
func (t *Template) Execute(wr io.Writer, data any) error
Применяет разобранный шаблон к указанным данным, записывая результат в wr. В случае ошибки выполнение прерывается, но частичные результаты могут быть уже записаны. Шаблон можно выполнять параллельно, но при использовании общего Writer вывод может перемешиваться.
func (*Template) ExecuteTemplate
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error
Применяет шаблон с указанным именем, ассоциированный с t, к данным и записывает результат в wr. Поведение при ошибках и параллельном выполнении аналогично Execute.
Отличие Execute от ExecuteTemplate
Отличие между Execute и ExecuteTemplate
Основное различие между этими методами заключается в том, какой шаблон они выполняют:
Execute - выполняет основной (корневой) шаблон
ExecuteTemplate - выполняет указанный именованный шаблон (включая ассоциированные шаблоны)
Пример использования:
package main
import (
"os"
"text/template"
)
func main() {
// Создаем шаблон с именем "base"
tmpl := template.Must(template.New("base").Parse(`
{{define "header"}}<h1>Заголовок</h1>{{end}}
{{define "content"}}<p>Основное содержимое</p>{{end}}
<!DOCTYPE html>
<html>
<head><title>Пример</title></head>
<body>
{{template "header"}}
{{template "content"}}
</body>
</html>
`))
// 1. Execute - выполнит основной шаблон "base"
err := tmpl.Execute(os.Stdout, nil)
if err != nil {
panic(err)
}
// 2. ExecuteTemplate - можно выполнить любой из определенных шаблонов
err = tmpl.ExecuteTemplate(os.Stdout, "header", nil)
if err != nil {
panic(err)
}
}
Ключевые отличия:
| Характеристика |
Execute |
ExecuteTemplate |
| Какой шаблон выполняется |
Только основной (корневой) |
Любой именованный (включая основной) |
| Использование |
Когда нужно выполнить весь шаблон |
Когда нужна часть шаблона |
| Производительность |
Чуть быстрее |
Чуть медленнее (поиск по имени) |
| Гибкость |
Меньше |
Больше (доступ ко всем шаблонам) |
Когда использовать:
Execute - для рендеринга всего шаблона целиком
ExecuteTemplate - когда нужно:
- выполнить часть шаблона
- переиспользовать компоненты в разных контекстах
- отладить отдельный блок шаблона
func (*Template) Funcs
func (t *Template) Funcs(funcMap FuncMap) *Template
Добавляет функции из переданной мапы в функциональную мапу шаблона. Должен вызываться до парсинга шаблона. Вызывает панику, если значение в мапе не является функцией с подходящим типом возвращаемого значения. Возвращает сам шаблон для цепочки вызовов.
func (*Template) Lookup
func (t *Template) Lookup(name string) *Template
Возвращает шаблон с указанным именем, ассоциированный с t, или nil если такого шаблона нет.
func (*Template) Name
func (t *Template) Name() string
Возвращает имя шаблона.
func (*Template) New
func (t *Template) New(name string) *Template
Создает новый HTML-шаблон с указанным именем, ассоциированный с текущим и имеющий те же разделители. Позволяет использовать вызовы шаблонов через {{template}}. Если шаблон с таким именем уже существует, он будет заменен.
func (*Template) Option
func (t *Template) Option(opt ...string) *Template
Устанавливает опции шаблона в формате строк “ключ=значение” или простых строк. Вызывает панику при неизвестной или неверной опции.
Поддерживаемые опции:
missingkey=default или missingkey=invalid - поведение по умолчанию (продолжать выполнение)
missingkey=zero - возвращать нулевое значение для отсутствующего ключа
missingkey=error - прерывать выполнение с ошибкой
func (*Template) Parse
func (t *Template) Parse(text string) (*Template, error)
Разбирает текст как тело шаблона для t. Определения именованных шаблонов ({{define}}/{{block}}) становятся ассоциированными шаблонами. Шаблоны можно переопределять до первого вызова Execute. Пустые определения (только пробелы/комментарии) не заменяют существующие шаблоны.
func (*Template) ParseFS
func (t *Template) ParseFS(fs fs.FS, patterns ...string) (*Template, error)
ParseFS аналогична Template.ParseFiles и Template.ParseGlob, но читает из абстрактной файловой системы fs вместо файловой системы хоста. Принимает список glob-шаблонов (большинство имен файлов работают как glob-шаблоны, соответствующие только самим себе).
func (*Template) ParseFiles
func (t *Template) ParseFiles(filenames ...string) (*Template, error)
ParseFiles парсит указанные файлы и ассоциирует полученные шаблоны с t. При ошибке парсинг останавливается и возвращается nil, иначе возвращается t. Должен быть указан хотя бы один файл.
Особенности:
- При парсинге нескольких файлов с одинаковыми именами используется последний указанный файл
- Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены
func (*Template) ParseGlob
func (t *Template) ParseGlob(pattern string) (*Template, error)
ParseGlob парсит шаблоны из файлов, соответствующих указанному шаблону (согласно семантике filepath.Match), и ассоциирует их с t. Эквивалентно вызову t.ParseFiles со списком файлов, соответствующих шаблону.
Особенности:
- Как и в ParseFiles, при дублировании имен используется последний файл
- Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены
func (*Template) Templates
func (t *Template) Templates() []*Template
Templates возвращает срез всех шаблонов, ассоциированных с t, включая сам t.
type URL
URL инкапсулирует безопасный URL или подстроку URL (согласно RFC 3986). URL типа javascript:checkThatFormNotEditedBeforeLeavingPage() из доверенного источника может быть использован, но по умолчанию динамические javascript: URL фильтруются как частый вектор атак.
Внимание: Использование этого типа представляет угрозу безопасности - содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.
4.2 - Пакет bufio встроенных функций Go
Пакет bufio реализует буферизованный ввод-вывод. Он оборачивает объект io.Reader или io.Writer, создавая другой объект (Reader или Writer), который также реализует интерфейс, но обеспечивает буферизацию и некоторую помощь для текстового ввода-вывода.
Объяснение bufio
Пакет bufio предоставляет буферизованный ввод/вывод, что делает работу с потоками данных более эффективной. Он особенно полезен для:
- Чтения больших файлов по частям
- Обработки текстовых данных построчно
- Разбивки входных данных на слова, символы и другие логические единицы
Почему использовать bufio?
- Эффективность: Буферизация уменьшает количество системных вызовов
- Удобство: Готовые функции для распространенных задач (чтение строк, слов)
- Гибкость: Можно создавать собственные функции разделения
- Производительность: Особенно заметна при работе с большими файлами
Пример комплексного использования:
file, _ := os.Open("data.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanWords) // Разбиваем по словам
wordCount := 0
for scanner.Scan() {
wordCount++
}
fmt.Printf("Файл содержит %d слов", wordCount)
Пакет bufio - это мощный инструмент для эффективной обработки текстовых и бинарных данных в Go, который стоит использовать вместо базовых операций ввода-вывода, когда важны производительность и удобство работы.
Константы
const (
// maxscantokensize - максимальный размер, используемый для буфера токена
// Если пользователь не предоставит явный буфер с [scanner.buffer].
// фактический максимальный размер токена может быть меньше в качестве буфера
// может потребоваться включить, например, новую линию.
Maxscantokensize = 64 * 1024
)
Переменные
var (
ErrinValidunReadByte = errors.new ("Bufio: недействительное использование UnReadByte")
Errinvalidunreadrune = errors.new ("bufio: недействительное использование Undeardrune")
Errbufferfull = errors.new ("bufio: buffer full")
Errnegativecount = errors.new ("bufio: отрицательный счет")
)
var (
Errtoolong = errors.new ("bufio.scanner: токен слишком длинный")
Errnegativeadvance = errors.new ("bufio.scanner: splitfunc возвращает отрицательное значение сдвига") ")
ErradvancetOfar = errors.new ("bufio.scanner: splitfunc возвращает сдвиг, превышающий входные данные")
ErrbadReadCount = errors.new ("bufio.scanner: read возвращается невозможное количество")
)
Ошибки возвращаемые Scanner.
var errfinaltoken = errors.new ("Окончательный токен")
ErrFinalToken — это специальное контрольное значение ошибки. Оно предназначено для возврата функцией Split, чтобы указать, что сканирование должно быть остановлено без ошибки. Если токен, передаваемый с этой ошибкой, не равен nil, то этот токен является последним токеном.
Это значение полезно для досрочного прекращения обработки или когда необходимо передать последний пустой токен (который отличается от токена nil). Такое же поведение можно достичь с помощью пользовательского значения ошибки, но использование этого значения более удобно. Пример использования этого значения см. в примере emptyFinalToken.
Функции
func ScanBytes
func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanBytes — это функция разделения для сканера, которая возвращает каждый байт в виде токена.
Назначение: Чтение входных данных побайтово.
Пример:
scanner := bufio.NewScanner(strings.NewReader("Go"))
scanner.Split(bufio.ScanBytes)
for scanner.Scan() {
fmt.Printf("%q ", scanner.Bytes())
}
// Вывод: 'G' 'o'
Когда использовать:
- Когда нужно обрабатывать каждый байт данных
- Для низкоуровневой обработки бинарных данных
func ScanLines
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanLines — это функция разделения для сканера, которая возвращает каждую строку текста без конечного маркера конца строки. Возвращаемая строка может быть пустой. Маркер конца строки — это один необязательный возврат каретки, за которым следует один обязательный символ новой строки. В нотации регулярных выражений это \r?\n. Последняя непустая строка ввода будет возвращена, даже если в ней нет символа новой строки.
Назначение: Чтение входных данных построчно (по умолчанию в Scanner).
Пример:
scanner := bufio.NewScanner(strings.NewReader("line1\nline2\n"))
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
// Вывод:
// line1
// line2
Когда использовать:
- Для обработки логов
- Чтения конфигурационных файлов
- Обработки любого текста, организованного по строкам
func ScanRunes
func ScanRunes(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanRunes — это функция разделения для сканера, которая возвращает каждую руну, закодированную в UTF-8, в виде токена. Последовательность возвращаемых рун эквивалентна последовательности из цикла range над входом в виде строки, что означает, что ошибочные кодировки UTF-8 переводятся в U+FFFD = “\xef\xbf\xbd”. Из-за интерфейса Scan это делает невозможным для клиента отличить правильно закодированные руны-замены от ошибок кодирования.
Назначение: Чтение входных данных посимвольно (с поддержкой UTF-8).
Пример:
scanner := bufio.NewScanner(strings.NewReader("Привет"))
scanner.Split(bufio.ScanRunes)
for scanner.Scan() {
fmt.Printf("%q ", scanner.Text())
}
// Вывод: 'П' 'р' 'и' 'в' 'е' 'т'
Когда использовать:
- Для обработки Unicode-текстов
- Когда нужно работать с отдельными символами (рунами)
- Для анализа текста на уровне символов
func ScanWords
func ScanWords(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanWords — это функция разделения для Scanner, которая возвращает каждое слово текста, разделенное пробелом, с удалением окружающих пробелов. Она никогда не возвращает пустую строку. Определение пробела устанавливается unicode.IsSpace.
Назначение: Чтение входных данных по словам.
Пример:
scanner := bufio.NewScanner(strings.NewReader("Hello world!"))
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
// Вывод:
// Hello
// world!
Когда использовать:
- Для анализа текста по словам
- При обработке естественного языка
- Для подсчета слов в тексте
Типы
type ReadWriter
type ReadWriter struct {
*Reader
*Writer
}
ReadWriter хранит указатели на Reader и Writer. Он реализует io.ReadWriter.
func NewReadWriter
func NewReadWriter(r *Reader, w *Writer) *ReadWriter
NewReadWriter выделяет новый ReadWriter, который отправляет данные в R и W.
type Reader
type Reader struct {
// contains filtered or unexported fields
}
Reader реализует буферизацию для объекта io.Reader. Новый Reader создается вызовом NewReader или NewReaderSize; в качестве альтернативы можно использовать нулевое значение Reader после вызова [Reset] для него.
Объяснение отличий Scanner, Reader и Writer
Сравнение Scanner, Reader и Writer в пакете bufio
Основные различия
1. Reader (буферизованное чтение)
Что делает:
- Читает данные порциями в буфер
- Предоставляет низкоуровневые методы (
Read, ReadByte, ReadLine и др.)
- Требует ручного управления процессом чтения
Когда использовать:
reader := bufio.NewReader(file)
// Чтение точного количества байт
buf := make([]byte, 1024)
n, err := reader.Read(buf)
// Чтение до разделителя
line, err := reader.ReadString('\n')
Лучше использовать когда:
- Нужен точный контроль над процессом чтения
- Требуется чтение данных фиксированного размера
- Работа с бинарными данными
- Когда нужно несколько раз переиспользовать один и тот же буфер
2. Scanner (высокоуровневое сканирование)
Что делает:
- Автоматически разбивает входные данные на токены
- Предоставляет простой итерационный интерфейс
- Имеет встроенные функции для разделения на строки, слова, руны
Когда использовать:
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text()) // автоматическое чтение строк
}
Лучше использовать когда:
- Обработка текстовых данных построчно
- Нужно разделять входные данные по сложным правилам
- Работа с текстовыми форматами (логи, CSV и т.д.)
- Когда важна простота кода
3. Writer (буферизованная запись)
Что делает:
- Накапливает данные в буфере перед записью
- Предоставляет методы для эффективной записи (
Write, WriteString, WriteByte)
Когда использовать:
writer := bufio.NewWriter(file)
writer.WriteString("Hello, ")
writer.WriteString("World!\n")
writer.Flush() // запись из буфера в файл
Сравнительная таблица
| Характеристика |
Reader |
Scanner |
Writer |
| Уровень абстракции |
Низкий |
Высокий |
Средний |
| Управление буфером |
Ручное |
Автоматическое |
Полуавтоматическое |
| Чтение строк |
ReadString(’\n') |
Scan() + Text() |
- |
| Производительность |
Очень высокая |
Высокая |
Высокая |
| Удобство использования |
Среднее |
Очень высокое |
Высокое |
| Лучший случай использования |
Бинарные данные, сети |
Текстовые файлы, логи |
Запись в файлы, сети |
Практические рекомендации
-
Используйте Scanner когда:
- Обрабатываете текстовые файлы построчно
- Нужно разделять данные по словам/символам
- Хотите простой и читаемый код
// Подсчет слов в файле
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanWords)
count := 0
for scanner.Scan() {
count++
}
-
Используйте Reader когда:
- Работаете с бинарными данными
- Нужно читать данные точными порциями
- Требуется низкоуровневый контроль
// Чтение заголовка файла
reader := bufio.NewReader(file)
header := make([]byte, 16)
_, err := reader.Read(header)
-
Используйте Writer когда:
- Нужно эффективно записывать много небольших фрагментов
- Важна производительность при записи
// Буферизованная запись
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
writer.WriteString(fmt.Sprintf("Line %d\n", i))
}
writer.Flush()
Производительность
- Для больших файлов
Reader обычно быстрее Scanner
- Для текстовой обработки
Scanner удобнее и часто достаточно быстр
Writer дает значительный прирост производительности при множественных мелких записях
Заключение
Выбор между Scanner, Reader и Writer зависит от конкретной задачи:
- Простота и удобство →
Scanner
- Контроль и производительность →
Reader/Writer
- Текстовые данные →
Scanner
- Бинарные данные →
Reader
- Эффективная запись →
Writer
func NewReader
func NewReader(rd io.Reader) *Reader
NewReader возвращает новый объект Reader, буфер которого имеет размер по умолчанию.
func NewReaderSize
func NewReaderSize(rd io.Reader, size int) *Reader
NewReaderSize возвращает новый Reader, буфер которого имеет как минимум указанный размер. Если аргумент io.Reader уже является Reader с достаточно большим размером, то возвращается базовый Reader.
func (*Reader) Buffered
func (b *Reader) Buffered() int
Buffered возвращает количество байтов, которые можно прочитать из текущего буфера.
func (*Reader) Discard
added in go1.5
func (b *Reader) Discard(n int) (discarded int, err error)
Discard пропускает следующие n байтов и возвращает количество пропущенных байтов.
Если Discard пропускает меньше n байтов, он также возвращает ошибку. Если 0 <= n <= b.Buffered(), Discard гарантированно выполнится успешно без чтения из базового io.Reader.
func (*Reader) Peek
func (b *Reader) Peek(n int) ([]byte, error)
Peek возвращает следующие n байтов, не продвигая считывающее устройство. Байты перестают быть действительными при следующем вызове чтения. При необходимости Peek прочитает больше байтов в буфер, чтобы сделать доступными n байтов. Если Peek возвращает меньше n байтов, он также возвращает ошибку, объясняющую, почему чтение является коротким. Ошибка ErrBufferFull возникает, если n больше размера буфера b.
Вызов Peek предотвращает выполнение вызова Reader.UnreadByte или Reader.UnreadRune до следующей операции чтения.
func (*Reader) Read
func (b *Reader) Read(p []byte) (n int, err error)
Read считывает данные в p. Он возвращает количество байтов, прочитанных в p. Байты берутся максимум из одного Read в базовом Reader, поэтому n может быть меньше len(p). Чтобы прочитать ровно len(p) байтов, используйте io.ReadFull(b, p). Если базовый Reader может возвращать ненулевое значение с io.EOF, то этот метод Read также может это делать; см. документацию io.Reader.
func (*Reader) ReadByte
func (b *Reader) ReadByte() (byte, error)
ReadByte считывает и возвращает один байт. Если байт недоступен, возвращает ошибку.
func (*Reader) ReadBytes
func (b *Reader) ReadBytes(delim byte) ([]byte, error)
ReadBytes читает входные данные до первого появления разделителя delim, возвращая фрагмент, содержащий данные до разделителя включительно. Если ReadBytes обнаруживает ошибку до нахождения разделителя, он возвращает данные, прочитанные до ошибки, и саму ошибку (часто io.EOF). ReadBytes возвращает err != nil, если и только если возвращаемые данные не заканчиваются на delim. Для простых задач может быть более удобно использовать Scanner.
func (*Reader) ReadLine
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
ReadLine — это низкоуровневая примитивная функция чтения строк. Большинству вызывающих функций следует использовать Reader.ReadBytes(’\n’) или Reader.ReadString(’\n’) вместо нее, либо использовать Scanner.
ReadLine пытается вернуть одну строку, не включая байты конца строки. Если строка была слишком длинной для буфера, то устанавливается isPrefix и возвращается начало строки. Остальная часть строки будет возвращена при последующих вызовах. isPrefix будет false при возвращении последнего фрагмента строки. Возвращаемый буфер действителен только до следующего вызова ReadLine. ReadLine либо возвращает строку, отличную от nil, либо возвращает ошибку, но никогда и то, и другое одновременно.
Текст, возвращаемый из ReadLine, не включает конец строки («\r\n» или «\n»). Если ввод заканчивается без конечного конца строки, никаких указаний или ошибок не выдается. Вызов Reader.UnreadByte после ReadLine всегда отменяет чтение последнего прочитанного байта (возможно, символа, принадлежащего концу строки), даже если этот байт не является частью строки, возвращаемой ReadLine.
func (*Reader) ReadRune
func (b *Reader) ReadRune() (r rune, size int, err error)
ReadRune считывает один символ Unicode, закодированный в UTF-8, и возвращает руну и ее размер в байтах. Если закодированная руна недействительна, она потребляет один байт и возвращает unicode.ReplacementChar (U+FFFD) с размером 1.
func (*Reader) ReadSlice
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
ReadSlice читает до первого появления разделителя в входных данных, возвращая фрагмент, указывающий на байты в буфере. Байты перестают быть действительными при следующем чтении. Если ReadSlice встречает ошибку до нахождения разделителя, он возвращает все данные в буфере и саму ошибку (часто io.EOF). ReadSlice завершается с ошибкой ErrBufferFull, если буфер заполняется без delim. Поскольку данные, возвращаемые из ReadSlice, будут перезаписаны следующей операцией ввода-вывода, большинство клиентов должны использовать Reader.ReadBytes или ReadString вместо этого. ReadSlice возвращает err != nil, если и только если строка не заканчивается на delim.
func (*Reader) ReadString
func (b *Reader) ReadString(delim byte) (string, error)
ReadString читает входные данные до первого появления delim, возвращая строку, содержащую данные до разделителя включительно. Если ReadString встречает ошибку до нахождения разделителя, он возвращает данные, прочитанные до ошибки, и саму ошибку (часто io.EOF). ReadString возвращает err != nil тогда и только тогда, когда возвращаемые данные не заканчиваются на delim. Для простых задач может быть более удобно использовать Scanner.
func (*Reader) Reset
func (b *Reader) Reset(r io.Reader)
Reset удаляет все данные из буфера, сбрасывает все состояния и переключает буферизованный считыватель на чтение из r. Вызов Reset с нулевым значением Reader инициализирует внутренний буфер до размера по умолчанию. Вызов b.Reset(b) (то есть сброс Reader на себя) ничего не делает.
func (*Reader) Size
func (b *Reader) Size() int
Size возвращает размер базового буфера в байтах.
func (*Reader) UnreadByte
func (b *Reader) UnreadByte() error
UnreadByte отменяет чтение последнего байта. Отменить чтение можно только последнего прочитанного байта.
UnreadByte возвращает ошибку, если последний вызов метода Reader не был операцией чтения. В частности, методы Reader.Peek, Reader.Discard и Reader.WriteTo не считаются операциями чтения.
func (*Reader) UnreadRune
func (b *Reader) UnreadRune() error
UnreadRune снимает отметку о прочтении с последней руны. Если последний вызов метода Reader не был Reader.ReadRune, Reader.UnreadRune возвращает ошибку. (В этом отношении он более строг, чем Reader.UnreadByte, который снимает отметку о прочтении с последнего байта любой операции чтения.)
func (*Reader) WriteTo
func (b *Reader) WriteTo(w io.Writer) (n int64, err error)
WriteTo реализует io.WriterTo. Это может привести к нескольким вызовам метода Reader.Read базового Reader. Если базовый Reader поддерживает метод Reader.WriteTo, то вызывается базовый Reader.WriteTo без буферизации.
type Scanner
type Scanner struct {
// contains filtered or unexported fields
}
Scanner предоставляет удобный интерфейс для чтения данных, таких как файл текста, разбитый на строки с разделителями новой строки. Последовательные вызовы метода Scanner.Scan будут проходить по «токену» файла, пропуская байты между токенами. Спецификация токена определяется функцией разбиения типа SplitFunc; функция разбиения по умолчанию разбивает входные данные на строки с удалением символов окончания строки. Функции Scanner.Split определены в этом пакете для сканирования файла на строки, байты, руны с кодировкой UTF-8 и слова, разделенные пробелами. Клиент может вместо этого предоставить собственную функцию разделения.
Сканирование останавливается без возможности восстановления при EOF, первой ошибке ввода-вывода или токене, который слишком велик, чтобы поместиться в Scanner.Buffer. Когда сканирование останавливается, читатель может продвинуться произвольно далеко за последний токен. Программы, которым требуется больший контроль над обработкой ошибок или большими токенами, или которые должны выполнять последовательное сканирование на читателе, должны использовать bufio.Reader.
Объяснение Scanner
Тип Scanner из пакета bufio в Go
Scanner предоставляет удобный интерфейс для чтения и разбора текстовых данных. Это один из самых полезных инструментов для обработки текста в Go.
Основные возможности
- Чтение данных по частям (строкам, словам, символам и т.д.)
- Гибкое разделение входных данных (можно использовать готовые или свои функции)
- Эффективная буферизация (работает быстро даже с большими файлами)
Создание Scanner
scanner := bufio.NewScanner(reader)
где reader - это любой объект, реализующий интерфейс io.Reader (файл, строка, сетевое соединение и т.д.)
Основные методы
1. Scan() bool
Продвигает сканер к следующему токену. Возвращает false, когда сканирование завершено.
for scanner.Scan() {
// Обработка текущего токена
}
2. Text() string
Возвращает текущий токен как строку.
scanner := bufio.NewScanner(strings.NewReader("Hello\nWorld"))
for scanner.Scan() {
fmt.Println(scanner.Text())
}
// Вывод:
// Hello
// World
3. Bytes() []byte
Возвращает текущий токен как срез байтов.
scanner := bufio.NewScanner(strings.NewReader("Hello"))
scanner.Scan()
fmt.Printf("%q", scanner.Bytes()) // Вывод: "Hello"
4. Err() error
Возвращает первую ошибку, возникшую при сканировании.
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
5. Split(splitFunc SplitFunc)
Устанавливает функцию разделения. По умолчанию используется ScanLines.
scanner := bufio.NewScanner(strings.NewReader("Hello World"))
scanner.Split(bufio.ScanWords) // Разделять по словам
Стандартные функции разделения
ScanLines (по умолчанию)
scanner := bufio.NewScanner(strings.NewReader("line1\nline2\nline3"))
for scanner.Scan() {
fmt.Println(scanner.Text())
}
ScanWords
scanner := bufio.NewScanner(strings.NewReader("word1 word2 word3"))
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
ScanRunes
scanner := bufio.NewScanner(strings.NewReader("Привет"))
scanner.Split(bufio.ScanRunes)
for scanner.Scan() {
fmt.Printf("%q ", scanner.Text())
}
// Вывод: 'П' 'р' 'и' 'в' 'е' 'т'
ScanBytes
scanner := bufio.NewScanner(strings.NewReader("Go"))
scanner.Split(bufio.ScanBytes)
for scanner.Scan() {
fmt.Printf("%q ", scanner.Bytes())
}
// Вывод: 'G' 'o'
Пользовательская функция разделения
Вы можете создать свою функцию разделения:
func atComma(data []byte, atEOF bool) (advance int, token []byte, err error) {
for i := 0; i < len(data); i++ {
if data[i] == ',' {
return i + 1, data[:i], nil
}
}
return 0, data, bufio.ErrFinalToken
}
func main() {
scanner := bufio.NewScanner(strings.NewReader("a,b,c,d"))
scanner.Split(atComma)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}
// Вывод:
// a
// b
// c
// d
Полезные советы
-
Ограничение размера буфера:
scanner := bufio.NewScanner(reader)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024) // Максимальный размер токена 1MB
-
Обработка больших строк:
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 100), bufio.MaxScanTokenSize)
-
Подсчет строк в файле:
file, _ := os.Open("data.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
lineCount++
}
Scanner идеально подходит для:
- Обработки логов
- Чтения конфигурационных файлов
- Анализа текстовых данных
- Разбора CSV и других текстовых форматов
Главное преимущество - это простота использования и эффективность при работе с большими объемами данных.
Пример пользовательский
Используйте сканер с настраиваемой функцией разделения (созданный с помощью ScanWords) для проверки 32-разрядного десятичного ввода.
package main
import (
"bufio"
"fmt"
"strconv"
"strings"
)
func main() {
// An artificial input source.
const input = "1234 5678 1234567901234567890"
scanner := bufio.NewScanner(strings.NewReader(input))
// Create a custom split function by wrapping the existing ScanWords function.
split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
advance, token, err = bufio.ScanWords(data, atEOF)
if err == nil && token != nil {
_, err = strconv.ParseInt(string(token), 10, 32)
}
return
}
// Set the split function for the scanning operation.
scanner.Split(split)
// Validate the input
for scanner.Scan() {
fmt.Printf("%s\n", scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Printf("Invalid input: %s", err)
}
}
Output:
1234
5678
Invalid input: strconv.ParseInt: parsing "1234567901234567890": value out of range
Пример EarlyStop
Используйте сканер с настраиваемой функцией разделения для разбора списка, разделенного запятыми, с пустым конечным значением, но останавливающимся на токене «STOP».
package main
import (
"bufio"
"bytes"
"fmt"
"os"
"strings"
)
func main() {
onComma := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
i := bytes.IndexByte(data, ',')
if i == -1 {
if !atEOF {
return 0, nil, nil
}
// If we have reached the end, return the last token.
return 0, data, bufio.ErrFinalToken
}
// If the token is "STOP", stop the scanning and ignore the rest.
if string(data[:i]) == "STOP" {
return i + 1, nil, bufio.ErrFinalToken
}
// Otherwise, return the token before the comma.
return i + 1, data[:i], nil
}
const input = "1,2,STOP,4,"
scanner := bufio.NewScanner(strings.NewReader(input))
scanner.Split(onComma)
for scanner.Scan() {
fmt.Printf("Got a token %q\n", scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading input:", err)
}
}
Output:
Got a token "1"
Got a token "2"
Пример EmptyFinalToken
Используйте сканер с настраиваемой функцией разделения для разбора списка, разделенного запятыми, с пустым конечным значением.
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
// Comma-separated list; last entry is empty.
const input = "1,2,3,4,"
scanner := bufio.NewScanner(strings.NewReader(input))
// Define a split function that separates on commas.
onComma := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
for i := 0; i < len(data); i++ {
if data[i] == ',' {
return i + 1, data[:i], nil
}
}
if !atEOF {
return 0, nil, nil
}
// There is one final token to be delivered, which may be the empty string.
// Returning bufio.ErrFinalToken here tells Scan there are no more tokens after this
// but does not trigger an error to be returned from Scan itself.
return 0, data, bufio.ErrFinalToken
}
scanner.Split(onComma)
// Scan.
for scanner.Scan() {
fmt.Printf("%q ", scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading input:", err)
}
}
Output:
"1" "2" "3" "4" ""
Пример Lines
Самый простой способ использования сканера — чтение стандартного ввода в виде набора строк.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fmt.Println(scanner.Text()) // Println will add back the final '\n'
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading standard input:", err)
}
}
Пример Words
Используйте сканер для реализации простой утилиты подсчета слов, сканируя входные данные как последовательность токенов, разделенных пробелами.
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
// An artificial input source.
const input = "Now is the winter of our discontent,\nMade glorious summer by this sun of York.\n"
scanner := bufio.NewScanner(strings.NewReader(input))
// Set the split function for the scanning operation.
scanner.Split(bufio.ScanWords)
// Count the words.
count := 0
for scanner.Scan() {
count++
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading input:", err)
}
fmt.Printf("%d\n", count)
}
func NewScanner
func NewScanner(r io.Reader) *Scanner
NewScanner возвращает новый сканер для чтения из r. По умолчанию функция разделения — ScanLines.
func (*Scanner) Buffer
func (s *Scanner) Buffer(buf []byte, max int)
Buffer устанавливает начальный буфер, который будет использоваться при сканировании, и максимальный размер буфера, который может быть выделен во время сканирования. Максимальный размер токена должен быть меньше большего из max и cap(buf). Если max <= cap(buf), Scanner.Scan будет использовать только этот буфер и не будет выделять дополнительную память.
По умолчанию Scanner.Scan использует внутренний буфер и устанавливает максимальный размер токена равным MaxScanTokenSize.
Buffer вызывает панику, если его вызвать после начала сканирования.
func (*Scanner) Bytes
func (s *Scanner) Bytes() []byte
Bytes возвращает самый последний токен, сгенерированный вызовом Scanner.Scan. Базовый массив может указывать на данные, которые будут перезаписаны последующим вызовом Scan. Он не выполняет выделение памяти.
Пример
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
scanner := bufio.NewScanner(strings.NewReader("gopher"))
for scanner.Scan() {
fmt.Println(len(scanner.Bytes()) == 6)
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "shouldn't see an error scanning a string")
}
}
func (*Scanner) Err
func (s *Scanner) Err() error
Err возвращает первую ошибку, не являющуюся EOF, которая была обнаружена Scanner.
func (*Scanner) Scan
func (s *Scanner) Scan() bool
Scan перемещает Scanner к следующему токену, который затем будет доступен через метод Scanner.Bytes или Scanner.Text. Он возвращает false, когда больше нет токенов, либо по достижении конца ввода, либо из-за ошибки. После того, как Scan возвращает false, метод Scanner.Err вернет любую ошибку, которая произошла во время сканирования, за исключением того, что если это было io.EOF, Scanner.Err вернет nil. Scan вызывает панику, если функция split возвращает слишком много пустых токенов, не продвигая ввод. Это обычный режим ошибки для сканеров.
func (*Scanner) Split
func (s *Scanner) Split(split SplitFunc)
Split устанавливает функцию разделения для сканера. Функция разделения по умолчанию — ScanLines.
Split вызывает панику, если его вызывают после начала сканирования.
func (*Scanner) Text
добавлено в go1.1
func (s *Scanner) Text() string
Text возвращает последний токен, сгенерированный вызовом Scanner.Scan, в виде вновь выделенной строки, содержащей его байты.
type SplitFunc
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
SplitFunc — это сигнатура функции разделения, используемой для токенизации входных данных. Аргументами являются начальная подстрока оставшихся необработанных данных и флаг atEOF, который сообщает, есть ли у Reader еще данные для передачи. Возвращаемые значения — это количество байтов для продвижения входных данных и следующий токен для возврата пользователю, если таковой имеется, плюс ошибка, если таковая имеется.
Сканирование останавливается, если функция возвращает ошибку, и в этом случае часть ввода может быть отброшена. Если эта ошибка — ErrFinalToken, сканирование останавливается без ошибки. Токен, отличный от nil, доставленный с ErrFinalToken, будет последним токеном, а токен nil с ErrFinalToken немедленно останавливает сканирование.
В противном случае сканер продвигает ввод. Если токен не равен nil, сканер возвращает его пользователю. Если токен равен nil, сканер считывает больше данных и продолжает сканирование; если данных больше нет (если atEOF было true), сканер возвращается. Если данные еще не содержат полный токен, например, если при сканировании строк в них нет символа новой строки, SplitFunc может вернуть (0, nil, nil), чтобы сигнализировать сканеру о необходимости считывать больше данных в срез и повторить попытку с более длинным срезом, начиная с той же точки во входных данных.
Функция никогда не вызывается с пустым фрагментом данных, если atEOF не равно true. Однако, если atEOF равно true, данные могут быть непустыми и, как всегда, содержать необработанный текст.
Объяснение SplitFunc
Тип SplitFunc в пакете bufio
SplitFunc - это специальный тип функции, который определяет как сканер (Scanner) должен разделять входные данные на токены. Это ключевой механизм, обеспечивающий гибкость работы с текстовыми данными.
Для чего нужен SplitFunc?
-
Определение логики разделения данных:
- Где заканчивается один токен и начинается следующий
- Как обрабатывать неполные данные
- Как реагировать на ошибки
-
Поддержка различных форматов данных:
- Стандартные (строки, слова, символы)
- Пользовательские (CSV, логи, специфичные форматы)
-
Обработка сложных случаев:
- Данные, поступающие частями
- Нестандартные разделители
- Составные токены
Как работает SplitFunc
Функция принимает:
data []byte - буфер с данными для анализа
atEOF bool - флаг, указывающий, что это конец данных
Возвращает:
advance int - на сколько байт продвинуться в буфере
token []byte - выделенный токен (может быть nil)
err error - ошибка, если возникла
Примеры использования
1. Стандартные функции разделения
// Чтение по строкам (по умолчанию)
scanner.Split(bufio.ScanLines)
// Чтение по словам
scanner.Split(bufio.ScanWords)
// Чтение по символам (рунам)
scanner.Split(bufio.ScanRunes)
// Чтение по байтам
scanner.Split(bufio.ScanBytes)
2. Пользовательская функция разделения
Пример разделения по запятым:
func splitByComma(data []byte, atEOF bool) (advance int, token []byte, err error) {
// Ищем запятую в данных
for i := 0; i < len(data); i++ {
if data[i] == ',' {
// Возвращаем позицию после запятой и токен
return i + 1, data[:i], nil
}
}
// Если достигнут конец данных и есть что возвращать
if atEOF && len(data) > 0 {
return len(data), data, nil
}
// Запрашиваем больше данных
return 0, nil, nil
}
// Использование
scanner.Split(splitByComma)
3. Сложный пример - чтение CSV
func csvSplit(data []byte, atEOF bool) (advance int, token []byte, err error) {
if len(data) == 0 {
return 0, nil, nil
}
// Пропускаем кавычки, если есть
inQuotes := false
if data[0] == '"' {
inQuotes = true
data = data[1:]
}
for i := 0; i < len(data); i++ {
switch {
case inQuotes && data[i] == '"' && i+1 < len(data) && data[i+1] == '"':
i++ // пропускаем двойные кавычки
case inQuotes && data[i] == '"':
// Закрывающая кавычка
return i + 2, data[:i], nil
case !inQuotes && data[i] == ',':
// Разделитель полей
return i + 1, data[:i], nil
case !inQuotes && (data[i] == '\r' || data[i] == '\n'):
// Конец строки
return i + 1, data[:i], nil
}
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
Когда создавать свой SplitFunc?
-
Нестандартные форматы данных:
- Специфичные логи
- Двоичные протоколы
- Сложные разделители
-
Оптимизация обработки:
- Когда стандартные функции неэффективны
- Для обработки очень больших файлов
-
Специальная логика:
- Пропуск комментариев
- Обработка escape-последовательностей
- Валидация данных на лету
Особенности работы
-
Инкрементная обработка:
- Функция может вызываться много раз для одного токена
- Нужно корректно обрабатывать частичные данные
-
Управление буфером:
- Можно ограничивать максимальный размер токена
- Важно правильно указывать позицию продвижения
-
Обработка ошибок:
- Можно возвращать специальные ошибки
bufio.ErrFinalToken - маркер последнего токена
SplitFunc - это мощный механизм, который делает Scanner чрезвычайно гибким инструментом для обработки текстовых данных в Go.
type Writer
type Writer struct {
// contains filtered or unexported fields
}
Writer реализует буферизацию для объекта io.Writer. Если при записи в Writer происходит ошибка, дальнейшая запись данных не будет приниматься, а все последующие операции записи и Writer.Flush будут возвращать ошибку. После записи всех данных клиент должен вызвать метод Writer.Flush, чтобы гарантировать, что все данные были переданы в базовый io.Writer.
Пример
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
w := bufio.NewWriter(os.Stdout)
fmt.Fprint(w, "Hello, ")
fmt.Fprint(w, "world!")
w.Flush() // Don't forget to flush!
}
func NewWriter
func NewWriter(w io.Writer) *Writer
NewWriter возвращает новый Writer, буфер которого имеет размер по умолчанию. Если аргумент io.Writer уже является Writer с достаточно большим размером буфера, он возвращает базовый Writer.
func NewWriterSize
func NewWriterSize(w io.Writer, size int) *Writer
NewWriterSize возвращает новый Writer, буфер которого имеет как минимум указанный размер. Если аргумент io.Writer уже является Writer с достаточно большим размером, он возвращает базовый Writer.
func (*Writer) Available
func (b *Writer) Available() int
Available возвращает количество неиспользуемых байтов в буфере.
func (*Writer) AvailableBuffer
func (b *Writer) AvailableBuffer() []byte
AvailableBuffer возвращает пустой буфер с емкостью b.Available(). Этот буфер предназначен для добавления и передачи в следующий вызов Writer.Write. Буфер действителен только до следующей операции записи в b.
Пример
package main
import (
"bufio"
"os"
"strconv"
)
func main() {
w := bufio.NewWriter(os.Stdout)
for _, i := range []int64{1, 2, 3, 4} {
b := w.AvailableBuffer()
b = strconv.AppendInt(b, i, 10)
b = append(b, ' ')
w.Write(b)
}
w.Flush()
}
func (*Writer) Buffered
func (b *Writer) Buffered() int
Buffered возвращает количество байтов, которые были записаны в текущий буфер.
func (*Writer) Flush
func (b *Writer) Flush() error
Flush записывает все буферизованные данные в базовый io.Writer.
func (*Writer) ReadFrom
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error)
ReadFrom реализует io.ReaderFrom. Если базовый writer поддерживает метод ReadFrom, то вызывается базовый ReadFrom. Если есть буферизованные данные и базовый ReadFrom, то буфер заполняется и записывается перед вызовом ReadFrom.
Пример
package main
import (
"bufio"
"bytes"
"fmt"
"strings"
)
func main() {
var buf bytes.Buffer
writer := bufio.NewWriter(&buf)
data := "Hello, world!\nThis is a ReadFrom example."
reader := strings.NewReader(data)
n, err := writer.ReadFrom(reader)
if err != nil {
fmt.Println("ReadFrom Error:", err)
return
}
if err = writer.Flush(); err != nil {
fmt.Println("Flush Error:", err)
return
}
fmt.Println("Bytes written:", n)
fmt.Println("Buffer contents:", buf.String())
}
Output:
Bytes written: 41
Buffer contents: Hello, world!
This is a ReadFrom example.
func (*Writer) Reset
func (b *Writer) Reset(w io.Writer)
Reset удаляет все невыгруженные данные из буфера, очищает все ошибки и сбрасывает b, чтобы записать его вывод в w. Вызов Reset с нулевым значением Writer инициализирует внутренний буфер до размера по умолчанию. Вызов w.Reset(w) (то есть сброс Writer к самому себе) ничего не делает.
func (*Writer) Size
added in go1.10
func (b *Writer) Size() int
Size возвращает размер базового буфера в байтах.
func (*Writer) Write
func (b *Writer) Write(p []byte) (nn int, err error)
Write записывает содержимое p в буфер. Он возвращает количество записанных байтов. Если nn < len(p), он также возвращает ошибку, объясняющую, почему запись не завершена.
func (*Writer) WriteByte
func (b *Writer) WriteByte(c byte) error
WriteByte записывает один байт.
func (*Writer) WriteRune
func (b *Writer) WriteRune(r rune) (size int, err error)
WriteRune записывает одну кодовую точку Unicode, возвращая количество записанных байтов и любую ошибку.
func (*Writer) WriteString
func (b *Writer) WriteString(s string) (int, error)
WriteString записывает строку. Он возвращает количество записанных байтов. Если количество меньше len(s), он также возвращает ошибку с объяснением, почему запись не завершена.
4.3 - Пакет builtin встроенных функций Go
Пакет builtin содержит документацию по заранее объявленным идентификаторам языка Go.
Элементы, задокументированные здесь, на самом деле не входят в пакет builtin, но их описание здесь позволяет godoc представить документацию для специальных идентификаторов языка.
Константы
const (
true = 0 == 0 // Untyped bool.
false = 0 != 0 // Untyped bool.
)
true и false - это два нетипизированных булевых значения.
const iota = 0 // Untyped int.
iota - это заранее объявленный идентификатор, представляющий нетипизированный целочисленный порядковый номер текущей спецификации const в объявлении const (обычно в круглых скобках). Он имеет нулевую индексацию.
Переменные ¶
var nil Type // Тип должен быть указателем, каналом, func, интерфейсом, map или slice-типом
nil - это заранее объявленный идентификатор, представляющий нулевое значение для указателя, канала, func, интерфейса, map или slice-типа.
4.3.1 - Функции builtin
Описание функций builtin
func append
func append(slice []Type, elems ...Type) []Type
Встроенная функция append добавляет элементы в конец слайса. Если он имеет достаточную емкость, целевой слайс переразбивается, чтобы вместить новые элементы. Если нет, выделяется новый базовый массив. Append возвращает обновленный слайс. Поэтому необходимо сохранить результат append, часто в переменной, содержащей сам слайс:
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
В качестве особого случая допустимо добавлять строку к байтовому слайсу, например:
slice = append([]byte(«hello »), «world»...)
func cap
Встроенная функция cap возвращает емкость v в зависимости от его типа:
- Массив: количество элементов в v (то же, что len(v)).
- Указатель на массив: количество элементов в *v (то же, что len(v)).
- Срез: максимальная длина, которую может достичь срез при повторном разрезании; если v равно nil, cap(v) равно нулю.
- Канал: емкость буфера канала в единицах элементов; если v равно nil, cap(v) равно нулю.
Для некоторых аргументов, таких как простое массивное выражение, результатом может быть константа. Подробности см. в разделе «Длина и емкость» спецификации языка Go.
func clear
func clear[T ~[]Type | ~map[Type]Type1](t T)
Встроенная функция clear очищает карты и фрагменты.
- Для карт clear удаляет все записи, в результате чего карта становится пустой.
- Для слайсов clear устанавливает все элементы до длины слайса в нулевое значение соответствующего типа элемента.
Если тип аргумента является типовым параметром, набор типов типового параметра должен содержать только типы карт или слайсов, и clear выполняет операцию, подразумеваемую типовым аргументом. Если t равно nil, clear не выполняет никаких действий.
func close
func close(c chan<- Type)
Встроенная функция close закрывает канал, который должен быть либо двунаправленным, либо только для отправки. Она должна выполняться только отправителем, никогда не получателем, и приводит к закрытию канала после получения последнего отправленного значения. После получения последнего значения из закрытого канала c любое получение из c будет выполняться без блокировки, возвращая нулевое значение для элемента канала. Форма
также установит ok в false для закрытого и пустого канала.
func complex
func complex(r, i FloatType) ComplexType
Встроенная функция complex создает комплексное значение из двух значений с плавающей запятой. Действительная и мнимая части должны быть одинакового размера, либо float32, либо float64 (или приравниваемы к ним), и возвращаемое значение будет соответствующим комплексным типом (complex64 для float32, complex128 для float64).
func copy
func copy(dst, src []Type) int
Встроенная функция copy копирует элементы из исходного слайса в целевой слайс. (В качестве особого случая она также копирует байты из строки в слайс байтов.) Исходный и целевой слайсы могут пересекаться. Copy возвращает количество скопированных элементов, которое будет минимальным из len(src) и len(dst).
func delete
func delete(m map[Type]Type1, key Type)
Встроенная функция delete удаляет элемент с указанным ключом (m[key]) из карты. Если m равно nil или такого элемента нет, delete не выполняет никаких действий.
func imag
func imag(c ComplexType) FloatType
Встроенная функция imag возвращает мнимую часть комплексного числа c. Возвращаемое значение будет иметь тип с плавающей запятой, соответствующий типу c.
func len
Встроенная функция len возвращает длину v в соответствии с его типом:
- Массив: количество элементов в v.
- Указатель на массив: количество элементов в *v (даже если v равен nil).
- Срез или карта: количество элементов в v; если v равен nil, len(v) равен нулю.
- Строка: количество байтов в v.
- Канал: количество элементов в очереди (непрочитанных) в буфере канала; если v равно nil, len(v) равно нулю.
Для некоторых аргументов, таких как строковый литерал или простое массивное выражение, результатом может быть константа. Подробности см. в разделе «Длина и емкость» спецификации языка Go.
func make
func make(t Type, size ...IntegerType) Type
Встроенная функция make выделяет и инициализирует объект типа slice, map или chan (только). Как и в случае с new, первый аргумент является типом, а не значением. В отличие от new, тип возвращаемого значения make совпадает с типом его аргумента, а не является указателем на него. Спецификация результата зависит от типа:
- Срез: размер определяет длину. Емкость среза равна его длине. Можно указать второй целочисленный аргумент, чтобы задать другую емкость; она не должна быть меньше длины. Например, make([]int, 0, 10) выделяет базовый массив размером 10 и возвращает срез длиной 0 и емкостью 10, который поддерживается этим базовым массивом.
- Карта: выделяется пустая карта с достаточным пространством для хранения указанного количества элементов. Размер может быть опущен, в этом случае выделяется небольшой начальный размер.
- Канал: буфер канала инициализируется с указанной емкостью буфера. Если размер равен нулю или опущен, канал не имеет буфера.
func max
func max[T cmp.Ordered](x T, y ...T) T
Встроенная функция max возвращает наибольшее значение из фиксированного числа аргументов типов cmp.Ordered. Должно быть как минимум один аргумент. Если T является типом с плавающей запятой и любой из аргументов является NaN, max вернет NaN.
func min
func min[T cmp.Ordered](x T, y ...T) T
Встроенная функция min возвращает наименьшее значение из фиксированного числа аргументов типов cmp.Ordered. Должно быть как минимум один аргумент. Если T является типом с плавающей запятой и любой из аргументов является NaN, min вернет NaN.
func new
Встроенная функция new выделяет память. Первый аргумент — это тип, а не значение, и возвращаемое значение — это указатель на вновь выделенное нулевое значение этого типа.
func panic
Встроенная функция panic останавливает нормальное выполнение текущего goroutine.
Когда функция F вызывает panic, нормальное выполнение F немедленно останавливается. Все функции, выполнение которых было отложено F, выполняются обычным образом, а затем F возвращается к своему вызывающему. Для вызывающего G вызов F ведет себя как вызов panic, прекращая выполнение G и запуская все отложенные функции. Это продолжается до тех пор, пока все функции в выполняющемся goroutine не остановятся в обратном порядке. В этот момент программа завершается с ненулевым кодом выхода. Эта последовательность завершения называется паникой и может контролироваться встроенной функцией recover.
Начиная с Go 1.21, вызов panic с нулевым значением интерфейса или нетипизированным nil вызывает ошибку выполнения (другой вид паники). Настройка GODEBUG panicnil=1 отключает ошибку выполнения.
func print
Встроенная функция print форматирует свои аргументы способом, специфичным для реализации, и записывает результат в стандартный поток ошибок. Print полезна для начальной загрузки и отладки; ее сохранение в языке не гарантируется.
func println
func println(args ...Type)
Встроенная функция println форматирует свои аргументы способом, специфичным для реализации, и записывает результат в стандартный поток ошибок. Между аргументами всегда добавляются пробелы и добавляется новая строка. Println полезна для начальной настройки и отладки; ее сохранение в языке не гарантируется.
func real
func real(c ComplexType) FloatType
Встроенная функция real возвращает действительную часть комплексного числа c. Возвращаемое значение будет иметь тип с плавающей запятой, соответствующий типу c.
func recover
Встроенная функция recover позволяет программе управлять поведением goroutine, входящего в состояние паники.
Вызов recover внутри отложенной функции (но не в любой функции, вызываемой ею) останавливает последовательность паники, восстанавливая нормальное выполнение и извлекая значение ошибки, переданное вызову panic.
Если recover вызывается вне отложенной функции, он не останавливает последовательность паники. В этом случае, или когда goroutine не входит в состояние паники, recover возвращает nil.
До Go 1.21 recover также возвращала nil, если panic вызывалась с аргументом nil. Подробности см. в разделе [panic].
Пример
Функция recover() используется для перехвата паники (panic) и восстановления нормального выполнения программы. Вот несколько практических примеров:
Пример 1: Базовое использование recover
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
fmt.Println("Start")
panic("something went wrong")
fmt.Println("This won't be executed")
}
Вывод:
Start
Recovered from panic: something went wrong
Пример 2: Восстановление после деления на ноль
package main
import "fmt"
func safeDivide(a, b int) (result int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
result = 0 // устанавливаем значение по умолчанию
}
}()
return a / b // может вызвать панику при b = 0
}
func main() {
fmt.Println(safeDivide(10, 2)) // 5
fmt.Println(safeDivide(10, 0)) // 0 (после восстановления)
fmt.Println("Program continues")
}
Пример 3: Разные типы паники
package main
import "fmt"
func handlePanic() {
if r := recover(); r != nil {
switch v := r.(type) {
case string:
fmt.Println("String panic:", v)
case error:
fmt.Println("Error panic:", v)
default:
fmt.Printf("Unknown panic: %v (%T)\n", v, v)
}
}
}
func main() {
defer handlePanic()
// Можно раскомментировать любой вариант:
panic("string panic")
// panic(fmt.Errorf("error panic"))
// panic(42)
}
Пример 4: recover в горутинах
package main
import (
"fmt"
"time"
)
func worker(id int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Worker %d recovered: %v\n", id, r)
}
}()
if id == 2 {
panic(fmt.Sprintf("panic in worker %d", id))
}
fmt.Printf("Worker %d working\n", id)
}
func main() {
for i := 1; i <= 3; i++ {
go worker(i)
}
time.Sleep(time.Second) // Даем время горутинам завершиться
}
Пример 5: Вложенные recover
package main
import "fmt"
func inner() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Inner recovered:", r)
}
}()
panic("inner panic")
}
func outer() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Outer recovered:", r)
}
}()
inner()
fmt.Println("This won't be executed")
}
func main() {
outer()
fmt.Println("Program continues")
}
Важные замечания:
recover() работает только внутри defer-функций
- Каждый вызов
recover() обрабатывает только одну панику
- После восстановления выполнение продолжается после блока defer
- В Go 1.21+
recover() возвращает nil только если паники не было
- Не злоупотребляйте recover - используйте только для действительно неожиданных ошибок
4.3.2 - Типы в пакете builtin
Описание типов из пакета builtin
type ComplexType
type ComplexType complex64
ComplexType используется здесь исключительно в целях документирования. Он заменяет любой из типов complex: complex64 или complex128.
type FloatType
FloatType используется здесь исключительно в целях документирования. Он заменяет любой из типов float: float32 или float64.
type IntegerType
IntegerType используется здесь только в целях документирования. Он заменяет любой тип целого числа: int, uint, int8 и т. д.
type Type
Type используется здесь только в целях документирования. Он заменяет любой тип Go, но представляет один и тот же тип для любого вызова функции.
type Type1
Type1 используется здесь только в целях документирования. Он является заменителем для любого типа Go, но представляет один и тот же тип для любого вызова функции.
type any
any является псевдонимом для interface{} и во всех отношениях эквивалентен interface{}.
type bool
bool — это набор булевых значений true и false.
type byte
byte — это псевдоним для uint8 и во всех отношениях эквивалентен uint8. По соглашению он используется для отличия значений byte от 8-битных целых чисел без знака.
type comparable
type comparable interface{ comparable }
comparable — это интерфейс, который реализуется всеми сопоставимыми типами (булевыми значениями, числами, строками, указателями, каналами, массивами сопоставимых типов, структурами, все поля которых являются сопоставимыми типами). Интерфейс comparable может использоваться только в качестве ограничения параметра типа, а не в качестве типа переменной.
Подробнее
Интерфейс comparable - это специальный встроенный интерфейс в Go, который представляет все типы, поддерживающие операции сравнения == и !=. Он используется исключительно как ограничение (constraint) для параметров типа в дженериках.
Особенности comparable:
- Неявная реализация - все сопоставимые типы автоматически удовлетворяют этому интерфейсу
- Ограниченное использование - может применяться только как ограничение типа в дженериках
- Строгая типизация - гарантирует, что типы можно сравнивать между собой
Пример 1: Простая функция сравнения
package main
import "fmt"
// Equal проверяет равенство двух значений comparable типа
func Equal[T comparable](a, b T) bool {
return a == b
}
func main() {
fmt.Println(Equal(1, 1)) // true
fmt.Println(Equal("a", "b")) // false
fmt.Println(Equal(true, false)) // false
}
Пример 2: Проверка наличия элемента в слайсе
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
func main() {
nums := []int{1, 2, 3}
fmt.Println(Contains(nums, 2)) // true
fmt.Println(Contains(nums, 5)) // false
strs := []string{"a", "b", "c"}
fmt.Println(Contains(strs, "b")) // true
}
Пример 3: Уникальные элементы в слайсе
func Unique[T comparable](slice []T) []T {
seen := make(map[T]bool)
result := []T{}
for _, v := range slice {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
func main() {
dupes := []int{1, 2, 2, 3, 4, 4, 5}
fmt.Println(Unique(dupes)) // [1 2 3 4 5]
}
Какие типы являются comparable:
- Базовые типы:
bool, int, float64, string и т.д.
- Указатели (
*T)
- Каналы (
chan T)
- Массивы, если их элементы comparable (
[3]int, [2]string)
- Структуры, если все их поля comparable
Какие типы НЕ являются comparable:
- Слайсы (
[]T)
- Мапы (
map[K]V)
- Функции
- Структуры, содержащие несопоставимые поля
Пример 4: Ошибка при использовании несопоставимого типа
func main() {
// Ошибка: slice does not satisfy comparable
fmt.Println(Equal([]int{1}, []int{1}))
// Ошибка: map does not satisfy comparable
fmt.Println(Equal(map[int]int{}, map[int]int{}))
}
Практическое применение:
- Создание универсальных контейнеров
- Реализация алгоритмов для разных типов
- Написание тестовых утилит
- Создание библиотечных функций
type complex128
type complex128 complex128
complex128 — это набор всех комплексных чисел с вещественными и мнимыми частями float64.
type complex64
complex64 — это набор всех комплексных чисел с вещественными и мнимыми частями типа float32.
type error
type error интерфейс {
Error() string
}
Встроенный интерфейсный тип error — это стандартный интерфейс для представления условия ошибки, причем значение nil означает отсутствие ошибки.
type float32
float32 — это набор всех 32-разрядных чисел с плавающей запятой IEEE 754.
type float64
float64 — это набор всех 64-разрядных чисел с плавающей запятой IEEE 754.
type int
int — это тип целых чисел с знаком, размер которого составляет не менее 32 бит. Однако это отдельный тип, а не псевдоним, например, int32.
type int16
int16 — это набор всех 16-разрядных целых чисел со знаком. Диапазон: от -32768 до 32767.
type int32
int32 — это набор всех 32-разрядных целых чисел со знаком. Диапазон: от -2147483648 до 2147483647.
type int64
int64 — это набор всех 64-разрядных целых чисел со знаком. Диапазон: от -9223372036854775808 до 9223372036854775807.
type int8
int8 — это набор всех 8-битных целых чисел со знаком. Диапазон: от -128 до 127.
type rune
rune — это псевдоним для int32 и во всех отношениях эквивалентен int32. По соглашению он используется для отличия значений символов от целочисленных значений.
type string
string — это набор всех строк из 8-битных байтов, которые по соглашению, но не обязательно, представляют текст, закодированный в UTF-8. Строка может быть пустой, но не может быть nil. Значения типа string являются неизменяемыми.
type uint
uint — это тип целого числа без знака, размер которого составляет не менее 32 бит. Однако это отдельный тип, а не псевдоним, например, для uint32.
type uint16
uint16 — это набор всех 16-битных целых чисел без знака. Диапазон: от 0 до 65535.
type uint32
uint32 — это набор всех 32-битных целых чисел без знака. Диапазон: от 0 до 4294967295.
type uint64
uint64 — это набор всех 64-разрядных целых чисел без знака. Диапазон: от 0 до 18446744073709551615.
type uint8
uint8 — это набор всех 8-разрядных целых чисел без знака. Диапазон: от 0 до 255.
type uintptr
uintptr — это целочисленный тип, который достаточно велик, чтобы содержать битовую матрицу любого указателя.
4.4 - Описание пакета context языка программирования Go
Пакет Context определяет тип Context, который передает сроки, сигналы отмены и другие значения в рамках запроса через границы API и между процессами.
Входящие запросы к серверу должны создавать Context, а исходящие вызовы к серверам должны принимать Context. Цепочка вызовов функций между ними должна передавать Context, при необходимости заменяя его производным Context, созданным с помощью WithCancel, WithDeadline, WithTimeout или WithValue.
Контекст может быть отменен, чтобы указать, что работа, выполняемая от его имени, должна быть остановлена. Контекст с крайним сроком отменяется после истечения крайнего срока. Когда контекст отменяется, все производные от него контексты также отменяются.
Функции WithCancel, WithDeadline и WithTimeout принимают контекст (родительский) и возвращают производный контекст (дочерний) и CancelFunc. Прямой вызов CancelFunc отменяет дочерний контекст и его дочерние контексты, удаляет ссылку родительского контекста на дочерний и останавливает все связанные таймеры. Невыполнение вызова CancelFunc приводит к утечке дочернего контекста и его дочерних контекстов до тех пор, пока не будет отменен родительский контекст. Инструмент go vet проверяет, что CancelFuncs используются на всех путях управления потоком.
Функции WithCancelCause, WithDeadlineCause и WithTimeoutCause возвращают CancelCauseFunc, который принимает ошибку и записывает ее как причину отмены. Вызов Cause на отмененном контексте или любом из его дочерних элементов извлекает причину. Если причина не указана, Cause(ctx) возвращает то же значение, что и ctx.Err().
Программы, использующие контексты, должны следовать этим правилам, чтобы обеспечить согласованность интерфейсов между пакетами и позволить инструментам статического анализа проверять распространение контекста:
Не храните контексты внутри типа struct; вместо этого явно передавайте контекст каждой функции, которой он нужен. Это подробнее обсуждается в разделе «https://go.dev/blog/context-and-structs». Контекст должен быть первым параметром, обычно называемым ctx:
func DoSomething(ctx context.Context, arg Arg) error {
// ... использовать ctx ...
}
Не передавайте nil Context, даже если функция это допускает. Передайте context.TODO, если не уверены, какой Context использовать.
Используйте значения контекста только для данных в рамках запроса, которые проходят через процессы и API, а не для передачи опциональных параметров функциям.
Один и тот же Context может передаваться функциям, выполняющимся в разных goroutines; Context безопасен для одновременного использования несколькими goroutines.
См. https://go.dev/blog/context для примера кода сервера, использующего Context.
Переменные пакета
var Canceled = errors.New(«context canceled»)
Canceled — это ошибка, возвращаемая [Context.Err], когда контекст отменяется по какой-либо причине, кроме истечения срока.
var DeadlineExceeded error = deadlineExceededError{}
DeadlineExceeded — это ошибка, возвращаемая [Context.Err], когда контекст отменяется из-за истечения срока.
4.4.1 - Описание функций пакета Context
Описание функций пакета Context: AfterFunc, Cause, WithCancel, WithCancelCause, WithDeadline, WithDeadlineCause, WithTimeout, WithTimeoutCause
func AfterFunc
func AfterFunc(ctx Context, f func()) (stop func() bool)
AfterFunc организует вызов f в собственной горутине после отмены ctx. Если ctx уже отменен, AfterFunc вызывает f сразу в своей собственной горутине.
Несколько вызовов AfterFunc на одном контексте работают независимо; один не заменяет другой.
Вызов возвращаемой функции stop прекращает ассоциацию ctx с f. Он возвращает true, если вызов остановил запуск f. Если stop возвращает false, то либо контекст отменен и f была запущена в своей собственной goroutine, либо f уже была остановлена. Функция stop не ждет завершения f перед возвратом. Если вызывающей стороне необходимо знать, завершено ли выполнение f, она должна явно согласовать это с f.
Если у ctx есть метод “AfterFunc(func()) func() bool”, AfterFunc будет использовать его для планирования вызова.
Пример (Cond)
В этом примере AfterFunc используется для определения функции, которая ожидает синхронизацию с Cond, прекращая ожидание при отмене контекста.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
waitOnCond := func(ctx context.Context, cond *sync.Cond, conditionMet func() bool) error {
// Создаем канал для отмены AfterFunc
done := make(chan struct{})
defer close(done)
// Настраиваем функцию отмены по таймауту контекста
stopf := context.AfterFunc(ctx, func() {
cond.L.Lock()
defer cond.L.Unlock()
select {
case <-done:
// Уже завершились, не нужно broadcast
return
default:
// Пробуждаем все ожидающие горутины
cond.Broadcast()
}
})
defer stopf()
// Ожидаем выполнения условия
for !conditionMet() {
// Проверяем контекст перед ожиданием
if ctx.Err() != nil {
return ctx.Err()
}
cond.Wait()
// Проверяем контекст после пробуждения
if ctx.Err() != nil {
return ctx.Err()
}
}
return nil
}
cond := sync.NewCond(&sync.Mutex{})
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 4; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
cond.L.Lock()
defer cond.L.Unlock()
err := waitOnCond(ctx, cond, func() bool { return false })
fmt.Printf("Goroutine %d finished after %v with error: %v\n",
id, time.Since(start), err)
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
}
Пример Connection
В этом примере используется AfterFunc для определения функции, которая считывает данные из net.Conn, останавливая считывание при отмене контекста.
package main
import (
"context"
"fmt"
"net"
"time"
)
func readFromConn(ctx context.Context, conn net.Conn, b []byte) (n int, err error) {
stopc := make(chan struct{})
defer close(stopc) // Гарантируем закрытие канала
// Устанавливаем функцию отмены чтения при отмене контекста
stop := context.AfterFunc(ctx, func() {
conn.SetReadDeadline(time.Now()) // Прерываем текущее чтение
close(stopc)
})
defer func() {
if !stop() {
// Если AfterFunc был запущен, сбрасываем дедлайн
<-stopc
conn.SetReadDeadline(time.Time{})
}
}()
n, err = conn.Read(b)
if ctx.Err() != nil {
return 0, ctx.Err() // Возвращаем ошибку контекста если он отменен
}
return n, err
}
func main() {
// Создаем тестовый сервер
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
fmt.Println("Failed to create listener:", err)
return
}
defer listener.Close()
// Устанавливаем соединение
conn, err := net.Dial(listener.Addr().Network(), listener.Addr().String())
if err != nil {
fmt.Println("Failed to dial:", err)
return
}
defer conn.Close()
// Создаем контекст с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
// Читаем данные
b := make([]byte, 1024)
_, err = readFromConn(ctx, conn, b)
// Проверяем тип ошибки
switch {
case err == nil:
fmt.Println("Read completed successfully")
case ctx.Err() != nil:
fmt.Println("Operation canceled:", ctx.Err())
default:
fmt.Println("Read error:", err)
}
}
Дополнительные рекомендации:
-
Таймауты соединения:
conn.SetDeadline(time.Now().Add(30 * time.Second))
-
Буферизация:
bufReader := bufio.NewReader(conn)
n, err = bufReader.Read(b)
-
Повторные попытки:
for retries := 0; retries < 3; retries++ {
n, err = readFromConn(ctx, conn, b)
if err == nil || !isTemporary(err) {
break
}
}
-
Логирование:
log.Printf("Read %d bytes, error: %v", n, err)
Пример Merge
В этом примере AfterFunc используется для определения функции, которая объединяет сигналы отмены двух Контекстов.
package main
import (
"context"
"errors"
"fmt"
"sync"
)
func main() {
// mergeCancel возвращает контекст, который отменяется при отмене любого из исходных контекстов
mergeCancel := func(ctx1, ctx2 context.Context) (context.Context, context.CancelFunc) {
mergedCtx, cancel := context.WithCancelCause(ctx1)
var once sync.Once
// Отслеживаем отмену первого контекста
go func() {
select {
case <-ctx1.Done():
once.Do(func() {
cancel(context.Cause(ctx1))
})
case <-ctx2.Done():
once.Do(func() {
cancel(context.Cause(ctx2))
})
case <-mergedCtx.Done():
// Уже отменен другим путем
}
}()
return mergedCtx, func() {
once.Do(func() {
cancel(context.Canceled)
})
}
}
// Создаем два контекста с возможностью отмены
ctx1, cancel1 := context.WithCancelCause(context.Background())
defer cancel1(errors.New("ctx1 canceled"))
ctx2, cancel2 := context.WithCancelCause(context.Background())
// Объединяем контексты
mergedCtx, mergedCancel := mergeCancel(ctx1, ctx2)
defer mergedCancel()
// Отменяем второй контекст
cancel2(errors.New("ctx2 canceled"))
// Ждем отмены объединенного контекста
<-mergedCtx.Done()
// Выводим причину отмены
fmt.Println("Merged context canceled because:", context.Cause(mergedCtx))
}
Ключевые моменты:
-
Потокобезопасность:
var once sync.Once
once.Do(func() {
cancel(context.Cause(ctx))
})
-
Полное отслеживание контекстов:
select {
case <-ctx1.Done():
case <-ctx2.Done():
case <-mergedCtx.Done():
}
-
Четкая причина отмены:
fmt.Println("Merged context canceled because:", context.Cause(mergedCtx))
-
Гарантированная очистка:
func Cause
func Cause(c Context) error
Cause возвращает не нулевую ошибку, объясняющую, почему c был отменен. Первая отмена c или одного из его родителей устанавливает причину. Если эта отмена произошла через вызов CancelCauseFunc(err), то Cause возвращает err. В противном случае Cause(c) возвращает то же значение, что и c.Err(). Cause возвращает nil, если c еще не был отменен.
func WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel возвращает производный контекст, который указывает на родительский контекст, но имеет новый канал Done. Канал Done возвращенного контекста закрывается при вызове возвращенной функции отмены или при закрытии канала Done родительского контекста, в зависимости от того, что произойдет раньше.
Отмена этого контекста освобождает связанные с ним ресурсы, поэтому код должен вызывать отмену, как только операции, выполняемые в этом контексте, завершатся.
Пример
Этот пример демонстрирует использование отменяемого контекста для предотвращения утечки данных из горутины. К концу выполнения функции примера горутина, запущенная gen, вернется без утечки.
package main
import (
"context"
"fmt"
"sync"
)
func main() {
// gen генерирует целые числа в отдельной горутине и
// отправляет их в возвращаемый канал.
// Вызывающая сторона должна отменить контекст после
// завершения потребления чисел, чтобы избежать утечки горутины.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
defer close(dst) // Закрываем канал при завершении
n := 1
for {
select {
case <-ctx.Done():
return
case dst <- n:
n++
}
}
}()
// Горутина для безопасного завершения
go func() {
wg.Wait()
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Отменяем контекст при завершении
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
// Дополнительная отмена контекста (хотя defer уже сделает это)
cancel()
}
func WithCancelCause
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
WithCancelCause ведет себя как WithCancel, но возвращает CancelCauseFunc вместо CancelFunc. Вызов cancel с no-nil ошибкой (причина) записывает эту ошибку в ctx; затем ее можно получить с помощью Cause(ctx). Вызов cancel с nil устанавливает причину в Canceled.
Пример использования:
ctx, cancel := context.WithCancelCause(parent)
cancel(myError)
ctx.Err() // возвращает context.Canceled
context.Cause(ctx) // возвращает myError
func WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDeadline возвращает производный контекст, который указывает на родительский контекст, но имеет срок, скорректированный так, чтобы он не был позднее d. Если срок родительского контекста уже раньше d, WithDeadline(parent, d) семантически эквивалентен parent. Возвращаемый канал [Context.Done] закрывается по истечении срока, при вызове возвращаемой функции cancel или при закрытии канала Done родительского контекста, в зависимости от того, что произойдет раньше.
Отмена этого контекста освобождает связанные с ним ресурсы, поэтому код должен вызывать отмену, как только операции, выполняемые в этом контексте, завершатся.
Пример
В этом примере контекст с произвольным сроком передается блокирующей функции, чтобы сообщить ей, что она должна прекратить свою работу, как только до нее доберется.
package main
import (
"context"
"fmt"
"time"
)
func main() {
shortDuration := 50 * time.Millisecond // Явное указание времени таймаута
d := time.Now().Add(shortDuration)
// Создаем контекст с дедлайном
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel() // Важно вызывать cancel для освобождения ресурсов
neverReady := make(chan struct{}) // Канал, который никогда не будет готов
select {
case <-neverReady:
fmt.Println("ready") // Эта ветка никогда не выполнится
case <-ctx.Done():
// Проверяем причину завершения контекста
switch ctx.Err() {
case context.DeadlineExceeded:
fmt.Println("context deadline exceeded")
case context.Canceled:
fmt.Println("context canceled")
default:
fmt.Println("context done for unknown reason")
}
}
}
func WithDeadlineCause
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)
WithDeadlineCause ведет себя как WithDeadline, но также устанавливает причину возвращаемого контекста при превышении срока. Возвращаемая CancelFunc не устанавливает причину.
func WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout возвращает WithDeadline(parent, time.Now().Add(timeout)).
Отмена этого контекста освобождает связанные с ним ресурсы, поэтому код должен вызывать отмену, как только операции, выполняемые в этом контексте, завершатся:
func slowOperationWithTimeout(ctx context.Context) (Result, error) {
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel() // освобождает ресурсы, если slowOperation завершается до истечения времени ожидания
return slowOperation(ctx)
}
Пример
В этом примере контекст передается с таймаутом, чтобы сообщить блокирующей функции, что она должна завершить свою работу по истечении таймаута.
package main
import (
"context"
"fmt"
"time"
)
func main() {
shortDuration := 100 * time.Millisecond // Явное указание времени таймаута
// Создаем контекст с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
defer cancel() // Всегда вызываем cancel для освобождения ресурсов
neverReady := make(chan struct{}) // Создаем канал, который никогда не закроется
select {
case <-neverReady:
fmt.Println("ready") // Эта ветка никогда не выполнится
case <-ctx.Done():
// Проверяем причину завершения контекста
switch err := ctx.Err(); err {
case context.DeadlineExceeded:
fmt.Println("context deadline exceeded")
case context.Canceled:
fmt.Println("context canceled")
default:
fmt.Printf("context done: %v\n", err)
}
}
}
func WithTimeoutCause
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)
WithTimeoutCause ведет себя как WithTimeout, но также устанавливает причину возвращаемого Context при истечении таймаута. Возвращаемый CancelFunc не устанавливает причину.
4.4.2 - Описание типов пакета Context
Описание типов пакета Context
type CancelCauseFunc
type CancelCauseFunc func(cause error)
CancelCauseFunc ведет себя как CancelFunc, но дополнительно устанавливает причину отмены. Эту причину можно получить, вызвав Cause на отмененном Context или на любом из его производных Context.
Если контекст уже был отменен, CancelCauseFunc не устанавливает причину. Например, если childContext является производным от parentContext:
- если parentContext отменен с причиной cause1 до того, как childContext отменен с причиной cause2, то Cause(parentContext) == Cause(childContext) == cause1
- если childContext отменен с причиной cause2 до того, как parentContext отменен с причиной cause1, то Cause(parentContext) == cause1 и Cause(childContext) == cause2
type CancelFunc
CancelFunc сообщает операции о необходимости прекратить работу. CancelFunc не ждет, пока работа будет остановлена. CancelFunc может вызываться несколькими goroutines одновременно. После первого вызова последующие вызовы CancelFunc ничего не делают.
type Context
type Context interface {
// Deadline возвращает время, когда работа, выполняемая от имени этого
// контекста должна быть отменена. Deadline возвращает ok==false, если
// срок не установлен. Последовательные вызовы Deadline возвращают
// одинаковые результаты.
Deadline() (deadline time.Time, ok bool)
// Done возвращает канал, который закрывается, когда работа, выполняемая
// от имени этого контекста, должна быть отменена. Done может возвращать
// nil, если этот контекст никогда не может быть отменен. Последовательные
// вызовы Done возвращают одинаковое значение.
// Закрытие канала Done может происходить асинхронно,
// после возврата функции cancel.
//
// WithCancel организует закрытие Done при вызове cancel;
// WithDeadline организует закрытие Done по истечении срока;
// WithTimeout организует закрытие Done по истечении таймаута
//.
//
// Done предоставляется для использования в операторах select:
//
// // Stream генерирует значения с помощью DoSomething и отправляет их в out,
// // пока DoSomething не вернет ошибку или ctx.Done не будет закрыт.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// См. https://blog.golang.org/pipelines для получения дополнительных примеров
// использования канала Done для отмены.
Done() <-chan struct{}
// Если Done еще не закрыт, Err возвращает nil.
// Если Done закрыт, Err возвращает no-nil ошибку, объясняющую причину:
// DeadlineExceeded, если срок контекста истек, или Canceled, если контекст
// был отменен по какой-либо другой причине.
// После того, как Err возвращает ошибку, отличную от nil, последующие вызовы
// Err возвращают ту же ошибку.
Err() error
// Value возвращает значение, связанное с этим контекстом для ключа, или nil,
// если с ключом не связано никакое значение. Последовательные вызовы Value с
// одним и тем же ключом возвращают один и тот же результат.
//
// Используйте значения контекста только для данных в рамках запроса, которые
// проходят через процессы и границы API, а не для передачи опциональных
// параметров в функции.
//
// Ключ идентифицирует конкретное значение в контексте. Функции, которые хотят
// сохранить значения в контексте, обычно выделяют ключ в глобальной
// переменной, а затем используют этот ключ в качестве аргумента для
// context.WithValue и Context.Value. Ключ может быть любого типа, который
// поддерживает равенство;
// пакеты должны определять ключи как неэкспортируемый тип, чтобы избежать коллизий.
//
// Пакеты, которые определяют ключ контекста, должны предоставлять типобезопасные
// аксессоры для значений, хранящихся с использованием этого ключа:
//
// // Пользователь пакета определяет тип User, который хранится в контекстах.
// пакет user
//
// import «context»
//
// // User — это тип значения, хранящегося в Contexts.
// type User struct {...}
//
// // key — это неэкспортируемый тип для ключей, определенных в этом пакете.
// // Это предотвращает конфликты с ключами, определенными в других пакетах.
// type key int
//
// // userKey — ключ для значений user.User в Contexts. Он
// // не экспортируется; клиенты используют user.NewContext и user.FromContext
// // вместо прямого использования этого ключа.
// var userKey key
//
// // NewContext возвращает новый Context, который несет значение u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext возвращает значение User, хранящееся в ctx, если оно есть.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key any) any
}
Context несет в себе срок, сигнал отмены и другие значения через границы API.
Методы контекста могут вызываться несколькими goroutines одновременно.
func Background
func Background() Context
Background возвращает непустой Context, не равный nil. Он никогда не отменяется, не имеет значений и не имеет срока действия. Обычно он используется главной функцией, инициализацией и тестами, а также в качестве Context верхнего уровня для входящих запросов.
func TODO
TODO возвращает непустой Context, не равный nil. Код должен использовать context.TODO, когда неясно, какой Context использовать, или он еще не доступен (потому что окружающая функция еще не была расширена для приема параметра Context).
func WithValue
func WithValue(parent Context, key, val any) Context
WithValue возвращает производный контекст, который указывает на родительский Context. В производном контексте значение, связанное с ключом, является val.
Используйте контекстные значения только для данных в рамках запроса, которые проходят через процессы и API, а не для передачи опциональных параметров функциям.
Предоставленный ключ должен быть сопоставимым и не должен быть типом string или любым другим встроенным типом, чтобы избежать конфликтов между пакетами, использующими контекст. Пользователи WithValue должны определять свои собственные типы для ключей. Чтобы избежать выделения памяти при присвоении interface {}, ключи контекста часто имеют конкретный тип struct {}. В качестве альтернативы статический тип экспортированных переменных ключей контекста должен быть указателем или интерфейсом.
Пример
Этот пример демонстрирует, как можно передать значение в контекст, а также как получить его, если оно существует.
package main
import (
"context"
"fmt"
)
func main() {
// Определяем пользовательский тип для ключей контекста
type favContextKey string
// Функция для поиска значения в контексте
lookupValue := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Printf("Found value for key '%s': %v\n", k, v)
return
}
fmt.Printf("Key '%s' not found in context\n", k)
}
// Создаем ключ и контекст со значением
languageKey := favContextKey("language")
ctx := context.WithValue(context.Background(), languageKey, "Go")
// Ищем существующее значение
lookupValue(ctx, languageKey)
// Ищем несуществующее значение
colorKey := favContextKey("color")
lookupValue(ctx, colorKey)
// Дополнительная проверка с другим типом (демонстрация безопасности)
otherKey := "otherKey" // обычная строка, не favContextKey
lookupValue(ctx, favContextKey(otherKey)) // конвертируем в правильный тип
}
Дополнительные рекомендации:
- Для production-кода:
// Лучше выносить ключи в package-level константы
const (
LanguageKey favContextKey = "language"
ColorKey favContextKey = "color"
)
- Добавить проверку типа значения:
if v, ok := ctx.Value(k).(string); ok {
fmt.Printf("Found string value: %s\n", v)
}
- Для сложных данных использовать указатели:
type configKey struct{}
ctx = context.WithValue(ctx, configKey{}, &MyConfig{...})
func WithoutCancel
func WithoutCancel(parent Context) Context
WithoutCancel возвращает производный контекст, который указывает на родительский контекст и не отменяется при отмене родительского контекста. Возвращаемый контекст не возвращает Deadline или Err, а его канал Done равен nil. Вызов Cause на возвращаемом контексте возвращает nil.
4.5 - Пакет bytes языка программирования Go
Пакет bytes реализует функции для работы с байтовыми фрагментами. Он аналогичен средствам пакета strings.
Константы
MinRead — минимальный размер фрагмента, передаваемый вызову Buffer.Read из Buffer.ReadFrom. Если буфер содержит не менее MinRead байт сверх того, что требуется для хранения содержимого r, Buffer.ReadFrom не будет увеличивать базовый буфер.
Переменные
var ErrTooLarge = errors.New("bytes.Buffer: too large")
ErrTooLarge передается в panic, если не удается выделить память для хранения данных в буфере.
Функции
func Clone
func Clone(b []byte) []byte
Clone возвращает копию b[:len(b)]. Результат может иметь дополнительный неиспользованный объем. Clone(nil) возвращает nil.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
b := []byte("abc")
clone := bytes.Clone(b)
fmt.Printf("%s\n", clone)
clone[0] = 'd'
fmt.Printf("%s\n", b)
fmt.Printf("%s\n", clone)
}
func Compare
func Compare(a, b []byte) int
Compare возвращает целое число, сравнивая два лексикографических фрагмента байтов. Результат будет равен 0, если a == b, -1, если a < b, и +1, если a > b. Аргумент nil эквивалентен пустому фрагменту.
Пример
package main
import (
"bytes"
)
func main() {
// Interpret Compare's result by comparing it to zero.
var a, b []byte
if bytes.Compare(a, b) < 0 {
// a less b
}
if bytes.Compare(a, b) <= 0 {
// a less or equal b
}
if bytes.Compare(a, b) > 0 {
// a greater b
}
if bytes.Compare(a, b) >= 0 {
// a greater or equal b
}
// Prefer Equal to Compare for equality comparisons.
if bytes.Equal(a, b) {
// a equal b
}
if !bytes.Equal(a, b) {
// a not equal b
}
}
Пример Поиск
package main
import (
"bytes"
"slices"
)
func main() {
// Binary search to find a matching byte slice.
var needle []byte
var haystack [][]byte // Assume sorted
_, found := slices.BinarySearchFunc(haystack, needle, bytes.Compare)
if found {
// Found it!
}
}
func Contains
func Contains(b, subslice []byte) bool
Contains сообщает, находится ли подфрагмент в пределах b.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.Contains([]byte("seafood"), []byte("foo")))
fmt.Println(bytes.Contains([]byte("seafood"), []byte("bar")))
fmt.Println(bytes.Contains([]byte("seafood"), []byte("")))
fmt.Println(bytes.Contains([]byte(""), []byte("")))
}
Output:
true
false
true
true
func ContainsAny
func ContainsAny(b []байт, chars string) bool
ContainsAny сообщает, находится ли какая-либо из кодовых точек в кодировке UTF-8 в chars в пределах b.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.ContainsAny([]byte("I like seafood."), "fÄo!"))
fmt.Println(bytes.ContainsAny([]byte("I like seafood."), "去是伟大的."))
fmt.Println(bytes.ContainsAny([]byte("I like seafood."), ""))
fmt.Println(bytes.ContainsAny([]byte(""), ""))
}
Output:
true
true
false
false
func ContainsFunc
func ContainsFunc(b []byte, f func(rune) bool) bool
ContainsFunc сообщает, удовлетворяет ли f(r) любая из кодовых точек r в кодировке UTF-8 в пределах b.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
f := func(r rune) bool {
return r >= 'a' && r <= 'z'
}
fmt.Println(bytes.ContainsFunc([]byte("HELLO"), f))
fmt.Println(bytes.ContainsFunc([]byte("World"), f))
}
func ContainsRune
func ContainsRune(b []byte, r rune) bool
ContainsRune сообщает, содержится ли руна в UTF-8-кодированном байтовом фрагменте b.
func Count
func Count(s, sep []byte) int
Count подсчитывает количество непересекающихся экземпляров sep в s. Если sep - пустой фрагмент, Count возвращает 1 + количество кодовых точек в s, закодированных в UTF-8.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.Count([]byte("cheese"), []byte("e")))
fmt.Println(bytes.Count([]byte("five"), []byte(""))) // before & after each rune
}
func Cut
func Cut(s, sep []byte) (before, after []byte, found bool)
Cut разрезает s вокруг первого экземпляра sep, возвращая текст до и после sep. Результат found сообщает, появляется ли sep в s. Если sep не появляется в s, cut возвращает s, nil, false.
Cut возвращает фрагменты исходного фрагмента s, а не его копии.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
show := func(s, sep string) {
before, after, found := bytes.Cut([]byte(s), []byte(sep))
fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
}
show("Gopher", "Go")
show("Gopher", "ph")
show("Gopher", "er")
show("Gopher", "Badger")
}
Output:
Cut("Gopher", "Go") = "", "pher", true
Cut("Gopher", "ph") = "Go", "er", true
Cut("Gopher", "er") = "Goph", "", true
Cut("Gopher", "Badger") = "Gopher", "", false
func CutPrefix
func CutPrefix(s, prefix []byte) (after []byte, found bool)
CutPrefix возвращает s без предоставленного ведущего префиксного байтового фрагмента и сообщает, найден ли префикс. Если s не начинается с префикса, CutPrefix возвращает s, false. Если префикс - это пустой байтовый фрагмент, CutPrefix возвращает s, true.
CutPrefix возвращает фрагменты исходного фрагмента s, а не его копии.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
show := func(s, sep string) {
after, found := bytes.CutPrefix([]byte(s), []byte(sep))
fmt.Printf("CutPrefix(%q, %q) = %q, %v\n", s, sep, after, found)
}
show("Gopher", "Go")
show("Gopher", "ph")
}
Output:
CutPrefix("Gopher", "Go") = "pher", true
CutPrefix("Gopher", "ph") = "Gopher", false
func CutSuffix
func CutSuffix(s, suffix []byte) (before []byte, found bool)
CutSuffix возвращает s без предоставленного завершающего суффикса байтового фрагмента и сообщает, найден ли суффикс. Если s не заканчивается суффиксом, CutSuffix возвращает s, false. Если суффикс - пустой байтовый фрагмент, CutSuffix возвращает s, true.
CutSuffix возвращает фрагменты исходного фрагмента s, а не его копии.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
show := func(s, sep string) {
before, found := bytes.CutSuffix([]byte(s), []byte(sep))
fmt.Printf("CutSuffix(%q, %q) = %q, %v\n", s, sep, before, found)
}
show("Gopher", "Go")
show("Gopher", "er")
}
Output:
CutSuffix("Gopher", "Go") = "Gopher", false
CutSuffix("Gopher", "er") = "Goph", true
func Equal
func Equal(a, b []byte) bool
Equal сообщает, имеют ли a и b одинаковую длину и содержат ли они одинаковые байты. Аргумент nil эквивалентен пустому фрагменту.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.Equal([]byte("Go"), []byte("Go")))
fmt.Println(bytes.Equal([]byte("Go"), []byte("C++")))
}
func EqualFold
func EqualFold(s, t []byte) bool
EqualFold сообщает, равны ли s и t, интерпретируемые как строки UTF-8, при простом преобразовании регистра Юникода, которое является более общей формой нечувствительности к регистру.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.EqualFold([]byte("Go"), []byte("go")))
}
func Fields
func Fields(s []byte) [][]byte
Fields интерпретирует s как последовательность кодовых точек, закодированных в UTF-8. Он разделяет срез s вокруг каждого экземпляра одного или нескольких последовательных пробельных символов, как определено в unicode.IsSpace, возвращая срез подсрезов s или пустой срез, если s содержит только пробельные символы.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("Fields are: %q", bytes.Fields([]byte(" foo bar baz ")))
}
Output:
Fields are: ["foo" "bar" "baz"]
func FieldsFunc
func FieldsFunc(s []byte, f func(rune) bool) [][]byte
FieldsFunc интерпретирует s как последовательность кодовых точек, закодированных в UTF-8. Он разбивает срез s на каждом проходе кодовых точек c, удовлетворяющих f(c), и возвращает срез подсрезов s. Если все кодовые точки в s удовлетворяют f(c) или len(s) == 0, возвращается пустой срез.
FieldsFunc не дает никаких гарантий относительно порядка вызова f(c) и предполагает, что f всегда возвращает одно и то же значение для данного c.
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
fmt.Printf("Fields are: %q", bytes.FieldsFunc([]byte(" foo1;bar2,baz3..."), f))
}
Output:
Fields are: ["foo1" "bar2" "baz3"]
func FieldsFuncSeq
func FieldsFuncSeq(s []byte, f func(rune) bool) iter.Seq[[]byte]
FieldsFuncSeq возвращает итератор над подфрагментами s, разбитыми по последовательностям кодовых точек Unicode, удовлетворяющих f(c). Итератор возвращает те же подфрагменты, которые были бы возвращены FieldsFunc(s), но без создания нового фрагмента, содержащего подфрагменты.
func FieldsSeq
func FieldsSeq(s []byte) iter.Seq[[]byte]
FieldsSeq возвращает итератор по подсрезам s, разделённым пробельными символами (как определено в unicode.IsSpace). Итератор возвращает те же подсрезы, что и функция Fields(s), но без создания нового среза.
func HasPrefix
func HasPrefix(s, prefix []byte) bool
HasPrefix проверяет, начинается ли байтовый срез s с prefix.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.HasPrefix([]byte("Gopher"), []byte("Go")))
fmt.Println(bytes.HasPrefix([]byte("Gopher"), []byte("C")))
fmt.Println(bytes.HasPrefix([]byte("Gopher"), []byte("")))
func HasSuffix
func HasSuffix(s, suffix []byte) bool
HasSuffix проверяет, заканчивается ли байтовый срез s на suffix.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.HasSuffix([]byte("Amigo"), []byte("go")))
fmt.Println(bytes.HasSuffix([]byte("Amigo"), []byte("O")))
fmt.Println(bytes.HasSuffix([]byte("Amigo"), []byte("Ami")))
fmt.Println(bytes.HasSuffix([]byte("Amigo"), []byte("")))
}
Output:
true
false
false
true
func Index
func Index(s, sep []byte) int
Index возвращает индекс первого вхождения sep в s или -1, если sep отсутствует в s.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.Index([]byte("chicken"), []byte("ken")))
fmt.Println(bytes.Index([]byte("chicken"), []byte("dmr")))
}
func IndexAny
func IndexAny(s []byte, chars string) int
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.IndexAny([]byte("chicken"), "aeiouy"))
fmt.Println(bytes.IndexAny([]byte("crwth"), "aeiouy"))
}
IndexAny интерпретирует s как последовательность UTF-8 кодовых точек. Возвращает байтовый индекс первого вхождения любого символа из chars в s. Возвращает -1, если chars пуст или нет совпадений.
func IndexByte
func IndexByte(b []byte, c byte) int
IndexByte возвращает индекс первого вхождения байта c в b или -1, если c отсутствует в b.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.IndexByte([]byte("chicken"), byte('k')))
fmt.Println(bytes.IndexByte([]byte("chicken"), byte('g')))
}
func IndexFunc
func IndexFunc(s []byte, f func(r rune) bool) int
IndexFunc интерпретирует s как последовательность UTF-8 кодовых точек. Возвращает байтовый индекс первой кодовой точки, удовлетворяющей условию f(c), или -1, если таких нет.
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
f := func(c rune) bool {
return unicode.Is(unicode.Han, c)
}
fmt.Println(bytes.IndexFunc([]byte("Hello, 世界"), f))
fmt.Println(bytes.IndexFunc([]byte("Hello, world"), f))
}
func IndexRune
func IndexRune(s []byte, r rune) int
IndexRune интерпретирует s как последовательность UTF-8 кодовых точек. Возвращает байтовый индекс первого вхождения руны r. Если r — utf8.RuneError, возвращает первый индекс невалидной UTF-8 последовательности.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.IndexRune([]byte("chicken"), 'k'))
fmt.Println(bytes.IndexRune([]byte("chicken"), 'd'))
}
func Join
func Join(s [][]byte, sep []byte) []byte
Join объединяет элементы среза s в новый байтовый срез, разделяя их sep.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
s := [][]byte{[]byte("foo"), []byte("bar"), []byte("baz")}
fmt.Printf("%s", bytes.Join(s, []byte(", ")))
}
func LastIndex
func LastIndex(s, sep []byte) int
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.Index([]byte("go gopher"), []byte("go")))
fmt.Println(bytes.LastIndex([]byte("go gopher"), []byte("go")))
fmt.Println(bytes.LastIndex([]byte("go gopher"), []byte("rodent")))
}
LastIndex возвращает индекс последнего экземпляра sep в s или -1, если sep отсутствует в s.
func LastIndexAny
func LastIndexAny(s []byte, chars string) int
LastIndexAny интерпретирует s как последовательность кодовых точек Unicode в кодировке UTF-8. Возвращает индекс байта последнего вхождения в s любой из кодовых точек Unicode в chars. Возвращает -1, если chars пуст или если нет общей кодовой точки.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.LastIndexAny([]byte("go gopher"), "MüQp"))
fmt.Println(bytes.LastIndexAny([]byte("go 地鼠"), "地大"))
fmt.Println(bytes.LastIndexAny([]byte("go gopher"), "z,!."))
}
func LastIndexByte
func LastIndexByte(s []byte, c byte) int
LastIndexByte возвращает индекс последнего экземпляра c в s или -1, если c отсутствует в s.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.LastIndexByte([]byte("go gopher"), byte('g')))
fmt.Println(bytes.LastIndexByte([]byte("go gopher"), byte('r')))
fmt.Println(bytes.LastIndexByte([]byte("go gopher"), byte('z')))
}
func LastIndexFunc
func LastIndexFunc(s []byte, f func(r rune) bool) int
LastIndexFunc интерпретирует s как последовательность кодовых точек, закодированных в UTF-8. Он возвращает индекс байта в s последней кодовой точки Unicode, удовлетворяющей f(c), или -1, если таковой нет.
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
fmt.Println(bytes.LastIndexFunc([]byte("go gopher!"), unicode.IsLetter))
fmt.Println(bytes.LastIndexFunc([]byte("go gopher!"), unicode.IsPunct))
fmt.Println(bytes.LastIndexFunc([]byte("go gopher!"), unicode.IsNumber))
}
func Lines
func Lines(s []byte) iter.Seq[[]byte]
Lines возвращает итератор по строкам, завершающимся символом новой строки, в байтовом срезе s. Строки, возвращаемые итератором, включают в себя завершающие символы новой строки. Если s пустой, итератор не возвращает никаких строк. Если s не заканчивается символом новой строки, последняя возвращаемая строка не будет заканчиваться символом новой строки. Возвращает итератор однократного использования.
func Map
func Map(mapping func(r rune) rune, s []byte) []byte
Map возвращает копию байтового среза s со всеми символами, измененными в соответствии с функцией отображения. Если отображение возвращает отрицательное значение, символ удаляется из байтового среза без замены. Символы в s и выводе интерпретируются как кодовые точки, закодированные в UTF-8.
Пример
import (
"bytes"
"fmt"
)
func main() {
rot13 := func(r rune) rune {
switch {
case r >= 'A' && r <= 'Z':
return 'A' + (r-'A'+13)%26
case r >= 'a' && r <= 'z':
return 'a' + (r-'a'+13)%26
}
return r
}
fmt.Printf("%s\n", bytes.Map(rot13, []byte("'Twas brillig and the slithy gopher...")))
}
Output:
'Gjnf oevyyvt naq gur fyvgul tbcure...
func Repeat
func Repeat(b []byte, count int) []byte
Repeat возвращает новый байтовый срез, состоящий из count копий b.
Он вызывает панику, если count отрицательно или если результат (len(b) * count) переполняется.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("ba%s", bytes.Repeat([]byte("na"), 2))
}
func Replace
func Replace(s, old, new []byte, n int) []byte
Replace возвращает копию фрагмента s с первыми n непересекающимися экземплярами old, замененными на new. Если old пуст, то он совпадает в начале фрагмента и после каждой последовательности UTF-8, что позволяет получить до k+1 замен для k-рангового фрагмента. Если n < 0, то количество замен не ограничено.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%s\n", bytes.Replace([]byte("oink oink oink"), []byte("k"), []byte("ky"), 2))
fmt.Printf("%s\n", bytes.Replace([]byte("oink oink oink"), []byte("oink"), []byte("moo"), -1))
}
Output:
oinky oinky oink
moo moo moo
func ReplaceAll
func ReplaceAll(s, old, new []byte) []byte
ReplaceAll возвращает копию фрагмента s со всеми непересекающимися экземплярами old, замененными на new. Если old пуст, то совпадение происходит в начале фрагмента и после каждой последовательности UTF-8, что позволяет получить до k+1 замен для фрагмента длиной в k строк.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%s\n", bytes.ReplaceAll([]byte("oink oink oink"), []byte("oink"), []byte("moo")))
}
func Runes
func Runes(s []byte) []rune
Runes интерпретирует s как последовательность кодовых точек в кодировке UTF-8. Она возвращает фрагмент рун (кодовых точек Юникода), эквивалентный s.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
rs := bytes.Runes([]byte("go gopher"))
for _, r := range rs {
fmt.Printf("%#U\n", r)
}
}
Output:
U+0067 'g'
U+006F 'o'
U+0020 ' '
U+0067 'g'
U+006F 'o'
U+0070 'p'
U+0068 'h'
U+0065 'e'
U+0072 'r'
func Split
func Split(s, sep []byte) [][]byte
Split разбивает s на все подфрагменты, разделенные sep, и возвращает фрагмент подфрагментов между этими разделителями. Если sep пуст, Split разделяет после каждой последовательности UTF-8. Это эквивалентно SplitN с числом -1.
Для разбиения вокруг первого экземпляра разделителя смотрите Cut.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%q\n", bytes.Split([]byte("a,b,c"), []byte(",")))
fmt.Printf("%q\n", bytes.Split([]byte("a man a plan a canal panama"), []byte("a ")))
fmt.Printf("%q\n", bytes.Split([]byte(" xyz "), []byte("")))
fmt.Printf("%q\n", bytes.Split([]byte(""), []byte("Bernardo O'Higgins")))
}
Output:
["a" "b" "c"]
["" "man " "plan " "canal panama"]
[" " "x" "y" "z" " "]
[""]
func SplitAfter
func SplitAfter(s, sep []byte) [][]byte
SplitAfter разбивает s на все подфрагменты после каждого экземпляра sep и возвращает фрагмент этих подфрагментов. Если sep пуст, SplitAfter разбивает после каждой последовательности UTF-8. Это эквивалентно SplitAfterN с количеством -1.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%q\n", bytes.SplitAfter([]byte("a,b,c"), []byte(",")))
}
func SplitAfterN
func SplitAfterN(s, sep []byte, n int) [][]byte
SplitAfterN разбивает s на подфрагменты после каждого экземпляра sep и возвращает фрагмент этих подфрагментов. Если sep пуст, SplitAfterN разбивает после каждой последовательности UTF-8. Параметр count определяет количество возвращаемых подфрагментов:
- n > 0: не более n подфрагментов; последний подфрагмент будет нерасщепленным остатком;
- n == 0: результат равен nil (нулевые подмножества);
- n < 0: все поддоли.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%q\n", bytes.SplitAfterN([]byte("a,b,c"), []byte(","), 2))
}
func SplitAfterSeq
func SplitAfterSeq(s, sep []byte) iter.Seq[[]byte]
SplitAfterSeq возвращает итератор по подфрагментам s, разделенным после каждого экземпляра sep. Итератор дает те же подфрагменты, которые были бы возвращены SplitAfter(s, sep), но без построения нового фрагмента, содержащего подфрагменты. Возвращается одноразовый итератор.
func SplitN
func SplitN(s, sep []байт, n int) [][]байт
SplitN разбивает s на подфрагменты, разделенные sep, и возвращает фрагмент подфрагментов между этими разделителями. Если sep пуст, SplitN разбивает после каждой последовательности UTF-8. Параметр count определяет количество возвращаемых подфрагментов:
- n > 0: не более n подфрагментов; последний подфрагмент будет нерасщепленным остатком;
- n == 0: результат равен nil (нулевые подмножества);
- n < 0: все подфрагменты.
Для разбиения вокруг первого экземпляра разделителя смотрите Cut.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%q\n", bytes.SplitN([]byte("a,b,c"), []byte(","), 2))
z := bytes.SplitN([]byte("a,b,c"), []byte(","), 0)
fmt.Printf("%q (nil = %v)\n", z, z == nil)
}
Output:
["a" "b,c"]
[] (nil = true)
func SplitSeq
func SplitSeq(s, sep []byte) iter.Seq[[]byte]
SplitSeq возвращает итератор по всем подфрагментам s, разделенным sep. Итератор дает те же подфрагменты, которые были бы возвращены функцией Split(s, sep), но без построения нового фрагмента, содержащего эти подфрагменты. Возвращается одноразовый итератор.
func Title (устарело)
func ToLower
func ToLower(s []byte) []byte
ToLower возвращает копию байтового фрагмента s со всеми буквами Юникода, переведенными в нижний регистр.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%s", bytes.ToLower([]byte("Gopher")))
}
func ToLowerSpecial
func ToLowerSpecial(c unicode.SpecialCase, s []byte) []byte
ToLowerSpecial обрабатывает s как байт в кодировке UTF-8 и возвращает копию со всеми буквами Юникода, переведенными в нижний регистр, отдавая приоритет правилам специального регистра.
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
str := []byte("AHOJ VÝVOJÁRİ GOLANG")
totitle := bytes.ToLowerSpecial(unicode.AzeriCase, str)
fmt.Println("Original : " + string(str))
fmt.Println("ToLower : " + string(totitle))
}
Output:
Original : AHOJ VÝVOJÁRİ GOLANG
ToLower : ahoj vývojári golang
func ToTitle
func ToTitle(s []byte) []byte
ToTitle обрабатывает s как байт в кодировке UTF-8 и возвращает его копию со всеми буквами Юникода, отображенными в регистр заголовка.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%s\n", bytes.ToTitle([]byte("loud noises")))
fmt.Printf("%s\n", bytes.ToTitle([]byte("брат")))
}
func ToTitleSpecial
func ToTitleSpecial(c unicode.SpecialCase, s []byte) []byte
ToTitleSpecial обрабатывает s как байты в кодировке UTF-8 и возвращает копию со всеми буквами Юникода, отображенными в их заглавный регистр, отдавая приоритет правилам специального регистра.
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
str := []byte("ahoj vývojári golang")
totitle := bytes.ToTitleSpecial(unicode.AzeriCase, str)
fmt.Println("Original : " + string(str))
fmt.Println("ToTitle : " + string(totitle))
}
Output:
Original : ahoj vývojári golang
ToTitle : AHOJ VÝVOJÁRİ GOLANG
func ToUpper
func ToUpper(s []byte) []byte
ToUpper возвращает копию байтового фрагмента s со всеми буквами Юникода, переведенными в верхний регистр.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%s", bytes.ToUpper([]byte("Gopher")))
}
func ToUpperSpecial
func ToUpperSpecial(c unicode.SpecialCase, s []byte) []byte
ToUpperSpecial обрабатывает s как байт в кодировке UTF-8 и возвращает копию со всеми буквами Юникода, переведенными в верхний регистр, отдавая приоритет правилам специального регистра.
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
str := []byte("ahoj vývojári golang")
totitle := bytes.ToUpperSpecial(unicode.AzeriCase, str)
fmt.Println("Original : " + string(str))
fmt.Println("ToUpper : " + string(totitle))
}
Output:
Original : ahoj vývojári golang
ToUpper : AHOJ VÝVOJÁRİ GOLANG
func ToValidUTF8
func ToValidUTF8(s, replacement []byte) []byte
ToValidUTF8 обрабатывает s как байты в кодировке UTF-8 и возвращает копию, в которой каждый ряд байтов, представляющих недопустимый UTF-8, заменен байтами в replacement, которая может быть пустой.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%s\n", bytes.ToValidUTF8([]byte("abc"), []byte("\uFFFD")))
fmt.Printf("%s\n", bytes.ToValidUTF8([]byte("a\xffb\xC0\xAFc\xff"), []byte("")))
fmt.Printf("%s\n", bytes.ToValidUTF8([]byte("\xed\xa0\x80"), []byte("abc")))
}
func Trim
func Trim(s []байт, cutset string) []байт
Trim возвращает подфрагмент s, отсекая все ведущие и последующие кодовые точки в кодировке UTF-8, содержащиеся в cutset.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("[%q]", bytes.Trim([]byte(" !!! Achtung! Achtung! !!! "), "! "))
}
Output:
["Achtung! Achtung"]
func TrimFunc
func TrimFunc(s []byte, f func(r rune) bool) []byte
TrimFunc возвращает подфрагмент s, отсекая все ведущие и последующие точки кода c в кодировке UTF-8, которые удовлетворяют f(c).
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
fmt.Println(string(bytes.TrimFunc([]byte("go-gopher!"), unicode.IsLetter)))
fmt.Println(string(bytes.TrimFunc([]byte("\"go-gopher!\""), unicode.IsLetter)))
fmt.Println(string(bytes.TrimFunc([]byte("go-gopher!"), unicode.IsPunct)))
fmt.Println(string(bytes.TrimFunc([]byte("1234go-gopher!567"), unicode.IsNumber)))
}
Output:
-gopher!
"go-gopher!"
go-gopher
go-gopher!
func TrimLeft
func TrimLeft(s []байт, cutset string) []байт
TrimLeft возвращает подфрагмент s, отсекая все ведущие точки кода в кодировке UTF-8, содержащиеся в cutset.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Print(string(bytes.TrimLeft([]byte("453gopher8257"), "0123456789")))
}
func TrimLeftFunc
func TrimLeftFunc(s []байт, f func(r rune) bool) []байт
TrimLeftFunc рассматривает s как байт в кодировке UTF-8 и возвращает подфрагмент s, отрезая все ведущие кодовые точки c в кодировке UTF-8, которые удовлетворяют f(c).
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
fmt.Println(string(bytes.TrimLeftFunc([]byte("go-gopher"), unicode.IsLetter)))
fmt.Println(string(bytes.TrimLeftFunc([]byte("go-gopher!"), unicode.IsPunct)))
fmt.Println(string(bytes.TrimLeftFunc([]byte("1234go-gopher!567"), unicode.IsNumber)))
}
Output:
-gopher
go-gopher!
go-gopher!567
func TrimPrefix
func TrimPrefix(s, prefix []byte) []byte
TrimPrefix возвращает s без предоставленной ведущей строки префикса. Если s не начинается с префикса, s возвращается без изменений.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
var b = []byte("Goodbye,, world!")
b = bytes.TrimPrefix(b, []byte("Goodbye,"))
b = bytes.TrimPrefix(b, []byte("See ya,"))
fmt.Printf("Hello%s", b)
}
func TrimRight
func TrimRight(s []байт, cutset string) []байт
TrimRight возвращает подфрагмент s, отрезая все кодовые точки в кодировке UTF-8, которые содержатся в cutset.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Print(string(bytes.TrimRight([]byte("453gopher8257"), "0123456789")))
}
func TrimRightFunc
func TrimRightFunc(s []byte, f func(r rune) bool) []byte
TrimRightFunc возвращает подфрагмент s, отсекая все кодовые точки c, которые удовлетворяют f(c).
Пример
package main
import (
"bytes"
"fmt"
"unicode"
)
func main() {
fmt.Println(string(bytes.TrimRightFunc([]byte("go-gopher"), unicode.IsLetter)))
fmt.Println(string(bytes.TrimRightFunc([]byte("go-gopher!"), unicode.IsPunct)))
fmt.Println(string(bytes.TrimRightFunc([]byte("1234go-gopher!567"), unicode.IsNumber)))
}
Output:
go-
go-gopher
1234go-gopher!
func TrimSpace
func TrimSpace(s []byte) []byte
TrimSpace возвращает подфрагмент s, отсекая все ведущие и последующие белые пробелы, как определено Unicode.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Printf("%s", bytes.TrimSpace([]byte(" \t\n a lone gopher \n\t\r\n")))
}
func TrimSuffix
func TrimSuffix(s, suffix []byte) []byte
TrimSuffix возвращает s без предоставленной строки суффикса. Если s не заканчивается суффиксом, s возвращается без изменений.
Пример
package main
import (
"bytes"
"os"
)
func main() {
var b = []byte("Hello, goodbye, etc!")
b = bytes.TrimSuffix(b, []byte("goodbye, etc!"))
b = bytes.TrimSuffix(b, []byte("gopher"))
b = append(b, bytes.TrimSuffix([]byte("world!"), []byte("x!"))...)
os.Stdout.Write(b)
}
Типы
type Buffer
type Buffer struct {
// содержит отфильтрованные или неэкспонированные поля
}
Буфер - это буфер переменного размера из байтов с методами Buffer.Read и Buffer.Write. Нулевое значение для Buffer - это пустой буфер, готовый к использованию.
Пример
package main
import (
"bytes"
"fmt"
"os"
)
func main() {
var b bytes.Buffer // A Buffer needs no initialization.
b.Write([]byte("Hello "))
fmt.Fprintf(&b, "world!")
b.WriteTo(os.Stdout)
}
Пример Reader
package main
import (
"bytes"
"encoding/base64"
"io"
"os"
)
func main() {
// A Buffer can turn a string or a []byte into an io.Reader.
buf := bytes.NewBufferString("R29waGVycyBydWxlIQ==")
dec := base64.NewDecoder(base64.StdEncoding, buf)
io.Copy(os.Stdout, dec)
}
func NewBuffer
func NewBuffer(buf []byte) *Buffer
NewBuffer создает и инициализирует новый буфер, используя buf в качестве его начального содержимого. Новый буфер получает право собственности на buf, и вызывающая сторона не должна использовать buf после этого вызова. NewBuffer предназначен для подготовки буфера к чтению существующих данных. Он также может быть использован для установки начального размера внутреннего буфера для записи. Для этого buf должен иметь желаемый объем, но длину, равную нулю.
В большинстве случаев для инициализации буфера достаточно использовать new(Buffer) (или просто объявить переменную Buffer).
func NewBufferString
func NewBufferString(s string) *Buffer
NewBufferString создает и инициализирует новый буфер, используя строку s в качестве его начального содержимого. Он предназначен для подготовки буфера к чтению существующей строки.
В большинстве случаев для инициализации буфера достаточно использовать new(Buffer) (или просто объявить переменную Buffer).
func (*Buffer) Available
func (b *Buffer) Available() int
Available возвращает количество неиспользованных байт в буфере.
func (*Buffer) AvailableBuffer
func (b *Buffer) AvailableBuffer() []byte
AvailableBuffer возвращает пустой буфер с емкостью b.Available(). Этот буфер предназначен для
Пример
package main
import (
"bytes"
"os"
"strconv"
)
func main() {
var buf bytes.Buffer
for i := 0; i < 4; i++ {
b := buf.AvailableBuffer()
b = strconv.AppendInt(b, int64(i), 10)
b = append(b, ' ')
buf.Write(b)
}
os.Stdout.Write(buf.Bytes())
}
func (*Buffer) Bytes
func (b *Buffer) Bytes() []byte
Bytes возвращает фрагмент длины b.Len(), содержащий непрочитанную часть буфера. Этот фрагмент действителен для использования только до следующего изменения буфера (то есть только до следующего вызова метода типа Buffer.Read, Buffer.Write, Buffer.Reset или Buffer.Truncate). Слайс псевдоним содержимого буфера, по крайней мере, до следующей модификации буфера, поэтому немедленные изменения в слайсе повлияют на результат последующих чтений.
Пример
package main
import (
"bytes"
"os"
)
func main() {
buf := bytes.Buffer{}
buf.Write([]byte{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'})
os.Stdout.Write(buf.Bytes())
}
func (*Buffer) Cap
func (b *Buffer) Cap() int
Cap возвращает емкость базового байтового среза буфера, то есть общее пространство, выделенное для данных буфера.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
buf1 := bytes.NewBuffer(make([]byte, 10))
buf2 := bytes.NewBuffer(make([]byte, 0, 10))
fmt.Println(buf1.Cap())
fmt.Println(buf2.Cap())
}
func (*Buffer) Grow
func (b *Buffer) Grow(n int)
Grow увеличивает емкость буфера, если необходимо, чтобы гарантировать место для еще n байт. После Grow(n) в буфер может быть записано не менее n байт без дополнительного выделения. Если n отрицательно, Grow паникует. Если буфер не может вырасти, то произойдет паника с ошибкой ErrTooLarge.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
var b bytes.Buffer
b.Grow(64)
bb := b.Bytes()
b.Write([]byte("64 bytes or fewer"))
fmt.Printf("%q", bb[:b.Len()])
}
Output:
"64 bytes or fewer"
func (*Buffer) Len
func (b *Buffer) Len() int
Len возвращает количество байт непрочитанной части буфера; b.Len() == len(b.Bytes()).
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
var b bytes.Buffer
b.Grow(64)
b.Write([]byte("abcde"))
fmt.Printf("%d", b.Len())
}
func (*Buffer) Next
func (b *Buffer) Next(n int) []byte
Next возвращает фрагмент, содержащий следующие n байт из буфера, продвигая буфер так, как если бы байты были возвращены функцией Buffer.Read. Если в буфере меньше n байт, Next возвращает весь буфер. Фрагмент действителен только до следующего вызова метода чтения или записи.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
var b bytes.Buffer
b.Grow(64)
b.Write([]byte("abcde"))
fmt.Printf("%s\n", b.Next(2))
fmt.Printf("%s\n", b.Next(2))
fmt.Printf("%s", b.Next(2))
}
func (*Buffer) Read
func (b *Buffer) Read(p []byte) (n int, err error)
Read считывает следующие len(p) байт из буфера или пока буфер не будет исчерпан. Возвращаемое значение n - количество прочитанных байт. Если в буфере нет данных для возврата, err будет io.EOF (если только len(p) не равно нулю); в противном случае это будет nil.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
var b bytes.Buffer
b.Grow(64)
b.Write([]byte("abcde"))
rdbuf := make([]byte, 1)
n, err := b.Read(rdbuf)
if err != nil {
panic(err)
}
fmt.Println(n)
fmt.Println(b.String())
fmt.Println(string(rdbuf))
}
func (*Buffer) ReadByte
func (b *Buffer) ReadByte() (byte, error)
ReadByte считывает и возвращает следующий байт из буфера. Если ни один байт не доступен, возвращается ошибка io.EOF.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
var b bytes.Buffer
b.Grow(64)
b.Write([]byte("abcde"))
c, err := b.ReadByte()
if err != nil {
panic(err)
}
fmt.Println(c)
fmt.Println(b.String())
}
func (*Buffer) ReadBytes
func (b *Buffer) ReadBytes(delim byte) (line []byte, err error)
ReadBytes читает до первого появления delim во входных данных, возвращая фрагмент, содержащий данные до разделителя включительно. Если ReadBytes встречает ошибку до нахождения разделителя, он возвращает данные, прочитанные до ошибки, и саму ошибку (часто io.EOF). ReadBytes возвращает err != nil тогда и только тогда, когда возвращаемые данные не заканчиваются на delim.
func (*Buffer) ReadFrom
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error)
ReadFrom читает данные из r до EOF и добавляет их в буфер, увеличивая буфер по мере необходимости. Возвращаемое значение n - это количество прочитанных байт. Также возвращается любая ошибка, кроме io.EOF, возникшая во время чтения. Если буфер становится слишком большим, ReadFrom паникует с сообщением ErrTooLarge.
func (*Buffer) ReadRune
func (b *Buffer) ReadRune() (r rune, size int, err error)
ReadRune считывает и возвращает следующую кодовую точку Юникода в кодировке UTF-8 из буфера. Если байтов нет, возвращается ошибка io.EOF. Если байты имеют ошибочную кодировку UTF-8, то считывается один байт и возвращается U+FFFD, 1.
func (*Buffer) ReadString
func (b *Buffer) ReadString(delim byte) (line string, err error)
ReadString читает до первого появления delim во входных данных, возвращая строку, содержащую данные до разделителя включительно. Если ReadString встречает ошибку до нахождения разделителя, она возвращает данные, считанные до ошибки, и саму ошибку (часто io.EOF). ReadString возвращает err != nil тогда и только тогда, когда возвращаемые данные не заканчиваются разделителем.
func (*Buffer) Reset
Сброс приводит к тому, что буфер становится пустым, но сохраняет базовое хранилище для использования в будущих записях. Сброс - это то же самое, что и Buffer.Truncate(0).
func (*Buffer) String
func (b *Buffer) String() string
String возвращает содержимое непрочитанной части буфера в виде строки. Если буфер является нулевым указателем, возвращается «».
Для более эффективного создания строк см. тип strings.Builder.
func (*Buffer) Truncate
func (b *Buffer) Truncate(n int)
Truncate удаляет из буфера все непрочитанные байты, кроме первых n, но продолжает использовать выделенную память. Если n отрицательно или больше длины буфера, происходит паника.
func (*Buffer) UnreadByte
func (b *Buffer) UnreadByte() error
UnreadByte считывает последний байт, возвращенный последней успешной операцией чтения, которая прочитала хотя бы один байт. Если с момента последнего чтения произошла запись, если последнее чтение вернуло ошибку или если при чтении было прочитано ноль байт, UnreadByte возвращает ошибку.
func (*Buffer) UnreadRune
func (b *Buffer) UnreadRune() error
UnreadRune считывает последнюю руну, возвращенную Buffer.ReadRune. Если последняя операция чтения или записи в буфер не была успешной Buffer.ReadRune, UnreadRune возвращает ошибку. (В этом отношении она строже, чем Buffer.UnreadByte, которая отменяет чтение последнего байта из любой операции чтения).
func (*Buffer) Write
func (b *Buffer) Write(p []byte) (n int, err error)
Write добавляет содержимое p в буфер, увеличивая буфер по мере необходимости. Возвращаемое значение n - длина p; err всегда равно nil. Если буфер становится слишком большим, Write паникует с сообщением ErrTooLarge.
func (*Buffer) WriteByte
func (b *Buffer) WriteByte(c byte) error
WriteByte добавляет байт c в буфер, увеличивая буфер по мере необходимости. Возвращаемая ошибка всегда равна nil, но она включается, чтобы соответствовать WriteByte от bufio.Writer. Если буфер становится слишком большим, WriteByte паникует с сообщением ErrTooLarge.
func (*Buffer) WriteRune
func (b *Buffer) WriteRune(r rune) (n int, err error)
WriteRune добавляет кодировку UTF-8 кодовой точки Юникода r к буферу, возвращая его длину и ошибку, которая всегда равна nil, но включается для соответствия WriteRune от bufio.Writer. Буфер увеличивается по мере необходимости; если он становится слишком большим, WriteRune паникует с сообщением ErrTooLarge.
func (*Buffer) WriteString
func (b *Buffer) WriteString(s string) (n int, err error)
WriteString добавляет содержимое s в буфер, увеличивая буфер по мере необходимости. Возвращаемое значение n - длина s; err всегда равно nil. Если буфер становится слишком большим, WriteString паникует с сообщением ErrTooLarge.
func (*Buffer) WriteTo
func (b *Buffer) WriteTo(w io.Writer) (n int64, err error)
WriteTo записывает данные в w до тех пор, пока буфер не будет исчерпан или не произойдет ошибка. Возвращаемое значение n - это количество записанных байт; оно всегда помещается в int, но для соответствия интерфейсу io.WriterTo оно равно int64. Также возвращается любая ошибка, возникшая во время записи.
type Reader
type Reader struct {
// содержит отфильтрованные или неотфильтрованные поля
}
Reader реализует интерфейсы io.Reader, io.ReaderAt, io.WriterTo, io.Seeker, io.ByteScanner и io.RuneScanner, читая из байтового фрагмента. В отличие от буфера, Reader доступен только для чтения и поддерживает поиск. Нулевое значение для Reader работает как Reader пустого фрагмента.
func NewReader
func NewReader(b []byte) *Reader
NewReader возвращает новый Reader, читающий из b.
func (*Reader) Len
func (r *Reader) Len() int
Len возвращает количество байт непрочитанной части фрагмента.
Пример
package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println(bytes.NewReader([]byte("Hi!")).Len())
fmt.Println(bytes.NewReader([]byte("こんにちは!")).Len())
}
func (*Reader) Read
func (r *Reader) Read(b []byte) (n int, err error)
Read реализует интерфейс io.Reader.
func (*Reader) ReadAt
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error)
ReadAt реализует интерфейс io.ReaderAt.
func (*Reader) ReadByte
func (r *Reader) ReadByte() (byte, error)
ReadByte реализует интерфейс io.ByteReader.
func (*Reader) ReadRune
func (r *Reader) ReadRune() (ch rune, size int, err error)
ReadRune реализует интерфейс io.RuneReader.
func (r *Reader) Reset(b []byte)
Сброс переводит ридер в режим чтения из b.
func (*Reader) Seek
func (r *Reader) Seek(offset int64, whence int) (int64, error)
Seek реализует интерфейс io.Seeker.
func (*Reader) Size
func (r *Reader) Size() int64
Size возвращает исходную длину базового байтового фрагмента. Size - это количество байт, доступных для чтения через Reader.ReadAt. На результат не влияют никакие вызовы методов, кроме Reader.Reset.
func (*Reader) UnreadByte
func (r *Reader) UnreadByte() error
UnreadByte дополняет Reader.ReadByte в реализации интерфейса io.ByteScanner.
func (*Reader) UnreadRune
func (r *Reader) UnreadRune() error
UnreadRune дополняет Reader.ReadRune в реализации интерфейса io.RuneScanner.
func (*Reader) WriteTo
func (r *Reader) WriteTo(w io.Writer) (n int64, err error)
WriteTo реализует интерфейс io.WriterTo.
4.6 - Описание пакета database языка программирования Go
Пакет для работы с базами данных
Пакет sql предоставляет общий интерфейс для баз данных SQL (или SQL-подобных).
Пакет sql должен использоваться вместе с драйвером базы данных. Список драйверов см. на сайте https://golang.org/s/sqldrivers.
Драйверы, не поддерживающие отмену контекста, вернут результат только после завершения запроса.
Примеры использования приведены на вики-странице https://golang.org/s/sqlwiki.
Пакет driver определяет интерфейсы, которые должны быть реализованы драйверами баз данных, используемыми пакетом sql.
Большая часть кода должна использовать пакет database/sql.
Подробная документация по драйверам: https://pkg.go.dev/database/sql/driver
4.6.1 - Работа с пакетом database/sql в Go: ошибки и их обработка
Переменные в пакете database/sql с примерами
Пакет database/sql в Go предоставляет универсальный интерфейс для работы с SQL-базами данных. Рассмотрим основные ошибки, которые могут возникнуть при работе с этим пакетом, и как их правильно обрабатывать.
Основные ошибки пакета database/sql
1. ErrConnDone - соединение уже закрыто
var ErrConnDone = errors.New("sql: connection is already closed")
Эта ошибка возникает, когда вы пытаетесь выполнить операцию на соединении, которое уже было возвращено в пул соединений.
Пример:
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
// Возвращаем соединение в пул
conn.Close()
// Пытаемся использовать закрытое соединение
err = conn.PingContext(context.Background())
if errors.Is(err, sql.ErrConnDone) {
fmt.Println("Ошибка: соединение уже закрыто")
}
2. ErrNoRows - нет строк в результате
var ErrNoRows = errors.New("sql: no rows in result set")
Эта ошибка возвращается методом Scan, когда QueryRow не находит ни одной строки.
Правильная обработка:
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 123).Scan(&name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("Пользователь не найден")
} else {
log.Fatal(err)
}
} else {
fmt.Printf("Имя пользователя: %s\n", name)
}
3. ErrTxDone - транзакция уже завершена
var ErrTxDone = errors.New("sql: transaction has already been committed or rolled back")
Эта ошибка возникает при попытке выполнить операцию в уже завершенной транзакции.
Пример:
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// Фиксируем транзакцию
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
// Пытаемся выполнить запрос в завершенной транзакции
_, err = tx.Exec("INSERT INTO users(name) VALUES (?)", "Alice")
if errors.Is(err, sql.ErrTxDone) {
fmt.Println("Ошибка: транзакция уже завершена")
}
Советы по работе с database/sql
- Всегда проверяйте ошибки после операций с базой данных.
- Используйте
errors.Is для проверки конкретных ошибок пакета database/sql.
- Закрывайте ресурсы (соединения, транзакции, результаты запросов) с помощью defer.
- Используйте контексты для управления таймаутами и отменой операций.
Полный пример работы с базой данных:
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
ID int
Name string
}
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверка соединения
err = db.Ping()
if err != nil {
log.Fatal(err)
}
// Пример запроса с обработкой ErrNoRows
user, err := getUser(db, 123)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("Пользователь не найден")
} else {
log.Fatal(err)
}
} else {
fmt.Printf("Найден пользователь: %+v\n", user)
}
// Пример транзакции
err = transferMoney(db, 1, 2, 100)
if err != nil {
log.Fatal(err)
}
}
func getUser(db *sql.DB, id int) (*User, error) {
var user User
err := db.QueryRow("SELECT id, name FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name)
if err != nil {
return nil, err
}
return &user, nil
}
func transferMoney(db *sql.DB, from, to, amount int) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
}()
// Списание денег
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
// Зачисление денег
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
if err != nil {
return err
}
return nil
}
4.6.2 - Подробное описание функций пакета database/sql в Go
Пакет database/sql предоставляет универсальный интерфейс для работы с SQL-базами данных. Рассмотрим основные функции этого пакета с примерами использования.
Основные функции управления драйверами
func Drivers() []string
Эта функция возвращает отсортированный список имен зарегистрированных драйверов баз данных.
Пример использования:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql" // регистрируем MySQL драйвер
_ "github.com/lib/pq" // регистрируем PostgreSQL драйвер
)
func main() {
// Получаем список зарегистрированных драйверов
drivers := sql.Drivers()
fmt.Println("Зарегистрированные драйверы:")
for _, driver := range drivers {
fmt.Println("-", driver)
}
// Выведет что-то вроде:
// Зарегистрированные драйверы:
// - mysql
// - postgres
}
func Register(name string, driver driver.Driver)
Эта функция регистрирует драйвер базы данных под указанным именем. Если попытаться зарегистрировать два драйвера с одинаковым именем или передать nil, функция вызовет panic.
Обычно драйверы регистрируют себя автоматически при импорте с пустым идентификатором _, как в примере выше. Но можно регистрировать драйверы и вручную:
package main
import (
"database/sql"
"database/sql/driver"
"fmt"
)
// Простой mock-драйвер для примера
type mockDriver struct{}
func (d *mockDriver) Open(name string) (driver.Conn, error) {
fmt.Println("Mock driver открывает соединение с:", name)
return nil, nil
}
func main() {
// Регистрируем наш mock-драйвер
sql.Register("mock", &mockDriver{})
// Теперь можем его использовать
db, err := sql.Open("mock", "test-connection")
if err != nil {
fmt.Println("Ошибка:", err)
return
}
defer db.Close()
// Проверяем, что драйвер зарегистрирован
fmt.Println("Зарегистрированные драйверы:", sql.Drivers())
}
Основные функции для работы с БД
func Open(driverName, dataSourceName string) (*DB, error)
Открывает новое соединение с базой данных. На самом деле не устанавливает соединение сразу, а только готовит объект DB.
Пример:
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
func (*DB) Ping() error и func (*DB) PingContext(ctx context.Context) error
Проверяет, что соединение с БД живо и доступно.
Пример:
err = db.Ping()
if err != nil {
log.Fatal("Не удалось подключиться к БД:", err)
}
fmt.Println("Успешное подключение к БД!")
Функции выполнения запросов
func (*DB) Exec(query string, args ...interface{}) (Result, error)
func (*DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error)
Выполняет запрос без возврата строк (INSERT, UPDATE, DELETE).
Пример:
result, err := db.Exec(
"INSERT INTO users(name, age) VALUES (?, ?)",
"Alice",
30,
)
if err != nil {
log.Fatal(err)
}
lastID, err := result.LastInsertId()
rowsAffected, err := result.RowsAffected()
func (*DB) Query(query string, args ...interface{}) (*Rows, error)
func (*DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)
Выполняет запрос, возвращающий строки (SELECT).
Пример:
rows, err := db.Query("SELECT id, name, age FROM users WHERE age > ?", 25)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
var age int
err = rows.Scan(&id, &name, &age)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s, %d\n", id, name, age)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
func (*DB) QueryRow(query string, args ...interface{}) *Row
func (*DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row
Выполняет запрос, который должен вернуть не более одной строки.
Пример:
var name string
var age int
err := db.QueryRow("SELECT name, age FROM users WHERE id = ?", 1).Scan(&name, &age)
if err != nil {
if err == sql.ErrNoRows {
fmt.Println("Пользователь не найден")
} else {
log.Fatal(err)
}
} else {
fmt.Printf("Имя: %s, Возраст: %d\n", name, age)
}
Функции для работы с транзакциями
func (*DB) Begin() (*Tx, error)
func (*DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)
Начинает новую транзакцию.
Пример:
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// Откатываем транзакцию в случае ошибки
defer func() {
if err != nil {
tx.Rollback()
}
}()
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
return err
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err != nil {
return err
}
// Если все успешно - коммитим
err = tx.Commit()
if err != nil {
return err
}
Работа с соединениями
func (*DB) Conn(ctx context.Context) (*Conn, error)
Получает одно соединение из пула для выполнения нескольких операций в одном контексте.
Пример:
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Выполняем несколько операций в одном соединении
var count int
err = conn.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
log.Fatal(err)
}
_, err = conn.ExecContext(context.Background(), "UPDATE stats SET user_count = ?", count)
if err != nil {
log.Fatal(err)
}
Заключение
Пакет database/sql предоставляет все необходимые функции для работы с SQL-базами данных в Go. Основные принципы:
- Всегда проверяйте ошибки
- Закрывайте ресурсы (Rows, Tx, Conn) с помощью defer
- Используйте контексты для управления таймаутами
- Для разных типов запросов используйте соответствующие методы (Exec, Query, QueryRow)
Правильное использование этих функций позволит вам создавать надежные и эффективные приложения, работающие с базами данных.
4.6.3 - Контекст (context) в транзакциях database/sql
Контекст (context.Context) - это механизм в Go для управления временем жизни операций, отмены и передачи значений между вызовами функций. В работе с базой данных через database/sql контекст играет ключевую роль.
Основные цели использования контекста в транзакциях
- Отмена операций - можно прервать долгий запрос
- Таймауты - установка максимального времени выполнения
- Распространение значений - передача метаданных через цепочку вызовов
Методы с поддержкой контекста
В database/sql большинство операций имеют две версии:
- Обычная (
Begin, Exec, Query и т.д.)
- С контекстом (
BeginTx, ExecContext, QueryContext и т.д.)
Пример использования контекста с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Начинаем транзакцию с таймаутом
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
defer tx.Rollback() // Безопасный откат при ошибках
// Выполняем запрос в транзакции
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
// Если контекст истек, получим context.DeadlineExceeded
log.Printf("Update failed: %v", err)
return
}
// Коммитим транзакцию
err = tx.Commit()
if err != nil {
log.Printf("Commit failed: %v", err)
}
Особенности работы контекста в транзакциях
- Каскадная отмена - отмена контекста прерывает все операции в транзакции
- Изоляция транзакций - контекст не влияет на другие транзакции
- Ресурсы - отмена не освобождает соединение автоматически
Практические сценарии использования
- HTTP-обработчики - привязка к времени жизни запроса:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tx, err := db.BeginTx(ctx, nil)
// ...
}
- Долгие отчеты - возможность отмены:
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(10*time.Second)
cancel() // Принудительная отмена через 10 сек
}()
rows, err := db.QueryContext(ctx, "SELECT * FROM big_table")
- Распределенные транзакции - передача идентификаторов:
ctx := context.WithValue(context.Background(), "txID", generateID())
tx, _ := db.BeginTx(ctx, nil)
Важные нюансы
- Всегда проверяйте ошибки на
context.Canceled и context.DeadlineExceeded
- Освобождайте ресурсы с помощью
defer даже при отмене контекста
- Не передавайте один контекст в несколько независимых транзакций
Использование контекста делает ваши транзакции более управляемыми и устойчивыми к долгим операциям.
4.6.4 - Описание типа database/sql DB
DB - это хэндл базы данных, представляющий пул из нуля или более базовых соединений. Он безопасен для одновременного использования несколькими горутинами.
type DB struct {
// содержит отфильтрованные или неэкспонированные поля
}
Пакет sql создает и освобождает соединения автоматически; он также поддерживает свободный пул незадействованных соединений. Если в базе данных есть понятие состояния каждого соединения, то такое состояние можно надежно наблюдать в рамках транзакции (Tx) или соединения (Conn). После вызова DB.Begin возвращаемая Tx привязывается к одному соединению. После вызова Tx.Commit или Tx.Rollback для транзакции, соединение этой транзакции возвращается в пул незанятых соединений DB. Размер пула можно контролировать с помощью DB.SetMaxIdleConns.
func Open
func Open(driverName, dataSourceName string) (*DB, error)
Open открывает базу данных, указанную именем драйвера базы данных и именем источника данных, обычно состоящим как минимум из имени базы данных и информации о подключении.
Большинство пользователей открывают базу данных с помощью специфической для драйвера вспомогательной функции подключения, которая возвращает *DB. Драйверы баз данных не включены в стандартную библиотеку Go. Список драйверов сторонних разработчиков см. на https://golang.org/s/sqldrivers.
Open может просто проверить свои аргументы, не создавая соединения с базой данных. Чтобы убедиться, что имя источника данных действительно, вызовите DB.Ping.
Возвращаемая БД безопасна для одновременного использования несколькими горутинами и поддерживает собственный пул незанятых соединений. Таким образом, функцию Open следует вызывать только один раз. Редко возникает необходимость закрывать БД.
func OpenDB
func OpenDB(c driver.Connector) *DB
OpenDB открывает базу данных с помощью driver.Connector, позволяя драйверам обходить строковое имя источника данных.
Большинство пользователей открывают базу данных с помощью специфической для драйвера функции-помощника подключения, которая возвращает *DB. Драйверы баз данных не включены в стандартную библиотеку Go.
OpenDB может просто проверить свои аргументы, не создавая соединения с базой данных. Чтобы убедиться, что имя источника данных действительно, вызовите DB.Ping.
Возвращаемая БД безопасна для одновременного использования несколькими горутинами и поддерживает свой собственный пул незанятых соединений. Таким образом, функция OpenDB должна быть вызвана только один раз. Редко возникает необходимость закрывать БД.
func (*DB) Close
func (db *DB) Close() error
Close закрывает базу данных и предотвращает запуск новых запросов. Затем Close ожидает завершения всех запросов, которые начали обрабатываться на сервере.
Закрывать БД приходится редко, так как хэндл БД должен быть долгоживущим и использоваться совместно многими горутинами.
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close() // Всегда закрывайте соединение
func (*DB) Ping
func (db *DB) Ping() error
Ping проверяет, что соединение с базой данных еще живо, и при необходимости устанавливает соединение.
Внутри Ping использует context.Background; чтобы указать контекст, используйте DB.PingContext.
err = db.Ping()
if err != nil {
log.Fatal("Connection failed:", err)
}
func (*DB) PingContext
func (db *DB) PingContext(ctx context.Context) error
PingContext проверяет, что соединение с базой данных еще живо, и при необходимости устанавливает соединение.
Пример
package main
import (
"context"
"database/sql"
"log"
"time"
)
var (
ctx context.Context
db *sql.DB
)
func main() {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
status := "up"
if err := db.PingContext(ctx); err != nil {
status = "down"
}
log.Println(status)
}
func (*DB) Begin
func (db *DB) Begin() (*Tx, error)
Begin начинает транзакцию. Уровень изоляции по умолчанию зависит от драйвера.
Begin внутренне использует context.Background; чтобы указать контекст, используйте DB.BeginTx.
func (*DB) BeginTx
func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)
BeginTx начинает транзакцию.
Предоставленный контекст используется до тех пор, пока транзакция не будет зафиксирована или откачена. Если контекст будет отменен, пакет sql откатит транзакцию. Tx.Commit вернет ошибку, если контекст, предоставленный BeginTx, будет отменен.
Предоставленный параметр TxOptions является необязательным и может быть равен nil, если следует использовать значения по умолчанию. Если используется уровень изоляции не по умолчанию, который драйвер не поддерживает, будет возвращена ошибка.
Пример
import (
"context"
"database/sql"
"log"
)
var (
ctx context.Context
db *sql.DB
)
func main() {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal(err)
}
id := 37
_, execErr := tx.Exec(`UPDATE users SET status = ? WHERE id = ?`, "paid", id)
if execErr != nil {
_ = tx.Rollback()
log.Fatal(execErr)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
}
func (*DB) Conn
func (db *DB) Conn(ctx context.Context) (*Conn, error)
Conn возвращает одно соединение, либо открывая новое соединение, либо возвращая существующее соединение из пула соединений. Conn будет блокироваться до тех пор, пока не будет возвращено соединение или не будет отменен ctx. Запросы, выполняемые по одному и тому же Conn, будут выполняться в одной и той же сессии базы данных.
Каждый Conn должен быть возвращен в пул баз данных после использования вызовом Conn.Close.
Подробнее о Conn
Работа с соединениями (Conn) в database/sql
Соединение (Conn) представляет собой одиночное подключение к базе данных, которое можно использовать как в уже открытом пуле соединений (DB), так и для прямого управления подключением.
Основные концепции
-
Отличие от DB:
DB - это пул соединений (управляет множеством подключений)
Conn - одно конкретное соединение
-
Когда использовать:
- Когда нужно выполнить несколько операций в рамках одного соединения
- Для работы с сессионными переменными
- Для транзакций, требующих изоляции от других операций
Примеры использования
1. Получение соединения из пула
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Получаем отдельное соединение из пула
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close() // Важно возвращать соединение в пул
// Используем соединение
var result int
err = conn.QueryRowContext(context.Background(), "SELECT 1 + 1").Scan(&result)
if err != nil {
log.Fatal(err)
}
fmt.Println("Result:", result)
}
2. Использование сессионных переменных
// Устанавливаем переменную сессии и используем её в запросе
err = conn.ExecContext(context.Background(), "SET @user_id = 123").Scan(&result)
if err != nil {
log.Fatal(err)
}
var userID int
err = conn.QueryRowContext(context.Background(), "SELECT @user_id").Scan(&userID)
fmt.Println("User ID:", userID) // Выведет: User ID: 123
3. Транзакции в отдельном соединении
// Начинаем транзакцию в этом соединении
tx, err := conn.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal(err)
}
// Выполняем операции в транзакции
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err != nil {
tx.Rollback()
log.Fatal(err)
} else {
tx.Commit()
}
4. Прямое подключение (без пула)
Для прямого подключения без пула можно использовать драйвер напрямую, но это не рекомендуется:
package main
import (
"database/sql"
"database/sql/driver"
"fmt"
"log"
)
func main() {
// Получаем драйвер по имени
driver := sql.Driver(new(mysql.MySQLDriver))
// Создаем прямое соединение (не рекомендуется для production)
conn, err := driver.Open("user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer conn.Close() // Закрываем соединение
// Преобразуем в тип Conn (если нужно)
if sqlConn, ok := conn.(driver.Conn); ok {
// Работаем с соединением...
}
}
Когда использовать Conn вместо DB?
- Сессионные переменные: Когда нужно сохранить состояние между запросами
- Временные таблицы: Которые видны только в текущем соединении
- LOCK TABLES: Когда нужно заблокировать таблицы на время сессии
- Точный контроль: Когда важно, чтобы несколько операций использовали одно соединение
Важные замечания
- Всегда закрывайте
Conn с помощью defer conn.Close()
- Не держите соединения долго - возвращайте их в пул
- Для большинства случаев достаточно стандартного
DB
Conn дороже в создании, чем операции через DB
Пример комплексного использования:
func complexOperation(db *sql.DB) error {
// Получаем соединение
conn, err := db.Conn(context.Background())
if err != nil {
return err
}
defer conn.Close()
// Настраиваем сессию
if _, err := conn.ExecContext(context.Background(), "SET @start_time = NOW()"); err != nil {
return err
}
// Выполняем транзакцию
tx, err := conn.BeginTx(context.Background(), nil)
if err != nil {
return err
}
// Бизнес-логика...
if _, err := tx.Exec("INSERT INTO orders (...) VALUES (...)"); err != nil {
tx.Rollback()
return err
}
// Получаем время выполнения
var startTime string
conn.QueryRowContext(context.Background(), "SELECT @start_time").Scan(&startTime)
fmt.Println("Operation started at:", startTime)
return tx.Commit()
}
Используйте Conn осознанно, только когда действительно нужно контролировать конкретное соединение. В большинстве случаев достаточно стандартного DB.
func (*DB) Driver
func (db *DB) Driver() driver.Driver
Driver возвращает базовый драйвер базы данных.
func (*DB) Exec
func (db *DB) Exec(query string, args ...any) (Result, error)
Exec выполняет запрос, не возвращая ни одной строки. В качестве args используются любые параметры-заполнители в запросе.
Exec использует внутренний контекст context.Background; чтобы указать контекст, используйте DB.ExecContext.
context.Background
func Background() Context
Background возвращает ненулевой, пустой Context. Он никогда не отменяется, не имеет значений и сроков. Обычно используется главной функцией, инициализацией и тестами, а также в качестве контекста верхнего уровня для входящих запросов.
func (*DB) ExecContext
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
ExecContext выполняет запрос, не возвращая ни одной строки. В качестве args используются любые параметры-заполнители в запросе.
func (*DB) Prepare
func (db *DB) Prepare(query string) (*Stmt, error)
Prepare создает подготовленный отчет для последующих запросов или выполнения. Из полученного отчета можно одновременно выполнять несколько запросов или операций. Вызывающая сторона должна вызвать метод *Stmt.Close оператора, когда он больше не нужен.
Prepare внутренне использует context.Background; чтобы указать контекст, используйте DB.PrepareContext.
Пример
import (
"context"
"database/sql"
"log"
)
var db *sql.DB
func main() {
projects := []struct {
mascot string
release int
}{
{"tux", 1991},
{"duke", 1996},
{"gopher", 2009},
{"moby dock", 2013},
}
stmt, err := db.Prepare("INSERT INTO projects(id, mascot, release, category) VALUES( ?, ?, ?, ? )")
if err != nil {
log.Fatal(err)
}
defer stmt.Close() // Prepared statements take up server resources and should be closed after use.
for id, project := range projects {
if _, err := stmt.Exec(id+1, project.mascot, project.release, "open source"); err != nil {
log.Fatal(err)
}
}
}
func (*DB) PrepareContext
func (db *DB) PrepareContext(ctx context.Context, query string) (*Stmt, error)
PrepareContext создает подготовленный запрос для последующих запросов или выполнений. Из полученного отчета можно одновременно выполнять несколько запросов или операций. Вызывающая сторона должна вызвать метод *Stmt.Close оператора, когда он больше не нужен.
Предоставленный контекст используется для подготовки утверждения, а не для его выполнения.
func (*DB) Query
func (db *DB) Query(query string, args ...any) (*Rows, error)
Query выполняет запрос, возвращающий строки, обычно SELECT. В качестве args используются любые параметры-заполнители в запросе.
Query внутренне использует context.Background; чтобы указать контекст, используйте DB.QueryContext.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/lib/pq" // или другой драйвер БД
)
var db *sql.DB
func main() {
// Инициализация подключения к БД
var err error
db, err = sql.Open("postgres", "user=postgres dbname=test sslmode=disable") // замените на свои параметры
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Проверка соединения
if err := db.Ping(); err != nil {
log.Fatal("Failed to ping database:", err)
}
age := 27
q := `
-- Создаем временную таблицу
CREATE TEMP TABLE uid (id bigint) ON COMMIT DROP;
-- Заполняем временную таблицу
INSERT INTO uid
SELECT id FROM users WHERE age < $1;
-- Первый набор результатов
SELECT
users.id, name
FROM
users
JOIN uid ON users.id = uid.id;
-- Второй набор результатов
SELECT
ur.user_id, ur.role_id
FROM
user_roles AS ur
JOIN uid ON uid.id = ur.user_id;
`
// Используем контекст для запроса
ctx := context.Background()
rows, err := db.QueryContext(ctx, q, age)
if err != nil {
log.Fatal("Query failed:", err)
}
defer rows.Close()
// Обрабатываем первый набор результатов
log.Println("Processing users:")
for rows.Next() {
var (
id int64
name string
)
if err := rows.Scan(&id, &name); err != nil {
log.Fatal("Failed to scan user row:", err)
}
log.Printf("id %d name is %s\n", id, name)
}
// Переходим ко второму набору результатов
if !rows.NextResultSet() {
if err := rows.Err(); err != nil {
log.Fatal("Failed to get next result set:", err)
}
log.Fatal("Expected more result sets, but none available")
}
// Обрабатываем второй набор результатов
roleMap := map[int64]string{
1: "user",
2: "admin",
3: "gopher",
}
log.Println("\nProcessing roles:")
for rows.Next() {
var (
userID int64
roleID int64
)
if err := rows.Scan(&userID, &roleID); err != nil {
log.Fatal("Failed to scan role row:", err)
}
roleName, ok := roleMap[roleID]
if !ok {
roleName = "unknown"
}
log.Printf("user %d has role %s\n", userID, roleName)
}
if err := rows.Err(); err != nil {
log.Fatal("Rows error:", err)
}
}
func (*DB) QueryContext
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
QueryContext выполняет запрос, возвращающий строки, обычно SELECT. В качестве args используются любые параметры-заполнители запроса.
Пример
package main
import (
"context"
"database/sql"
"fmt"
"log"
"strings"
_ "github.com/go-sql-driver/mysql" // или другой драйвер БД
)
func main() {
// 1. Инициализация подключения к БД
var err error
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// 2. Проверка соединения
ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
log.Fatal("Failed to ping database:", err)
}
// 3. Выполнение запроса
age := 27
rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)
if err != nil {
log.Fatal("Query failed:", err)
}
defer rows.Close()
// 4. Обработка результатов
names := make([]string, 0)
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
log.Printf("Scan error (skipping row): %v", err) // Не прерываем выполнение
continue
}
names = append(names, name)
}
// 5. Проверка ошибок после итерации
if err := rows.Err(); err != nil {
log.Fatal("Rows error:", err)
}
// 6. Проверка ошибок закрытия (более аккуратная обработка)
if err := rows.Close(); err != nil {
log.Printf("Warning: error closing rows: %v", err) // Не фатальная ошибка
}
// 7. Вывод результатов
if len(names) > 0 {
fmt.Printf("%s are %d years old\n", strings.Join(names, ", "), age)
} else {
fmt.Printf("No users found at age %d\n", age)
}
}
func (*DB) QueryRow
func (db *DB) QueryRow(query string, args ...any) *Row
QueryRow выполняет запрос, который, как ожидается, вернет не более одной строки. QueryRow всегда возвращает значение, не являющееся нулем. Ошибки откладываются до вызова метода Row’s Scan. Если запрос не выбрал ни одной строки, то *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
QueryRow внутренне использует context.Background;
func (*DB) QueryRowContext
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
QueryRowContext выполняет запрос, который, как ожидается, вернет не более одной строки. QueryRowContext всегда возвращает значение non-nil. Ошибки откладываются до вызова метода Row’s Scan. Если запрос не выбрал ни одной строки, то *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
Пример
package main
import (
"context"
"database/sql"
"log"
"time"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)
func main() {
// Инициализация подключения к БД
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
// Проверка соединения
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
// Выполнение запроса
id := 123
var username string
var created time.Time
// Используем контекст с таймаутом для запроса
queryCtx, queryCancel := context.WithTimeout(context.Background(), 3*time.Second)
defer queryCancel()
err = db.QueryRowContext(queryCtx,
"SELECT username, created_at FROM users WHERE id=?",
id,
).Scan(&username, &created)
switch {
case err == sql.ErrNoRows:
log.Printf("No user found with id %d", id)
case err != nil:
log.Printf("Query error: %v", err) // Не используем Fatalf чтобы не прерывать программу
default:
log.Printf(
"Username: %q, Account created on: %s",
username,
created.Format("2006-01-02 15:04:05"),
)
}
}
func (*DB) SetConnMaxIdleTime
func (db *DB) SetConnMaxIdleTime(d time.Duration)
SetConnMaxIdleTime устанавливает максимальное время, в течение которого соединение может простаивать.
Просроченные соединения могут быть лениво закрыты перед повторным использованием.
Если d <= 0, соединения не закрываются из-за времени простоя соединения.
func (*DB) SetConnMaxLifetime
func (db *DB) SetConnMaxLifetime(d time.Duration)
SetConnMaxLifetime устанавливает максимальное количество времени, в течение которого соединение может быть использовано повторно.
Просроченные соединения могут быть лениво закрыты перед повторным использованием.
Если d <= 0, соединения не закрываются из-за возраста соединения.
func (*DB) SetMaxIdleConns
func (db *DB) SetMaxIdleConns(n int)
SetMaxIdleConns устанавливает максимальное количество соединений в пуле незадействованных соединений.
Если MaxOpenConns больше 0, но меньше нового MaxIdleConns, то новый MaxIdleConns будет уменьшен, чтобы соответствовать лимиту MaxOpenConns.
Если n <= 0, простаивающие соединения не сохраняются.
По умолчанию максимальное количество незадействованных соединений в настоящее время равно 2. Это может измениться в будущем выпуске.
func (*DB) SetMaxOpenConns
func (db *DB) SetMaxOpenConns(n int)
SetMaxOpenConns устанавливает максимальное количество открытых соединений с базой данных.
Если MaxIdleConns больше 0, а новое значение MaxOpenConns меньше MaxIdleConns, то MaxIdleConns будет уменьшено, чтобы соответствовать новому ограничению MaxOpenConns.
Если n <= 0, то количество открытых соединений не ограничено. По умолчанию 0 (неограниченно).
func (*DB) Stats
func (db *DB) Stats() DBStats
Stats возвращает статистику базы данных.
4.6.5 - Описание типа database/sql ColumnType
ColumnType тип для управлени структурой базы данных
type ColumnType struct {
// содержит отфильтрованные или неэкспортированные поля
}
ColumnType содержит имя и тип колонки.
func (*ColumnType) DatabaseTypeName
func (ci *ColumnType) DatabaseTypeName() string
DatabaseTypeName возвращает системное имя базы данных типа столбца. Если возвращается пустая строка, значит, имя типа драйвером не поддерживается. Список типов данных драйвера см. в документации к драйверу.
Спецификаторы ColumnType.Length не учитываются. Общие имена типов включают “VARCHAR”, “TEXT”, “NVARCHAR”, “DECIMAL”, “BOOL”, “INT” и “BIGINT”.
func (*ColumnType) DecimalSize
func (ci *ColumnType) DecimalSize() (precision, scale int64, ok bool)
DecimalSize возвращает масштаб и точность десятичного типа. Если они не применяются или не поддерживаются, ok равно false.
func (*ColumnType) Length
func (ci *ColumnType) Length() (length int64, ok bool)
Length возвращает длину типа столбца для типов столбцов переменной длины, таких как текстовые и бинарные типы полей. Если длина типа не ограничена, то значение будет math.MaxInt64 (все ограничения базы данных будут действовать). Если тип столбца не переменной длины, например int, или если он не поддерживается драйвером, ok будет false.
func (*ColumnType) Name
func (ci *ColumnType) Name() string
Name возвращает имя или псевдоним колонки.
func (*ColumnType) Nullable
func (ci *ColumnType) Nullable() (nullable, ok bool)
Nullable сообщает, может ли столбец быть нулевым. Если драйвер не поддерживает это свойство, ok будет равно false.
func (*ColumnType) ScanType
func (ci *ColumnType) ScanType() reflect.Type
ScanType возвращает тип Go, подходящий для сканирования с помощью Rows.Scan. Если драйвер не поддерживает это свойство, ScanType вернет тип пустого интерфейса.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// Подключение к базе данных
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/testdb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверка соединения
if err := db.Ping(); err != nil {
log.Fatal(err)
}
// Выполнение запроса
rows, err := db.QueryContext(context.Background(),
"SELECT id, username, created_at, balance, is_active FROM users LIMIT 1")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Получение информации о столбцах
columnTypes, err := rows.ColumnTypes()
if err != nil {
log.Fatal(err)
}
// Анализ метаданных каждого столбца
for i, ct := range columnTypes {
fmt.Printf("\n--- Столбец %d: %s ---\n", i+1, ct.Name())
// Тип данных в БД
databaseType := ct.DatabaseTypeName()
fmt.Printf("Тип в БД: %s\n", databaseType)
// Nullable
nullable, ok := ct.Nullable()
if ok {
fmt.Printf("Может быть NULL: %v\n", nullable)
} else {
fmt.Println("Информация о NULL недоступна")
}
// Длина/размер
length, ok := ct.Length()
if ok {
fmt.Printf("Длина: %d\n", length)
}
// Точность и масштаб (для чисел)
precision, scale, ok := ct.DecimalSize()
if ok {
fmt.Printf("Точность: %d, Масштаб: %d\n", precision, scale)
}
// Тип сканирования в Go
scanType := ct.ScanType()
fmt.Printf("Тип в Go: %v\n", scanType)
}
// Пример использования информации о типах для динамического сканирования
var (
id int64
username string
createdAt time.Time
balance float64
isActive bool
)
if rows.Next() {
err = rows.Scan(&id, &username, &createdAt, &balance, &isActive)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nРеальные значения: %d, %s, %v, %.2f, %t\n",
id, username, createdAt, balance, isActive)
}
}
4.6.6 - Описание типа Conn database/sql
Conn представляет собой одно соединение с базой данных, а не пул соединений с базой данных.
type Conn struct {
// содержит отфильтрованные или неэкспонированные поля
}
Conn представляет собой одно соединение с базой данных, а не пул соединений с базой данных. Предпочтительнее запускать запросы из БД, если нет особой необходимости в постоянном соединении с одной базой данных.
Conn должен вызвать Conn.Close, чтобы вернуть соединение в пул баз данных, и может делать это одновременно с выполняющимся запросом.
После вызова Conn.Close все операции над соединением завершаются с ошибкой ErrConnDone.
func (*Conn) BeginTx
func (c *Conn) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)
BeginTx начинает транзакцию.
Предоставленный контекст используется до тех пор, пока транзакция не будет зафиксирована или откачена. Если контекст будет отменен, пакет sql откатит транзакцию. Tx.Commit вернет ошибку, если контекст, предоставленный BeginTx, будет отменен.
Предоставленный параметр TxOptions является необязательным и может быть равен nil, если следует использовать значения по умолчанию. Если используется уровень изоляции не по умолчанию, который драйвер не поддерживает, будет возвращена ошибка.
func (*Conn) Close
func (c *Conn) Close() error
Close возвращает соединение в пул соединений. Все операции после Close возвращаются с ошибкой ErrConnDone. Close безопасно вызывать одновременно с другими операциями, и он будет блокироваться до тех пор, пока все остальные операции не завершатся. Может оказаться полезным сначала отменить любой используемый контекст, а затем вызвать Close непосредственно после него.
func (*Conn) ExecContext
func (c *Conn) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
ExecContext выполняет запрос, не возвращая никаких строк. В качестве args используются любые параметры-заполнители в запросе.
Пример
package main
import (
"context"
"database/sql"
"log"
"os"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)
func main() {
// 1. Инициализация подключения к БД
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// 2. Проверка соединения
ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
log.Fatal("Failed to ping database:", err)
}
// 3. Получение выделенного соединения
conn, err := db.Conn(ctx)
if err != nil {
log.Fatal("Failed to get connection:", err)
}
defer func() {
if err := conn.Close(); err != nil {
log.Printf("Warning: error closing connection: %v", err)
}
}()
// 4. Выполнение транзакции
id := 41
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// 5. Выполнение UPDATE в транзакции
result, err := tx.ExecContext(ctx,
`UPDATE balances SET balance = balance + 10 WHERE user_id = ?`,
id,
)
if err != nil {
tx.Rollback()
log.Fatal("Failed to update balance:", err)
}
// 6. Проверка количества измененных строк
rows, err := result.RowsAffected()
if err != nil {
tx.Rollback()
log.Fatal("Failed to get rows affected:", err)
}
if rows != 1 {
tx.Rollback()
log.Printf("Expected single row affected, got %d rows affected", rows)
os.Exit(1) // Более мягкое завершение чем log.Fatal
}
// 7. Фиксация транзакции
if err := tx.Commit(); err != nil {
log.Fatal("Failed to commit transaction:", err)
}
log.Println("Balance updated successfully")
}
func (*Conn) PingContext
func (c *Conn) PingContext(ctx context.Context) error
PingContext проверяет, что соединение с базой данных все еще живо.
func (*Conn) PrepareContext
func (c *Conn) PrepareContext(ctx context.Context, query string) (*Stmt, error)
PrepareContext создает подготовленный запрос для последующих запросов или выполнений. Несколько запросов или выполнений могут быть запущены одновременно из полученного утверждения. Вызывающая сторона должна вызвать метод *Stmt.Close оператора, когда он больше не нужен.
Предоставленный контекст используется для подготовки утверждения, а не для его выполнения.
func (*Conn) QueryContext
func (c *Conn) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
QueryContext выполняет запрос, возвращающий строки, обычно SELECT. В args указываются любые параметры-заполнители запроса.
func (*Conn) QueryRowContext
func (c *Conn) QueryRowContext(ctx context.Context, query string, args ...any) *Row
QueryRowContext выполняет запрос, который, как ожидается, вернет не более одной строки. QueryRowContext всегда возвращает значение non-nil. Ошибки откладываются до вызова метода *Row.Scan. Если запрос не выбрал ни одной строки, то *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
func (*Conn) Raw
func (c *Conn) Raw(f func(driverConn any) error) (err error)
Raw выполняет f, раскрывая базовое соединение драйвера на время выполнения f. DriverConn не должен использоваться вне f.
После того как f вернется и err не будет driver.ErrBadConn, Conn будет продолжать использоваться до вызова Conn.Close.
Разбираем метод Raw() для работы с низкоуровневыми соединениями
Метод Raw() в database/sql позволяет получить прямое доступ к драйвер-специфичному соединению, минуя абстракции пакета database/sql. Это нужно для использования специфичных функций драйвера, которые не доступны через стандартный API.
Что такое f в этом контексте?
f - это ваша функция-колбэк, которая получает доступ к нативному соединению драйвера. Она должна иметь сигнатуру:
func(driverConn any) error
Зачем это нужно?
- Использование специфичных функций драйвера (например, PostgreSQL-специфичные команды)
- Оптимизация производительности для критичных участков кода
- Работа с нестандартными возможностями БД
Простой пример (PostgreSQL)
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=test sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Используем Raw для доступа к нативному соединению PostgreSQL
err = conn.Raw(func(driverConn interface{}) error {
// Приводим к типу, который ожидаем (для PostgreSQL)
pgConn, ok := driverConn.(driver.Conn) // Здесь нужен ваш тип соединения
if !ok {
return fmt.Errorf("unexpected driver connection type")
}
// Пример: выполняем LISTEN для PostgreSQL-нотификаций
// Это специфичная функция PostgreSQL, недоступная через стандартный API
_, err := pgConn.(*pq.Conn).Exec("LISTEN channel_name")
return err
})
if err != nil {
log.Fatal(err)
}
}
Более практичный пример (MySQL)
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Получаем низкоуровневое соединение MySQL
err = conn.Raw(func(driverConn interface{}) error {
// Приводим к ожидаемому типу соединения MySQL
mysqlConn, ok := driverConn.(*mysql.Conn) // Требуется import драйвера
if !ok {
return fmt.Errorf("expected mysql.Conn, got %T", driverConn)
}
// Используем специфичные методы MySQL
fmt.Println("Server version:", mysqlConn.GetServerVersion())
fmt.Println("Connection ID:", mysqlConn.GetConnectionID())
// Можно выполнить специфичные команды
return mysqlConn.Ping()
})
if err != nil {
log.Fatal(err)
}
}
Важные предупреждения:
-
Тип соединения зависит от драйвера:
- PostgreSQL:
*pq.Conn
- MySQL:
*mysql.Conn
- SQLite: зависит от используемого драйвера
-
Безопасность:
- Не сохраняйте соединение вне функции
f
- Все ошибки должны быть обработаны
-
Совместимость:
- Код становится зависимым от конкретного драйвера
- Может сломаться при смене драйвера
Когда стоит использовать Raw()?
- Когда вам нужен доступ к специфичным функциям СУБД
- Для оптимизации критических участков
- При работе с нестандартными возможностями (нотификации, специфичные команды)
В большинстве случаев стандартного API database/sql достаточно, и Raw() не понадобится.
4.6.7 - Описание типа DBStats database/sql
DBStats DBStats содержит статистику базы данных. В этом разделе представлены другие типы: IsolationLevel, NamedArg, Null, NullBool
type DBStats
type DBStats struct {
MaxOpenConnections int // Максимальное количество открытых соединений с базой данных.
// Состояние пула
OpenConnections int // Количество установленных соединений, как используемых, так и простаивающих.
InUse int // Количество соединений, используемых в данный момент.
Idle int // Количество простаивающих соединений.
// Счетчики
WaitCount int64 // Общее количество ожидающих соединений.
WaitDuration time.Duration // Общее время, заблокированное в ожидании нового соединения.
MaxIdleClosed int64 // Общее количество соединений, закрытых из-за SetMaxIdleConns.
MaxIdleTimeClosed int64 // Общее количество соединений, закрытых из-за SetConnMaxIdleTime.
MaxLifetimeClosed int64 // Общее количество соединений, закрытых из-за SetConnMaxLifetime.
}
DBStats содержит статистику базы данных.
type IsolationLevel
IsolationLevel - это уровень изоляции транзакции, используемый в TxOptions.
const (
LevelDefault IsolationLevel = iota
LevelReadUncommitted
LevelReadCommitted
LevelWriteCommitted
LevelRepeatableRead
LevelSnapshot
LevelSerializable
LevelLinearizable
)
Различные уровни изоляции, которые драйверы могут поддерживать в DB.BeginTx. Если драйвер не поддерживает заданный уровень изоляции, может быть возвращена ошибка.
См. https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels.
func (IsolationLevel) String
func (i IsolationLevel) String() string
String возвращает имя уровня изоляции транзакции.
type NamedArg
type NamedArg struct {
// Name - имя параметра-заместителя.
//
// Если пусто, то будет использоваться
// порядковая позиция в списке аргументов.
//
// Имя не должно содержать префикса символа.
Name string
// Value - это значение параметра.
// Ему могут быть присвоены те же типы значений, что и аргументам запроса
//.
Value any
// содержит отфильтрованные или неэкспонированные поля
}
NamedArg - это именованный аргумент. Значения NamedArg могут использоваться в качестве аргументов DB.Query или DB.Exec и связываться с соответствующим именованным параметром в операторе SQL.
Для более краткого способа создания значений NamedArg см. функцию Named.
func Named
func Named(name string, value any) NamedArg
Named предоставляет более лаконичный способ создания значений NamedArg.
Пример
db.ExecContext(ctx, `
delete from Invoice
where
TimeCreated < @end
and TimeCreated >= @start;`,
sql.Named("start", startTime),
sql.Named("end", endTime),
)
type Null
type Null[T any] struct {
V T
Valid bool
}
Null представляет значение, которое может быть нулевым. Null реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования:
var s Null[string]
err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s)
...
if s.Valid {
// используем s.V
} else {
// NULL значение
}
T должен быть одним из типов, принимаемых driver.Value.
func (*Null[T]) Scan
func (n *Null[T]) Scan(value any) error
func (Null[T]) Value
func (n Null[T]) Value() (driver.Value, error)
type NullBool ¶
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}
NullBool представляет bool, который может быть нулевым. NullBool реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullBool) Scan
func (n *NullBool) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullBool) Value
func (n NullBool) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullByte
type NullByte struct {
Byte byte
Valid bool // Valid is true if Byte is not NULL
}
NullByte представляет байт, который может быть нулевым. NullByte реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullByte) Scan
func (n *NullByte) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullByte) Value
func (n NullByte) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullFloat64 ¶
type NullFloat64 struct {
Float64 float64
Valid bool // Valid is true if Float64 is not NULL
}
NullFloat64 представляет float64, который может быть нулевым. NullFloat64 реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullFloat64) Scan ¶
func (n *NullFloat64) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullFloat64) Value ¶
func (n NullFloat64) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullInt16
type NullInt16 struct {
Int16 int16
Valid bool // Valid is true if Int16 is not NULL
}
NullInt16 представляет int16, который может быть нулевым. NullInt16 реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullInt16) Scan
func (n *NullInt16) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullInt16) Value
func (n NullInt16) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullInt32
type NullInt32 struct {
Int32 int32
Valid bool // Valid is true if Int32 is not NULL
}
NullInt32 представляет int32, который может быть нулевым. NullInt32 реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullInt32) Scan
func (n *NullInt32) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullInt32) Value
func (n NullInt32) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullInt64
type NullInt64 struct {
Int64 int64
Valid bool // Valid is true if Int64 is not NULL
}
NullInt64 представляет int64, который может быть нулевым. NullInt64 реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullInt64) Scan
func (n *NullInt64) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullInt64) Value
func (n NullInt64) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullString
type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}
NullString представляет строку, которая может быть нулевой. NullString реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования:
var s NullString
err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s)
...
if s.Valid {
// использовать s.String
} else {
// NULL значение
}
func (*NullString) Scan
func (ns *NullString) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullString) Value
func (ns NullString) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullTime
type NullTime struct {
Time.Time
Valid bool // Valid is true if Time is not NULL
}
NullTime представляет время time.Time, которое может быть нулевым. NullTime реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullTime) Scan
func (n *NullTime) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullTime) Value
func (n NullTime) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
Практическое использование Null-типов в Go
Null-типы (NullString, NullInt64, NullTime и др.) нужны для работы с NULL-значениями из базы данных. Разберём на реальных примерах.
Интерфейсы
- Scanner - позволяет сканировать (читать) значение из БД
- driver.Valuer - позволяет преобразовывать значение для записи в БД
Полный пример с NullString и NullTime
Пример
package main
import (
"database/sql"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
type UserProfile struct {
ID int64
Name sql.NullString
Bio sql.NullString
DeletedAt sql.NullTime
}
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 1. Запись данных с NULL-значениями
profile := UserProfile{
Name: sql.NullString{String: "Иван Иванов", Valid: true},
Bio: sql.NullString{}, // NULL
DeletedAt: sql.NullTime{Time: time.Now(), Valid: false}, // Будет записан как NULL
}
_, err = db.Exec(
"INSERT INTO users (name, bio, deleted_at) VALUES (?, ?, ?)",
profile.Name,
profile.Bio,
profile.DeletedAt,
)
if err != nil {
log.Fatal(err)
}
// 2. Чтение данных с возможными NULL-значениями
var user UserProfile
err = db.QueryRow(`
SELECT id, name, bio, deleted_at
FROM users
WHERE id = ?
`, 1).Scan(
&user.ID,
&user.Name,
&user.Bio,
&user.DeletedAt,
)
if err != nil {
log.Fatal(err)
}
// 3. Обработка NULL-значений
log.Println("ID:", user.ID)
if user.Name.Valid {
log.Println("Name:", user.Name.String)
} else {
log.Println("Name not set")
}
if user.Bio.Valid {
log.Println("Bio:", user.Bio.String)
} else {
log.Println("Bio not set")
}
if user.DeletedAt.Valid {
log.Println("Deleted at:", user.DeletedAt.Time)
} else {
log.Println("Not deleted")
}
}
Как работают Null-типы?
1. Scan (чтение из БД)
Пример
func (ns *NullString) Scan(value interface{}) error {
if value == nil {
ns.String, ns.Valid = "", false
return nil
}
ns.Valid = true
return convertAssign(&ns.String, value)
}
2. Value (запись в БД)
Пример
func (ns NullString) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.String, nil
}
Пример с пользовательским Null-типом
Создадим свой NullStatus для enum-поля:
Пример
type NullStatus struct {
Status string
Valid bool
}
func (ns *NullStatus) Scan(value interface{}) error {
if value == nil {
ns.Status, ns.Valid = "", false
return nil
}
ns.Valid = true
switch v := value.(type) {
case []byte:
ns.Status = string(v)
case string:
ns.Status = v
default:
return fmt.Errorf("unsupported type: %T", value)
}
return nil
}
func (ns NullStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.Status, nil
}
Когда использовать Null-типы?
- Когда поле в БД может быть NULL
- Когда нужно отличать “нулевое значение” от “неустановленного”
- При работе с опциональными полями
Альтернативы
В новых версиях Go можно использовать указатели:
Пример
var name *string
err := db.QueryRow("SELECT name FROM users WHERE id = 1").Scan(&name)
if name != nil {
// есть значение
}
Но Null-типы предоставляют более удобный интерфейс и явный флаг Valid.
4.6.8 - Описание типа Out database/sql
Out может использоваться для извлечения параметров OUTPUT из хранимых процедур. В этом разделе представлены другие типы: RawBytes, Result
type Out
type Out struct {
// Dest — указатель на значение, которое будет установлено в качестве результата
// параметра OUTPUT хранящейся процедуры.
Dest any
// In — является ли параметр параметром INOUT. Если да, то входное значение для хранящейся
// процедуры — это разыменованное значение указателя Dest, которое затем заменяется
// выходным значением.
In bool
// содержит отфильтрованные или неэкспортированные поля
}
Out может использоваться для извлечения параметров OUTPUT из хранимых процедур.
Не все драйверы и базы данных поддерживают параметры OUTPUT.
Пример использования:
var outArg string
_, err := db.ExecContext(ctx, «ProcName», sql.Named(«Arg1», sql.Out{Dest: &outArg}))
type RawBytes
RawBytes — это байтовый срез, который содержит ссылку на память, принадлежащую самой базе данных. После выполнения Rows.Scan в RawBytes срез остается действительным только до следующего вызова Rows.Next, Rows.Scan или Rows.Close.
RawBytes - это специальный тип в пакете database/sql, определённый как []byte, который используется для сканирования данных из базы данных без копирования памяти. Это может быть полезно для оптимизации производительности при работе с большими бинарными данными.
Основные принципы работы с RawBytes
- Временное владение памятью: RawBytes содержит ссылку на память, управляемую драйвером БД, а не на копию данных.
- Ограниченное время жизни: Данные в RawBytes действительны только до следующего вызова методов:
- Небезопасность: Если сохранить RawBytes и попытаться использовать после вышеуказанных вызовов, это приведёт к неопределённому поведению.
Пример 1: Базовое сканирование
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query("SELECT blob_column FROM my_table WHERE id = ?", 1)
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var raw sql.RawBytes
if err := rows.Scan(&raw); err != nil {
panic(err)
}
// Используем raw сразу, пока он действителен
fmt.Printf("Data: %s\n", raw)
// Если нужно сохранить данные, нужно сделать копию:
dataCopy := make([]byte, len(raw))
copy(dataCopy, raw)
// Теперь dataCopy можно использовать после rows.Next()
}
}
Пример 2: Сканирование нескольких столбцов
func scanMultipleColumns(db *sql.DB) error {
rows, err := db.Query("SELECT id, name, data FROM documents")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int
var name string
var data sql.RawBytes
if err := rows.Scan(&id, &name, &data); err != nil {
return err
}
fmt.Printf("ID: %d, Name: %s, Data length: %d\n", id, name, len(data))
// Обработка данных должна быть здесь
processData(data)
}
return rows.Err()
}
func processData(data []byte) {
// Обработка данных
}
Пример 3: Опасное использование (неправильное)
func incorrectUsage(db *sql.DB) {
rows, err := db.Query("SELECT data FROM large_blobs")
if err != nil {
panic(err)
}
defer rows.Close()
var allData [][]byte
for rows.Next() {
var raw sql.RawBytes
if err := rows.Scan(&raw); err != nil {
panic(err)
}
// ОШИБКА: сохраняем RawBytes, который станет недействительным
allData = append(allData, raw)
}
// Здесь allData содержит недействительные данные!
for _, data := range allData {
fmt.Println(data) // Может привести к панике или неверным данным
}
}
Когда использовать RawBytes
- Для больших бинарных данных, когда копирование нежелательно
- Для временной обработки данных, которые не нужно сохранять
- Когда производительность критична, и вы готовы следить за временем жизни данных
Альтернативы
Если нужно сохранить данные:
var raw sql.RawBytes
var data []byte
rows.Scan(&raw)
data = make([]byte, len(raw))
copy(data, raw)
// Теперь data можно использовать в любом месте
type Result ¶
type Result интерфейс {
// LastInsertId возвращает целое число, сгенерированное базой данных
// в ответ на команду. Обычно это будет из
// столбца «автоинкремент» при вставке новой строки. Не все
// базы данных поддерживают эту функцию, и синтаксис таких
// операторов варьируется.
LastInsertId() (int64, error)
// RowsAffected возвращает количество строк, затронутых
// обновлением, вставкой или удалением. Не все базы данных или драйверы баз данных
// драйвер базы данных может поддерживать эту функцию.
RowsAffected() (int64, error)
}
Результат обобщает выполненную команду SQL.
4.6.9 - Описание типа Row database/sql
Row — результат вызова DB.QueryRow для выбора одной строки. В этом разделе представлены другие типы: Rows, Scanner
type Row
type Row struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Row — результат вызова DB.QueryRow для выбора одной строки.
func (*Row) Err
func (r *Row) Err() error
Err предоставляет способ для обертывающих пакетов проверять ошибки запроса без вызова Row.Scan. Err возвращает ошибку, если таковая имела место при выполнении запроса. Если эта ошибка не равна nil, она также будет возвращена из Row.Scan.
func (*Row) Scan
func (r *Row) Scan(dest ...any) error
Scan копирует столбцы из соответствующей строки в значения, на которые указывает dest. Подробности см. в документации по Rows.Scan. Если запросу соответствует более одной строки, Scan использует первую строку и отбрасывает остальные. Если ни одна строка не соответствует запросу, Scan возвращает ErrNoRows.
type Rows
type Rows struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Rows — это результат запроса. Его курсор начинается перед первой строкой набора результатов. Используйте Rows.Next для перехода от строки к строке.
Пример
package main
import (
"context"
"database/sql"
"log"
"strings"
)
// Глобальные переменные лучше инициализировать
var (
ctx = context.Background() // Инициализация контекста
db *sql.DB
)
func main() {
// На практике db должен быть инициализирован перед использованием
// Например:
// var err error
// db, err = sql.Open("driver-name", "datasource")
// if err != nil {
// log.Fatal(err)
// }
// defer db.Close()
age := 27
// Добавляем проверку, что db не nil
if db == nil {
log.Fatal("database connection is not initialized")
}
// Используем QueryContext вместо Query для явного указания контекста
rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
names := make([]string, 0)
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
log.Fatal(err)
}
names = append(names, name)
}
// Проверяем ошибки после итерации
if err := rows.Err(); err != nil {
log.Fatal(err)
}
// Добавляем проверку на пустой результат
if len(names) == 0 {
log.Printf("No users found with age %d", age)
return
}
log.Printf("%s are %d years old", strings.Join(names, ", "), age)
}
func (*Rows) Close
func (rs *Rows) Close() error
Close закрывает Rows, предотвращая дальнейшее перечисление. Если Rows.Next вызывается и возвращает false, а дальнейших наборов результатов нет, Rows закрывается автоматически, и достаточно проверить результат Rows.Err. Close является идемпотентным и не влияет на результат Rows.Err.
func (*Rows) ColumnTypes
func (rs *Rows) ColumnTypes() ([]*ColumnType, error)
ColumnTypes возвращает информацию о столбцах, такую как тип столбца, длина и возможность принятия нулевого значения. Некоторая информация может быть недоступна из некоторых драйверов.
func (*Rows) Columns
func (rs *Rows) Columns() ([]string, error)
Columns возвращает имена столбцов. Columns возвращает ошибку, если строки закрыты.
func (*Rows) Err
func (rs *Rows) Err() error
Err возвращает ошибку, если таковая возникла во время итерации. Err может быть вызван после явного или неявного Rows.Close.
func (*Rows) Next
func (rs *Rows) Next() bool
Next подготавливает следующую строку результата для чтения с помощью метода Rows.Scan. Он возвращает true в случае успеха или false, если следующей строки результата нет или при ее подготовке произошла ошибка. Для различения этих двух случаев следует обратиться к Rows.Err.
Каждому вызову Rows.Scan, даже первому, должен предшествовать вызов Rows.Next.
Функция Rows.Next() используется для итерации по строкам результата SQL-запроса.
Пример
Базовый пример использования Rows.Next()
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// Подключение к базе данных
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Выполнение запроса
rows, err := db.Query("SELECT id, name, email FROM users WHERE active = ?", true)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Итерация по строкам результата
for rows.Next() {
var id int
var name, email string
// Сканирование значений из текущей строки
if err := rows.Scan(&id, &name, &email); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s, Email: %s\n", id, name, email)
}
// Проверка ошибок после итерации
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
Почему используется цикл for?
Цикл for rows.Next() выполняет итерации по всем строкам результата запроса:
-
Количество итераций равно количеству строк, возвращенных запросом.
-
Механизм работы:
- При первом вызове
rows.Next() перемещает курсор к первой строке результата
- При последующих вызовах перемещает курсор к следующей строке
- Когда строки заканчиваются, возвращает
false и цикл завершается
-
Важно: Каждый вызов rows.Scan() должен быть предварен вызовом rows.Next()
Детализированный пример с обработкой ошибок
func getActiveUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name, email FROM users WHERE active = ?", true)
if err != nil {
return nil, fmt.Errorf("query failed: %v", err)
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
// Можно продолжить обработку других строк или прервать
return nil, fmt.Errorf("scan failed: %v", err)
}
users = append(users, u)
}
// Проверяем ошибки, которые могли возникнуть во время итерации
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration failed: %v", err)
}
return users, nil
}
type User struct {
ID int
Name string
Email string
}
Что происходит внутри цикла?
-
Первая итерация:
rows.Next() перемещает курсор к первой строке (если она есть)
- Возвращает
true, если строка доступна
- Выполняется
rows.Scan()
-
Последующие итерации:
rows.Next() перемещает курсор к следующей строке
- Цикл продолжается, пока есть строки
-
Завершение:
- Когда строки заканчиваются,
rows.Next() возвращает false
- Цикл прерывается
- Проверяется
rows.Err() на наличие ошибок
Важные нюансы:
- Всегда вызывайте
defer rows.Close() для освобождения ресурсов
- Проверяйте
rows.Err() после цикла для выявления ошибок итерации
- Не используйте
rows.Next() без последующего rows.Scan()
- Для пустых результатов цикл не выполнится ни разу
Альтернативный пример с обработкой пустого результата
func printUserCount(db *sql.DB) {
rows, err := db.Query("SELECT COUNT(*) FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// rows.Next() вернет true, даже если только одна строка
if rows.Next() {
var count int
if err := rows.Scan(&count); err != nil {
log.Fatal(err)
}
fmt.Printf("Total users: %d\n", count)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
Этот пример показывает, что даже для запросов, возвращающих одну строку (как COUNT), необходимо использовать rows.Next() перед rows.Scan().
func (*Rows) NextResultSet
func (rs *Rows) NextResultSet() bool
NextResultSet подготавливает следующий набор результатов для чтения. Он сообщает, есть ли дальнейшие наборы результатов, или false, если дальнейших наборов результатов нет или если произошла ошибка при переходе к ним. Для различения этих двух случаев следует обратиться к методу Rows.Err.
После вызова NextResultSet перед сканированием всегда следует вызывать метод Rows.Next. Если есть дальнейшие наборы результатов, они могут не содержать строк в наборе результатов.
Функция NextResultSet() используется, когда запрос к базе данных возвращает несколько наборов результатов (например, при выполнении хранимых процедур или пакетных запросов).
Пример
Пример с несколькими наборами результатов
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера MySQL
)
func main() {
// Инициализация подключения к базе данных
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Запрос, возвращающий несколько наборов результатов
// (например, хранимая процедура с несколькими SELECT)
query := `
SELECT id, name FROM users WHERE active = 1;
SELECT id, title FROM products WHERE price > 100;
SELECT COUNT(*) FROM orders WHERE status = 'completed';
`
// Выполнение запроса
rows, err := db.QueryContext(context.Background(), query)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Обработка первого набора результатов (пользователи)
fmt.Println("=== Active Users ===")
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s\n", id, name)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
// Переход к следующему набору результатов (продукты)
if rows.NextResultSet() {
fmt.Println("\n=== Expensive Products ===")
for rows.Next() {
var id int
var title string
if err := rows.Scan(&id, &title); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Title: %s\n", id, title)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
// Переход к следующему набору результатов (количество заказов)
if rows.NextResultSet() {
fmt.Println("\n=== Completed Orders Count ===")
for rows.Next() {
var count int
if err := rows.Scan(&count); err != nil {
log.Fatal(err)
}
fmt.Printf("Completed orders: %d\n", count)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
// Проверяем, есть ли еще наборы результатов
if rows.NextResultSet() {
fmt.Println("\nThere are more result sets available")
} else {
fmt.Println("\nNo more result sets")
}
}
Ключевые моменты работы с NextResultSet():
-
Порядок обработки: Всегда сначала обрабатывайте текущий набор результатов с помощью Next() и Scan(), прежде чем переходить к следующему.
-
Проверка наличия результатов: NextResultSet() возвращает true, если есть следующий набор результатов, даже если он пустой.
-
Обработка ошибок: После NextResultSet() всегда проверяйте rows.Err() на наличие ошибок.
-
Пустые наборы: Набор результатов может не содержать строк - это нормально, просто Next() вернет false сразу.
Альтернативный пример с хранимой процедурой
func callMultiResultStoredProcedure(db *sql.DB) error {
// Вызов хранимой процедуры, возвращающей несколько наборов результатов
rows, err := db.Query("CALL get_report_data()")
if err != nil {
return err
}
defer rows.Close()
// Обработка всех наборов результатов
resultSetIndex := 0
for {
// Обработка текущего набора результатов
for rows.Next() {
// В зависимости от набора результатов используем разную логику сканирования
switch resultSetIndex {
case 0:
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
return err
}
fmt.Printf("User: %d - %s\n", id, name)
case 1:
var product string
var price float64
if err := rows.Scan(&product, &price); err != nil {
return err
}
fmt.Printf("Product: %s - $%.2f\n", product, price)
// Можно добавить дополнительные case для других наборов
}
}
if err := rows.Err(); err != nil {
return err
}
// Переход к следующему набору результатов
if !rows.NextResultSet() {
break
}
resultSetIndex++
}
return nil
}
Важные предупреждения:
- Не все драйверы баз данных поддерживают несколько наборов результатов.
- Всегда проверяйте
rows.Err() после NextResultSet().
- После перехода к новому набору результатов обязательно вызывайте
rows.Next() перед сканированием.
- Структура данных в каждом наборе результатов может отличаться.
func (*Rows) Scan
func (rs *Rows) Scan(dest ...any) error
Scan копирует столбцы в текущей строке в значения, на которые указывает dest. Количество значений в dest должно совпадать с количеством столбцов в Rows.
Scan преобразует столбцы, прочитанные из базы данных, в следующие общие типы Go и специальные типы, предоставляемые пакетом sql:
*string
*[]byte
*int, *int8, *int16, *int32, *int64
*uint, *uint8, *uint16, *uint32, *uint64
*bool
*float32, *float64
*interface{}
*RawBytes
*Rows (cursor value)
любой тип, реализующий Scanner (см. документацию по Scanner)
В самом простом случае, если тип значения из исходного столбца является целым, bool или строковым типом T, а dest имеет тип *T, Scan просто присваивает значение через указатель.
Scan также преобразует строковые и числовые типы, если при этом не теряется информация. В то время как Scan преобразует все числа, сканированные из числовых столбцов базы данных, в *string, сканирование в числовые типы проверяется на переполнение. Например, float64 со значением 300 или строка со значением «300» могут быть отсканированы в uint16, но не в uint8, хотя float64(255) или «255» могут быть отсканированы в uint8. Исключением является то, что при сканировании некоторых чисел float64 в строки может произойти потеря информации при преобразовании в строку. В общем случае, сканируйте столбцы с плавающей запятой в *float64.
Если аргумент dest имеет тип *[]byte, Scan сохраняет в этом аргументе копию соответствующих данных. Копия принадлежит вызывающему и может быть изменена и храниться неограниченное время. Копирование можно избежать, используя вместо этого аргумент типа *RawBytes; ограничения на его использование см. в документации по RawBytes.
Если аргумент имеет тип *interface{}, Scan копирует значение, предоставленное базовым драйвером, без преобразования. При сканировании из исходного значения типа []byte в *interface{} создается копия среза, и вызывающая сторона владеет результатом.
Исходные значения типа time.Time могут быть отсканированы в значения типа *time.Time, *interface{}, *string или *[]byte. При преобразовании в два последних используется time.RFC3339Nano.
Источниковые значения типа bool могут быть просканированы в типы *bool, *interface{}, *string, *[]byte или *RawBytes.
Для сканирования в *bool источником могут быть true, false, 1, 0 или строковые входы, которые можно проанализировать с помощью strconv.ParseBool.
Scan также может преобразовать курсор, возвращенный из запроса, такого как «select cursor(select * from my_table) from dual», в значение *Rows, которое само по себе может быть просканировано. Родительский запрос select закроет любой курсор *Rows, если родительский *Rows закрыт.
Если любой из первых аргументов, реализующих Scanner, возвращает ошибку, эта ошибка будет обернута в возвращаемую ошибку.
type Scanner
type Scanner interface {
// Scan назначает значение из драйвера базы данных.
//
// Значение src будет одного из следующих типов:
//
// int64
// float64
// bool
// []byte
// string
// time.Time
// nil - для значений NULL
//
// Если значение не может быть сохранено
// без потери информации.
//
// Типы ссылок, такие как []byte, действительны только до следующего вызова Scan
// и не должны сохраняться. Их базовая память принадлежит драйверу.
// Если сохранение необходимо, скопируйте их значения перед следующим вызовом Scan.
Scan(src any) error
}
Scanner — интерфейс, используемый Rows.Scan.
4.6.10 - Описание типа Stmt database/sql
Stmt — это подготовленное выражение. Stmt безопасно для одновременного использования несколькими goroutines.
type Stmt
type Stmt struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Stmt — это подготовленное выражение. Stmt безопасно для одновременного использования несколькими goroutines.
Если Stmt подготовлен на Tx или Conn, он будет навсегда привязан к одному базовому соединению. Если Tx или Conn закрывается, Stmt станет непригодным для использования, и все операции будут возвращать ошибку. Если Stmt подготовлен на DB, он будет оставаться пригодным для использования в течение всего срока жизни DB. Когда Stmt необходимо выполнить на новом базовом соединении, он автоматически подготовится на новом соединении.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql" // Добавляем импорт драйвера
)
func main() {
// Инициализируем контекст
ctx := context.Background()
// Инициализируем соединение с базой данных (в реальном коде)
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверяем соединение
if err := db.PingContext(ctx); err != nil {
log.Fatal(err)
}
// Создаем подготовленное выражение
stmt, err := db.PrepareContext(ctx, "SELECT username FROM users WHERE id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// Выполняем запрос с параметром
id := 43
var username string
err = stmt.QueryRowContext(ctx, id).Scan(&username)
switch {
case err == sql.ErrNoRows:
log.Printf("no user with id %d", id) // Используем Printf вместо Fatalf чтобы не завершать программу
case err != nil:
log.Fatal(err)
default:
log.Printf("username is %s\n", username)
}
}
func (*Stmt) Close
func (s *Stmt) Close() error
Close закрывает оператор.
func (*Stmt) Exec
func (s *Stmt) Exec(args ...any) (Result, error)
Exec выполняет подготовленный оператор с заданными аргументами и возвращает Result, обобщающий результат оператора.
Exec использует context.Background внутренне; чтобы указать контекст, используйте Stmt.ExecContext.
func (*Stmt) ExecContext
func (s *Stmt) ExecContext(ctx context.Context, args ...any) (Result, error)
ExecContext выполняет подготовленное выражение с заданными аргументами и возвращает Result, обобщающий результат выражения.
func (*Stmt) Query
func (s *Stmt) Query(args ...any) (*Rows, error)
Query выполняет подготовленный запрос с заданными аргументами и возвращает результаты запроса в виде *Rows.
Query использует context.Background внутренне; для указания контекста используйте Stmt.QueryContext.
func (*Stmt) QueryContext ¶
func (s *Stmt) QueryContext(ctx context.Context, args ...any) (*Rows, error)
QueryContext выполняет подготовленное запросное выражение с заданными аргументами и возвращает результаты запроса в виде *Rows.
func (*Stmt) QueryRow
func (s *Stmt) QueryRow(args ...any) *Row
QueryRow выполняет подготовленный запрос с заданными аргументами. Если во время выполнения запроса происходит ошибка, она будет возвращена вызовом Scan на возвращенном *Row, который всегда не равен nil. Если запрос не выбирает никаких строк, *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
Пример использования:
var name string
err := nameByUseridStmt.QueryRow(id).Scan(&name)
QueryRow использует context.Background внутренне; для указания контекста используйте Stmt.QueryRowContext.
func (*Stmt) QueryRowContext
func (s *Stmt) QueryRowContext(ctx context.Context, args ...any) *Row
QueryRowContext выполняет подготовленный запрос с заданными аргументами. Если во время выполнения запроса происходит ошибка, она будет возвращена вызовом Scan на возвращенном *Row, который всегда не равен nil. Если запрос не выбирает никаких строк, *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql" // Не забудьте импортировать драйвер
)
func main() {
// 1. Инициализация контекста
ctx := context.Background()
// 2. Инициализация подключения к БД (пример для MySQL)
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// 3. Проверка соединения
if err := db.PingContext(ctx); err != nil {
log.Fatal("Failed to ping database:", err)
}
// 4. Создание подготовленного выражения
stmt, err := db.PrepareContext(ctx, "SELECT username FROM users WHERE id = ?")
if err != nil {
log.Fatal("Failed to prepare statement:", err)
}
defer stmt.Close()
// 5. Выполнение запроса
id := 43
var username string
err = stmt.QueryRowContext(ctx, id).Scan(&username)
// 6. Обработка результатов
switch {
case err == sql.ErrNoRows:
log.Printf("User with id %d not found", id) // Не фатальная ошибка
case err != nil:
log.Fatal("Query failed:", err)
default:
log.Printf("Found user: %s (ID: %d)", username, id)
}
}
4.6.11 - Описание типа Tx database/sql
Tx — это незавершенная транзакция базы данных.
type Tx
type Tx struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Tx — это незавершенная транзакция базы данных.
Транзакция должна заканчиваться вызовом Tx.Commit или Tx.Rollback.
После вызова Tx.Commit или Tx.Rollback все операции по транзакции завершаются с ошибкой ErrTxDone.
Инструкции, подготовленные для транзакции вызовом методов Tx.Prepare или Tx.Stmt транзакции, закрываются вызовом Tx.Commit или Tx.Rollback.
func (*Tx) Commit
func (tx *Tx) Commit() error
Commit фиксирует транзакцию.
func (*Tx) Exec
func (tx *Tx) Exec(query string, args ...any) (Result, error)
Exec выполняет запрос, который не возвращает строки. Например: INSERT и UPDATE.
Exec использует context.Background внутренне; для указания контекста используйте Tx.ExecContext.
func (*Tx) ExecContext
func (tx *Tx) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
ExecContext выполняет запрос, который не возвращает строки. Например: INSERT и UPDATE.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)
func main() {
// Инициализация подключения к БД
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Проверка соединения
ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
log.Fatal("Database connection failed:", err)
}
// Начало транзакции
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// Выполнение операций в транзакции
id := 37
_, execErr := tx.ExecContext(ctx, "UPDATE users SET status = ? WHERE id = ?", "paid", id)
if execErr != nil {
// Попытка отката при ошибке
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Printf("UPDATE failed: %v, rollback also failed: %v", execErr, rollbackErr)
} else {
log.Printf("UPDATE failed, transaction rolled back: %v", execErr)
}
return
}
// Фиксация транзакции
if err := tx.Commit(); err != nil {
log.Fatal("Failed to commit transaction:", err)
}
log.Println("Transaction completed successfully")
}
func (*Tx) Prepare
func (tx *Tx) Prepare(query string) (*Stmt, error)
Prepare создает подготовленное выражение для использования в транзакции.
Возвращаемое выражение работает в рамках транзакции и будет закрыто после фиксации или отката транзакции.
Чтобы использовать существующее подготовленное выражение в этой транзакции, см. Tx.Stmt.
Prepare использует context.Background внутренне; чтобы указать контекст, используйте Tx.PrepareContext.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)
func main() {
// Инициализация подключения к БД
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname?parseTime=true")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Проверка соединения
if err := db.Ping(); err != nil {
log.Fatal("Database connection failed:", err)
}
projects := []struct {
mascot string
release int
}{
{"tux", 1991},
{"duke", 1996},
{"gopher", 2009},
{"moby dock", 2013},
}
// Начало транзакции
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// Откат в случае ошибки (игнорируется если был Commit)
defer func() {
if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
log.Printf("Warning: rollback failed: %v", err)
}
}()
// Подготовка выражения
stmt, err := tx.PrepareContext(ctx, "INSERT INTO projects(id, mascot, release, category) VALUES(?, ?, ?, ?)")
if err != nil {
log.Fatal("Failed to prepare statement:", err)
}
defer stmt.Close()
// Вставка данных
for id, project := range projects {
if _, err := stmt.ExecContext(ctx, id+1, project.mascot, project.release, "open source"); err != nil {
log.Fatal("Failed to insert project:", err)
}
}
// Фиксация транзакции
if err := tx.Commit(); err != nil {
log.Fatal("Failed to commit transaction:", err)
}
log.Println("Successfully inserted", len(projects), "projects")
}
Дополнительные рекомендации:
-
Параметры подключения:
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5*time.Minute)
-
Пакетная вставка:
Для большого количества данных рассмотрите возможность:
// Начало пакетной вставки
values := make([]interface{}, 0, len(projects)*4)
query := "INSERT INTO projects(id, mascot, release, category) VALUES"
for id, project := range projects {
if id > 0 {
query += ","
}
query += "(?, ?, ?, ?)"
values = append(values, id+1, project.mascot, project.release, "open source")
}
if _, err := tx.ExecContext(ctx, query, values...); err != nil {
log.Fatal(err)
}
-
Таймауты:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
-
Валидация данных:
Добавьте проверку данных перед вставкой:
if project.release < 1990 || project.release > time.Now().Year() {
log.Printf("Invalid release year for %s: %d", project.mascot, project.release)
continue
}
func (*Tx) PrepareContext
func (tx *Tx) PrepareContext(ctx context.Context, query string) (*Stmt, error)
PrepareContext создает подготовленное выражение для использования в транзакции.
Возвращенное выражение работает в рамках транзакции и будет закрыто после фиксации или отката транзакции.
Чтобы использовать существующее подготовленное выражение в этой транзакции, см. Tx.Stmt.
Предоставленный контекст будет использоваться для подготовки контекста, а не для выполнения возвращаемого оператора. Возвращаемый оператор будет выполняться в контексте транзакции.
func (*Tx) Query
func (tx *Tx) Query(query string, args ...any) (*Rows, error)
Query выполняет запрос, который возвращает строки, обычно SELECT.
Query использует context.Background внутренне; чтобы указать контекст, используйте Tx.QueryContext.
func (*Tx) QueryContext
func (tx *Tx) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
QueryContext выполняет запрос, который возвращает строки, обычно SELECT.
func (*Tx) QueryRow
func (tx *Tx) QueryRow(query string, args ...any) *Row
QueryRow выполняет запрос, который должен вернуть не более одной строки. QueryRow всегда возвращает значение, отличное от nil. Ошибки откладываются до вызова метода Scan Row. Если запрос не выбирает ни одной строки, *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
QueryRow использует context.Background внутренне; для указания контекста используйте Tx.QueryRowContext.
func (*Tx) QueryRowContext
func (tx *Tx) QueryRowContext(ctx context.Context, query string, args ...any) *Row
QueryRowContext выполняет запрос, который должен вернуть не более одной строки. QueryRowContext всегда возвращает значение, отличное от nil. Ошибки откладываются до вызова метода Scan Row. Если запрос не выбирает ни одной строки, *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
func (*Tx) Rollback
func (tx *Tx) Rollback() error
Rollback прерывает транзакцию.
Пример
код транзакции для обновления водителей и заказов
package main
import (
"context"
"database/sql"
"log"
_ "github.com/lib/pq" // Импорт драйвера PostgreSQL
)
func main() {
// Инициализация подключения к БД
connStr := "user=postgres dbname=mydb sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Проверка соединения
if err := db.Ping(); err != nil {
log.Fatal("Database connection failed:", err)
}
// Создаем контекст с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Начало транзакции
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// Гарантированный откат при ошибках
defer func() {
if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
log.Printf("Warning: rollback error: %v", err)
}
}()
id := 53
// 1. Обновление статуса водителя
_, err = tx.ExecContext(ctx, "UPDATE drivers SET status = $1 WHERE id = $2", "assigned", id)
if err != nil {
log.Printf("Failed to update driver status: %v", err)
return
}
// 2. Назначение водителя на заказы
_, err = tx.ExecContext(ctx, "UPDATE pickups SET driver_id = $1 WHERE driver_id IS NULL", id)
if err != nil {
log.Printf("Failed to assign driver to pickups: %v", err)
return
}
// Фиксация транзакции
if err := tx.Commit(); err != nil {
log.Fatal("Failed to commit transaction:", err)
}
log.Printf("Successfully assigned driver %d and updated pickup orders", id)
}
Дополнительные улучшения:
-
Безопасность UPDATE:
// Ограничиваем обновление только незанятыми заказами
"UPDATE pickups SET driver_id = $1 WHERE driver_id IS NULL"
-
Проверка результата:
res, err := tx.ExecContext(...)
if rowsAffected, _ := res.RowsAffected(); rowsAffected == 0 {
log.Printf("Warning: no rows were updated")
}
-
Повторные попытки:
maxRetries := 3
for i := 0; i < maxRetries; i++ {
// ... выполнение транзакции ...
if err == nil {
break
}
if shouldRetry(err) {
continue
}
break
}
-
Connection Pool:
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5*time.Minute)
func (*Tx) Stmt
func (tx *Tx) Stmt(stmt *Stmt) *Stmt
Stmt возвращает подготовленное заявление, специфичное для транзакции, из существующего заявления.
Пример:
updateMoney, err := db.Prepare(«UPDATE balance SET money=money+? WHERE id=?»)
...
tx, err := db.Begin()...
res, err := tx.Stmt(updateMoney).Exec(123.45, 98293203)
Возвращенное выражение работает в рамках транзакции и будет закрыто после фиксации или отката транзакции.
Stmt использует context.Background внутри всегда; для указания контекста используйте Tx.StmtContext.
func (*Tx) StmtContext
func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt
StmtContext возвращает подготовленное выражение, специфичное для транзакции, из существующего выражения.
Пример:
updateMoney, err := db.Prepare(«UPDATE balance SET money=money+? WHERE id=?»)
…
tx, err := db.Begin()…
res, err := tx.StmtContext(ctx, updateMoney).Exec(123.45, 98293203)
Предоставленный контекст используется для подготовки оператора, а не для его выполнения.
Возвращенный оператор работает в рамках транзакции и будет закрыт после фиксации или отката транзакции.
type TxOptions
type TxOptions struct {
// Isolation — уровень изоляции транзакции.
// Если равен нулю, используется уровень по умолчанию драйвера или базы данных.
Isolation IsolationLevel
ReadOnly bool
}
TxOptions содержит параметры транзакции, которые будут использоваться в DB.BeginTx.
4.7 - Описание пакета flag языка программирования Go
Пакет flag реализует разбор флагов командной строки.
Использование
Определите флаги с помощью flag.String, Bool, Int и т. д.
Это объявляет целочисленный флаг -n, хранящийся в указателе nFlag, с типом *int:
import «flag»
var nFlag = flag.Int("n", 1234, "сообщение справки для флага n")
При желании можно привязать флаг к переменной с помощью функций Var().
var flagvar int
func init() {
flag.IntVar(&flagvar, "flagname", 1234, "сообщение справки для flagname")
}
Или можно создать пользовательские флаги, которые удовлетворяют интерфейсу Value (с указателями-приемниками), и связать их с разбором флагов с помощью
flag.Var(&flagVal, "name", "сообщение справки для flagname")
Для таких флагов значением по умолчанию является просто начальное значение переменной.
После определения всех флагов вызовите
чтобы проанализировать командную строку в определенные флаги.
Затем флаги можно использовать напрямую. Если вы используете сами флаги, все они являются указателями; если вы связываете их с переменными, они являются значениями.
fmt.Println(«ip имеет значение », *ip)
fmt.Println(«flagvar имеет значение », flagvar)
После разбора аргументы, следующие за флагами, доступны как срез flag.Args или по отдельности как flag.Arg(i). Аргументы индексируются от 0 до flag.NArg-1.
Синтаксис флагов командной строки ¶
Допускаются следующие формы:
-flag
--flag // также допускаются двойные тире
-flag=x
-flag x // только небулевые флаги
Можно использовать один или два тире; они эквивалентны. Последняя форма не допускается для булевых флагов, поскольку значение команды
где * — подстановочный знак оболочки Unix, изменится, если есть файл с именем 0, false и т. д. Для отключения булевого флага необходимо использовать форму -flag=false.
Разбор флагов прекращается непосредственно перед первым аргументом, не являющимся флагом («-» является аргументом, не являющимся флагом), или после терминатора «–».
- Целочисленные флаги принимают значения
1234, 0664, 0x1234 и могут быть отрицательными.
- Булевы флаги могут быть:
1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False
Флаги продолжительности принимают любой ввод, допустимый для time.ParseDuration.
Набор флагов командной строки по умолчанию управляется функциями верхнего уровня. Тип FlagSet позволяет определять независимые наборы флагов, например для реализации подкоманд в интерфейсе командной строки. Методы FlagSet аналогичны функциям верхнего уровня для набора флагов командной строки.
Пример
package main
import (
"errors"
"flag"
"fmt"
"os"
"strings"
"time"
)
// Пример 1: Флаг строки с именем "species" и значением по умолчанию "gopher"
var species = flag.String("species", "gopher", "the species we are studying")
// Пример 2: Два флага (длинный и короткий) с общей переменной
var gopherType string
func init() {
const (
defaultGopher = "pocket"
usage = "the variety of gopher"
)
flag.StringVar(&gopherType, "gopher_type", defaultGopher, usage)
flag.StringVar(&gopherType, "g", defaultGopher, usage+" (shorthand)")
}
// Пример 3: Пользовательский тип флага - срез интервалов времени
type interval []time.Duration
// String реализует интерфейс flag.Value
func (i *interval) String() string {
if len(*i) == 0 {
return ""
}
var b strings.Builder
for idx, d := range *i {
if idx > 0 {
b.WriteString(", ")
}
b.WriteString(d.String())
}
return b.String()
}
// Set реализует интерфейс flag.Value
func (i *interval) Set(value string) error {
// Разрешаем множественные установки флага для накопления значений
for _, dt := range strings.Split(value, ",") {
duration, err := time.ParseDuration(strings.TrimSpace(dt))
if err != nil {
return fmt.Errorf("invalid duration %q: %v", dt, err)
}
*i = append(*i, duration)
}
return nil
}
var intervalFlag interval
func init() {
flag.Var(&intervalFlag, "deltaT", "comma-separated list of intervals to use between events")
}
func main() {
// Парсим флаги
flag.Parse()
// Выводим значения флагов
fmt.Println("Species:", *species)
fmt.Println("Gopher type:", gopherType)
fmt.Println("Intervals:", intervalFlag.String())
// Пример использования
if len(intervalFlag) == 0 {
fmt.Println("No intervals specified. Use -deltaT flag")
os.Exit(1)
}
// Демонстрация работы с интервалами
for i, d := range intervalFlag {
fmt.Printf("Event %d will happen after %v\n", i+1, d)
time.Sleep(d) // В реальном коде здесь была бы полезная работа
}
}
Как использовать:
- Сборка:
- Примеры запуска:
# Простой запуск
./app -species=rabbit -gopher_type=arctic -deltaT=1s,2s,3s
# Использование короткого флага
./app -g=arctic -deltaT=500ms,1s
# Просмотр помощи
./app -help
Переменные
var ErrHelp = errors.New("flag: help requested")
ErrHelp - это ошибка, возвращаемая при вызове флага -help или -h, но такой флаг не определен.
var Usage = func() {
fmt.Fprintf(CommandLine.Output(), "Usage of %s:\n", os.Args[0])
PrintDefaults()
}
Usage печатает сообщение об использовании всех определенных флагов командной строки в выходной файл CommandLine, который по умолчанию является os.Stderr.
Функция вызывается при возникновении ошибки во время разбора флагов.
Функция - это переменная, которая может быть изменена для указания на пользовательскую функцию. По умолчанию она печатает простой заголовок и вызывает PrintDefaults; подробности о формате вывода и о том, как им управлять, см. в документации к PrintDefaults.
Пользовательские функции могут выбрать выход из программы; по умолчанию выход происходит в любом случае, поскольку стратегия обработки ошибок в командной строке установлена на ExitOnError.
4.7.1 - Функции пакета flag
Основные функции пакета flag
func Arg
Arg возвращает i-й аргумент командной строки. Arg(0) — это первый аргумент, оставшийся после обработки флагов. Arg возвращает пустую строку, если запрошенный элемент не существует.
func Args
Args возвращает аргументы командной строки, не являющиеся флагами.
func Bool
func Bool(name string, value bool, usage string) *bool
Bool определяет флаг bool с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной bool, в которой хранится значение флага.
func BoolFunc
добавлено в go1.21.0
func BoolFunc(name, usage string, fn func(string) error)
BoolFunc определяет флаг с указанным именем и строкой использования без требования значений. Каждый раз, когда флаг встречается, вызывается fn со значением флага. Если fn возвращает ошибку, отличную от nil, она будет рассматриваться как ошибка разбора значения флага.
Пример
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Создаем новый набор флагов
fs := flag.NewFlagSet("ExampleBoolFunc", flag.ContinueOnError)
fs.SetOutput(os.Stdout) // Выводим ошибки в stdout
// Регистрируем BoolFunc флаг
fs.BoolFunc("log", "logs a dummy message (default: false)", func(s string) error {
if s == "" {
// Без значения - просто флаг присутствует
fmt.Println("dummy message: flag is set")
return nil
}
// С значением - выводим его
fmt.Printf("dummy message: %s\n", s)
return nil
})
// Пример 1: Флаг без значения
fmt.Println("\nCase 1: -log (no value)")
if err := fs.Parse([]string{"-log"}); err != nil {
fmt.Println("Error:", err)
}
// Пример 2: Флаг с произвольным значением
fmt.Println("\nCase 2: -log=0")
if err := fs.Parse([]string{"-log=0"}); err != nil {
fmt.Println("Error:", err)
}
// Пример 3: Неизвестный флаг
fmt.Println("\nCase 3: -unknown")
if err := fs.Parse([]string{"-unknown"}); err != nil {
fmt.Println("Error:", err)
}
// Пример 4: Вывод помощи
fmt.Println("\nCase 4: -help")
fs.PrintDefaults()
}
Запуск программы:
go run main.go -log -log=hello
Результат:
Case 1: -log (no value)
dummy message: flag is set
Case 2: -log=0
dummy message: 0
Case 3: -unknown
Error: flag provided but not defined: -unknown
Case 4: -help
-log
logs a dummy message (default: false)
func BoolVar
func BoolVar(p *bool, name string, value bool, usage string)
BoolVar определяет флаг bool с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную bool, в которой будет храниться значение флага.
func Duration
func Duration(name string, value time.Duration, usage string) *time.Duration
Duration определяет флаг time.Duration с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной time.Duration, в которой хранится значение флага. Флаг принимает значение, допустимое для time.ParseDuration.
func DurationVar
func DurationVar(p *time.Duration, name string, value time.Duration, usage string)
DurationVar определяет флаг time.Duration с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную time.Duration, в которой хранится значение флага. Флаг принимает значение, допустимое для time.ParseDuration.
func Float64
func Float64(name string, value float64, usage string) *float64
Float64 определяет флаг float64 с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной float64, в которой хранится значение флага.
func Float64Var
func Float64Var(p *float64, name string, value float64, usage string)
Float64Var определяет флаг float64 с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную float64, в которой хранится значение флага.
func Float64Var
func Float64Var(p *float64, name string, value float64, usage string)
Float64Var определяет флаг float64 с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную float64, в которой будет храниться значение флага.
func Func
func Func(name, usage string, fn func(string) error)
Func определяет флаг с указанным именем и строкой использования. Каждый раз, когда флаг встречается, вызывается функция fn со значением флага. Если fn возвращает ошибку, отличную от nil, это будет расценено как ошибка разбора значения флага.
Пример
package main
import (
"errors"
"flag"
"fmt"
"net"
"os"
)
func main() {
// Создаем новый набор флагов
fs := flag.NewFlagSet("ExampleFunc", flag.ContinueOnError)
fs.SetOutput(os.Stdout)
// Переменная для хранения IP
var ip net.IP
// Регистрируем функцию-парсер для IP
fs.Func("ip", "`IP address` to parse (e.g. 192.168.1.1 or ::1)", func(s string) error {
parsedIP := net.ParseIP(s)
if parsedIP == nil {
return fmt.Errorf("invalid IP address format: %q", s)
}
ip = parsedIP
return nil
})
// Тест 1: Валидный IPv4 адрес
fmt.Println("Test case 1: Valid IPv4 address")
if err := fs.Parse([]string{"-ip", "127.0.0.1"}); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Parsed IP: %v\n", ip)
fmt.Printf("Is loopback: %t\n\n", ip.IsLoopback())
}
// Тест 2: Невалидный IP адрес
fmt.Println("Test case 2: Invalid IP address")
if err := fs.Parse([]string{"-ip", "256.0.0.1"}); err != nil {
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Printf("Parsed IP: %v\n", ip)
}
// Тест 3: Валидный IPv6 адрес
fmt.Println("Test case 3: Valid IPv6 address")
if err := fs.Parse([]string{"-ip", "::1"}); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Parsed IP: %v\n", ip)
fmt.Printf("Is loopback: %t\n\n", ip.IsLoopback())
}
// Вывод справки
fmt.Println("Help message:")
fs.PrintDefaults()
}
Вывод программы:
Test case 1: Valid IPv4 address
Parsed IP: 127.0.0.1
Is loopback: true
Test case 2: Invalid IP address
Error: invalid IP address format: "256.0.0.1"
Test case 3: Valid IPv6 address
Parsed IP: ::1
Is loopback: true
Help message:
-ip IP address
`IP address` to parse (e.g. 192.168.1.1 or ::1)
func Int
func Int(name string, value int, usage string) *int
Int определяет флаг int с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной int, в которой хранится значение флага.
func Int64
func Int64(name string, value int64, usage string) *int64
Int64 определяет флаг int64 с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной int64, в которой хранится значение флага.
func Int64Var
func Int64Var(p *int64, name string, value int64, usage string)
Int64Var определяет флаг int64 с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную int64, в которой хранится значение флага.
func IntVar
func IntVar(p *int, name string, value int, usage string)
IntVar определяет флаг int с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную int, в которой будет храниться значение флага.
func NArg
NArg — количество аргументов, оставшихся после обработки флагов.
func NFlag
NFlag возвращает количество установленных флагов командной строки.
func Parse
Parse анализирует флаги командной строки из os.Args[1:]. Должен вызываться после определения всех флагов и до доступа к флагам из программы.
func Parsed
Parsed сообщает, были ли проанализированы флаги командной строки.
func PrintDefaults
PrintDefaults выводит в стандартный поток ошибок (если не настроено иначе) сообщение об использовании, показывающее настройки по умолчанию всех определенных флагов командной строки. Для флага x с целочисленным значением вывод по умолчанию имеет вид
-x int
usage-message-for-x (default 7)
Сообщение об использовании будет отображаться в отдельной строке для всех флагов, кроме флагов типа bool с однобайтовым именем.
Для флагов типа bool тип опускается, и если имя флага состоит из одного байта, сообщение об использовании отображается в той же строке.
Значение по умолчанию в скобках опускается, если значение по умолчанию для типа равно нулю.
Указанный тип, в данном случае int, можно изменить, поместив имя в обратные кавычки в строке использования флага; первый такой элемент в сообщении принимается за имя параметра, которое будет отображаться в сообщении, а обратные кавычки удаляются из сообщения при отображении. Например, при задании
flag.String("I", "", "search `directory` for include files")
вывод будет следующим
-I directory
search directory for include files.
Чтобы изменить место назначения для сообщений флага, вызовите CommandLine.SetOutput.
func Set
func Set(name, value string) error
Set устанавливает значение флага командной строки с указанным именем.
func String
func String(name string, value string, usage string) *string
String определяет строковый флаг с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес строковой переменной, в которой хранится значение флага.
func StringVar
func StringVar(p *string, name string, value string, usage string)
StringVar определяет строковый флаг с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на строковую переменную, в которой хранится значение флага.
func TextVar
func TextVar(p encoding.TextUnmarshaler, name string, value encoding.TextMarshaler, usage string)
TextVar определяет флаг с указанным именем, значением по умолчанию и строкой использования. Аргумент p должен быть указателем на переменную, которая будет хранить значение флага, и p должен реализовывать encoding.TextUnmarshaler. Если флаг используется, его значение будет передано методу UnmarshalText p. Тип значения по умолчанию должен быть таким же, как тип p.
Пример
package main
import (
"flag"
"fmt"
"net"
"os"
)
func main() {
// Создаем новый набор флагов
fs := flag.NewFlagSet("ExampleTextVar", flag.ContinueOnError)
fs.SetOutput(os.Stdout)
// Переменная для хранения IP с значением по умолчанию
var ip net.IP = net.IPv4(192, 168, 0, 100)
// Регистрируем TextVar флаг для парсинга IP
fs.TextVar(&ip, "ip", ip, "`IP address` to parse (e.g. 192.168.1.1 or ::1)")
// Тест 1: Валидный IPv4 адрес
fmt.Println("Test case 1: Valid IPv4 address (127.0.0.1)")
if err := fs.Parse([]string{"-ip", "127.0.0.1"}); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Parsed IP: %v\n", ip)
fmt.Printf("Is loopback: %t\n\n", ip.IsLoopback())
}
// Тест 2: Невалидный IP адрес
fmt.Println("Test case 2: Invalid IP address (256.0.0.1)")
ip = nil // Сбрасываем предыдущее значение
if err := fs.Parse([]string{"-ip", "256.0.0.1"}); err != nil {
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Printf("Parsed IP: %v\n\n", ip)
}
// Тест 3: Вывод справки
fmt.Println("Test case 3: Help message")
fs.PrintDefaults()
}
Вывод программы:
Test case 1: Valid IPv4 address (127.0.0.1)
Parsed IP: 127.0.0.1
Is loopback: true
Test case 2: Invalid IP address (256.0.0.1)
Error: invalid IP address: 256.0.0.1
Test case 3: Help message
-ip IP address
`IP address` to parse (e.g. 192.168.1.1 or ::1) (default 192.168.0.100)
func Uint
func Uint(name string, value uint, usage string) *uint
Uint определяет флаг uint с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной uint, которая хранит значение флага.
func Uint64
func Uint64(name string, value uint64, usage string) *uint64
Uint64 определяет флаг uint64 с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной uint64, в которой хранится значение флага.
func Uint64Var
func Uint64Var(p *uint64, name string, value uint64, usage string)
Uint64Var определяет флаг uint64 с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную uint64, в которой будет храниться значение флага.
func UintVar
func UintVar(p *uint, name string, value uint, usage string)
UintVar определяет флаг uint с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную uint, в которой будет храниться значение флага.
func UnquoteUsage
func UnquoteUsage(flag *Flag) (name string, usage string)
UnquoteUsage извлекает имя в обратных кавычках из строки использования для флага и возвращает его и использование без кавычек. При заданном “a `name` to show” возвращает (“name”, “a name to show”). Если обратные кавычки отсутствуют, имя является обоснованным предположением о типе значения флага или пустой строкой, если флаг является булевым.
func Var
func Var(value Value, name string, usage string)
Var определяет флаг с указанным именем и строкой использования. Тип и значение флага представлены первым аргументом типа Value, который обычно содержит пользовательскую реализацию Value. Например, вызывающий может создать флаг, который преобразует строку, разделенную запятыми, в набор строк, предоставив набору методы Value; в частности, Set разложит строку, разделенную запятыми, на набор.
Пример
Пример использования flag.Var() с пользовательским типом
package main
import (
"errors"
"flag"
"fmt"
"strings"
)
// StringSet - пользовательский тип для множества строк
type StringSet map[string]struct{}
// String реализует интерфейс flag.Value
func (s StringSet) String() string {
return strings.Join(s.ToSlice(), ",")
}
// Set реализует интерфейс flag.Value
func (s StringSet) Set(value string) error {
if len(s) > 0 {
return errors.New("StringSet already set")
}
for _, item := range strings.Split(value, ",") {
item = strings.TrimSpace(item)
if item != "" {
s[item] = struct{}{}
}
}
return nil
}
// ToSlice преобразует StringSet в срез строк
func (s StringSet) ToSlice() []string {
result := make([]string, 0, len(s))
for k := range s {
result = append(result, k)
}
return result
}
func main() {
// Инициализируем наш StringSet
var tags StringSet = make(map[string]struct{})
// Регистрируем флаг через flag.Var
flag.Var(&tags, "tags", "comma-separated list of tags")
// Парсим аргументы командной строки
flag.Parse()
// Выводим результат
fmt.Println("Tags count:", len(tags))
fmt.Println("Tags list:", tags.ToSlice())
// Примеры запуска:
// go run main.go -tags=go,programming,example
// go run main.go -tags="rust, systems programming"
}
Ключевые компоненты:
-
Пользовательский тип StringSet:
- Реализует интерфейс
flag.Value с методами String() и Set()
- Хранит уникальные строки как ключи map
-
Метод Set():
- Разбивает строку по запятым
- Игнорирует пустые элементы
- Возвращает ошибку при повторной установке
-
Метод String():
- Формирует строку из элементов через запятую
- Используется для вывода значения по умолчанию
-
Дополнительный метод ToSlice():
- Удобное преобразование в срез строк
Как это работает:
-
При вызове flag.Var() пакет flag получает:
- Указатель на наш StringSet
- Имя флага (“tags”)
- Описание для справки
-
При парсинге аргументов:
- Для флага
-tags=go,programming вызывается Set("go,programming")
- Наш метод разбивает строку и заполняет map
-
При выводе справки:
- Вызывается
String() для показа текущего значения
func Visit
func Visit(fn func(*Flag))
Visit посещает флаги командной строки в лексикографическом порядке, вызывая fn для каждого из них. Он посещает только те флаги, которые были установлены.
func VisitAll
func VisitAll(fn func(*Flag))
VisitAll посещает флаги командной строки в лексикографическом порядке, вызывая fn для каждого из них. Он посещает все флаги, даже те, которые не были установлены.
4.7.2 - Типы пакета flag
Типы и их функции, включенные в пакет flag
type ErrorHandling
ErrorHandling определяет, как будет вести себя FlagSet.Parse в случае неудачи разбора.
const (
ContinueOnError ErrorHandling = iota // Возвращаем описательную ошибку.
ExitOnError // Вызов os.Exit(2) или для -h/-help Exit(0).
PanicOnError // Вызываем панику с описанием ошибки.
)
Эти константы заставляют FlagSet.Parse вести себя так, как описано в случае неудачи разбора.
type Flag
type Flag struct {
Name string // имя в командной строке
Usage string // справочное сообщение
Value Value // значение по умолчанию
DefValue string // значение по умолчанию (в виде текста); для сообщения об использовании
}
Флаг представляет состояние флага.
func Lookup
func Lookup(name string) *Flag
Lookup возвращает структуру Flag именованного флага командной строки, возвращая nil, если таковой не существует.
type FlagSet
type FlagSet struct {
// Usage - это функция, вызываемая при возникновении ошибки во время разбора флагов.
// Поле представляет собой функцию (не метод), которая может быть изменена для указания на
// пользовательский обработчик ошибок. То, что произойдет после вызова Usage, зависит
// от настройки ErrorHandling; для командной строки по умолчанию
// установлено значение ExitOnError, которое завершает работу программы после вызова Usage.
Usage func()
// содержит отфильтрованные или неэкспонированные поля
}
FlagSet представляет собой набор определенных флагов. Нулевое значение FlagSet не имеет имени и имеет обработку ошибки ContinueOnError.
Имена флагов должны быть уникальными в пределах FlagSet. Попытка определить флаг, имя которого уже используется, приведет к панике.
Пример
package main
import (
"flag"
"fmt"
"os"
"time"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
switch os.Args[1] {
case "start":
start(os.Args[2:])
case "stop":
stop(os.Args[2:])
case "help", "-h", "--help":
printUsage()
default:
fmt.Printf("error: unknown command %q\n", os.Args[1])
printUsage()
os.Exit(1)
}
}
func start(args []string) {
fs := flag.NewFlagSet("start", flag.ExitOnError)
addr := fs.String("addr", ":8080", "address to listen on")
logLevel := fs.String("log-level", "info", "log level (debug, info, warn, error)")
if err := fs.Parse(args); err != nil {
fs.Usage()
os.Exit(1)
}
fmt.Printf("Starting server on %s with log level %s\n", *addr, *logLevel)
// Здесь была бы реальная логика запуска сервера
}
func stop(args []string) {
fs := flag.NewFlagSet("stop", flag.ExitOnError)
timeout := fs.Duration("timeout", 5*time.Second, "stop timeout duration")
force := fs.Bool("force", false, "force shutdown")
if err := fs.Parse(args); err != nil {
fs.Usage()
os.Exit(1)
}
fmt.Printf("Stopping server (timeout=%v, force=%t)\n", *timeout, *force)
// Здесь была бы реальная логика остановки сервера
}
func printUsage() {
fmt.Println("Usage:")
fmt.Println(" httpd start [-addr :port] [-log-level level]")
fmt.Println(" httpd stop [-timeout duration] [-force]")
fmt.Println(" httpd help")
fmt.Println("\nFlags:")
fmt.Println(" -h, --help Show this help message")
}
Как использовать:
- Запуск сервера:
./httpd start -addr :9999 -log-level debug
- Остановка сервера:
./httpd stop -timeout 10s -force
- Просмотр справки:
./httpd help
# или
./httpd -h
CommandLine - это набор флагов командной строки по умолчанию, взятый из os.Args. Функции верхнего уровня, такие как BoolVar, Arg и так далее, являются обертками для методов CommandLine.
func NewFlagSet
func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet
NewFlagSet возвращает новый пустой набор флагов с указанным именем и свойством обработки ошибок. Если имя не пустое, оно будет выведено в сообщении об использовании по умолчанию и в сообщениях об ошибках.
Пример
Объяснение flag.NewFlagSet с примерами
Функция flag.NewFlagSet создает новый независимый набор флагов, что особенно полезно для реализации:
- Подкоманд в CLI-приложениях (например,
git commit, docker run)
- Изолированных групп флагов
- Повторного использования флагов в разных контекстах
Параметры:
name - имя набора флагов (используется в сообщениях об ошибках и справке)
errorHandling - стратегия обработки ошибок:
flag.ContinueOnError - продолжить выполнение после ошибки
flag.ExitOnError - вызвать os.Exit(2) при ошибке
flag.PanicOnError - вызвать panic при ошибке
Пример 1: Базовое использование
package main
import (
"flag"
"fmt"
)
func main() {
// Создаем новый набор флагов
fs := flag.NewFlagSet("example", flag.ExitOnError)
// Добавляем флаги
verbose := fs.Bool("verbose", false, "enable verbose output")
port := fs.Int("port", 8080, "port to listen on")
// Парсим аргументы
fs.Parse([]string{"-verbose", "-port", "9090"})
// Используем значения
fmt.Printf("Verbose: %v, Port: %d\n", *verbose, *port)
}
Пример 2: Подкоманды (как в git/docker)
package main
import (
"flag"
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Expected 'greet' or 'time' subcommands")
os.Exit(1)
}
switch os.Args[1] {
case "greet":
// Набор флагов для команды greet
greetCmd := flag.NewFlagSet("greet", flag.ExitOnError)
name := greetCmd.String("name", "World", "name to greet")
greetCmd.Parse(os.Args[2:])
fmt.Printf("Hello, %s!\n", *name)
case "time":
// Набор флагов для команды time
timeCmd := flag.NewFlagSet("time", flag.ExitOnError)
format := timeCmd.String("format", "15:04:05", "time format")
timeCmd.Parse(os.Args[2:])
fmt.Println("Current time:", time.Now().Format(*format))
default:
fmt.Printf("Unknown command %q\n", os.Args[1])
os.Exit(1)
}
}
Пример 3: Разные стратегии обработки ошибок
package main
import (
"flag"
"fmt"
)
func main() {
// ContinueOnError - продолжить после ошибки
fs1 := flag.NewFlagSet("continue", flag.ContinueOnError)
fs1.Int("port", 0, "port number")
err := fs1.Parse([]string{"-port", "invalid"})
fmt.Println("ContinueOnError:", err)
// ExitOnError - выход при ошибке
fs2 := flag.NewFlagSet("exit", flag.ExitOnError)
fs2.Int("port", 0, "port number")
// В реальном коде это вызвало бы os.Exit(2)
fmt.Print("ExitOnError: ")
fs2.Parse([]string{"-port", "invalid"})
}
Рекомендации по использованию:
- Для CLI-приложений используйте
flag.ExitOnError
- Для библиотек лучше
flag.ContinueOnError
- Имя набора флагов помогает пользователю понять контекст ошибки
- Можно создавать несколько независимых наборов флагов в одной программе
func (*FlagSet) Arg
func (f *FlagSet) Arg(i int) string
Arg возвращает i-й аргумент. Arg(0) — это первый оставшийся аргумент после обработки флагов. Arg возвращает пустую строку, если запрошенный элемент не существует.
func (*FlagSet) Args
func (f *FlagSet) Args() []string
Args возвращает аргументы, не являющиеся флагами.
func (*FlagSet) Bool
func (f *FlagSet) Bool(name string, value bool, usage string) *bool
Bool определяет флаг bool с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной bool, в которой хранится значение флага.
func (*FlagSet) BoolFunc
func (f *FlagSet) BoolFunc(name, usage string, fn func(string) error)
BoolFunc определяет флаг с указанным именем и строкой использования без требования значений. Каждый раз, когда флаг встречается, вызывается fn со значением флага. Если fn возвращает ошибку, отличную от nil, она будет рассматриваться как ошибка разбора значения флага.
func (*FlagSet) BoolVar
func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string)
BoolVar определяет флаг bool с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную bool, в которой будет храниться значение флага.
func (*FlagSet) Duration
func (f *FlagSet) Duration(name string, value time.Duration, usage string) *time.Duration
Duration определяет флаг time.Duration с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной time.Duration, в которой хранится значение флага. Флаг принимает значение, допустимое для time.ParseDuration.
Пример
Объяснение flag.Duration с примерами
Метод Duration создает флаг для работы с временными интервалами (тип time.Duration). Это особенно полезно для параметров, которые представляют временные значения (таймауты, интервалы и т.д.).
Основные параметры:
name - имя флага (например, “timeout”)
value - значение по умолчанию (например, 5*time.Second)
usage - описание флага для справки
Пример 1: Простое использование
package main
import (
"flag"
"fmt"
"time"
)
func main() {
// Создаем флаг с продолжительностью
timeout := flag.Duration("timeout", 3*time.Second, "operation timeout duration")
// Парсим аргументы командной строки
flag.Parse()
// Используем значение
fmt.Printf("Waiting for %v before timeout...\n", *timeout)
time.Sleep(*timeout)
fmt.Println("Timeout reached!")
}
Использование:
go run main.go -timeout=2s
# или
go run main.go -timeout=500ms
Пример 2: В составе FlagSet (подкоманды)
package main
import (
"flag"
"fmt"
"time"
)
func main() {
serverCmd := flag.NewFlagSet("server", flag.ExitOnError)
// Флаги для команды server
port := serverCmd.Int("port", 8080, "port to listen on")
readTimeout := serverCmd.Duration("read-timeout", 5*time.Second, "read timeout")
writeTimeout := serverCmd.Duration("write-timeout", 10*time.Second, "write timeout")
serverCmd.Parse([]string{"-port=9000", "-read-timeout=2s", "-write-timeout=5s"})
fmt.Printf("Server config:\n")
fmt.Printf(" Port: %d\n", *port)
fmt.Printf(" Read Timeout: %v\n", *readTimeout)
fmt.Printf(" Write Timeout: %v\n", *writeTimeout)
}
Пример 3: Поддержка разных форматов времени
Флаг Duration автоматически поддерживает все форматы, которые понимает time.ParseDuration:
package main
import (
"flag"
"fmt"
)
func main() {
interval := flag.Duration("interval", 1*time.Minute, "check interval")
flag.Parse()
fmt.Printf("Checking every %v\n", *interval)
fmt.Printf("In nanoseconds: %d\n", interval.Nanoseconds())
}
Допустимые форматы:
- “300ms” - миллисекунды
- “1.5h” - часы с дробной частью
- “2h45m” - комбинированный формат
- “1d” - дни (расширение Go, означает 24h)
Типичные сценарии использования:
- Таймауты операций:
timeout := flag.Duration("timeout", 30*time.Second, "operation timeout")
- Интервалы повторения:
interval := flag.Duration("interval", 1*time.Hour, "check interval")
- Временные ограничения:
ttl := flag.Duration("ttl", 24*time.Hour, "time to live")
func (*FlagSet) DurationVar
func (f *FlagSet) DurationVar(p *time.Duration, name string, value time.Duration, usage string)
DurationVar определяет флаг time.Duration с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную time.Duration, в которой хранится значение флага. Флаг принимает значение, допустимое для time.ParseDuration.
func (*FlagSet) ErrorHandling
func (f *FlagSet) ErrorHandling() ErrorHandling
ErrorHandling возвращает поведение обработки ошибок набора флагов.
Пример
Объяснение ErrorHandling() с примерами
Метод ErrorHandling() возвращает текущую стратегию обработки ошибок для набора флагов (FlagSet). Это полезно, когда нужно проверить или изменить поведение обработки ошибок во время выполнения.
Возможные значения:
flag.ContinueOnError - продолжить выполнение после ошибки
flag.ExitOnError - вызвать os.Exit(2) при ошибке
flag.PanicOnError - вызвать panic при ошибке
Пример 1: Проверка текущей стратегии
package main
import (
"flag"
"fmt"
)
func main() {
// Создаем набор флагов с разными стратегиями
fs1 := flag.NewFlagSet("server", flag.ContinueOnError)
fs2 := flag.NewFlagSet("client", flag.ExitOnError)
// Проверяем стратегии обработки ошибок
fmt.Println("Server error handling:", fs1.ErrorHandling())
fmt.Println("Client error handling:", fs2.ErrorHandling())
}
Вывод:
Server error handling: 1 // ContinueOnError
Client error handling: 2 // ExitOnError
Пример 2: Динамическое изменение поведения
package main
import (
"flag"
"fmt"
"os"
)
func main() {
fs := flag.NewFlagSet("app", flag.ContinueOnError)
fs.Int("port", 8080, "port number")
// Для демонстрации - имитируем ошибку
args := []string{"-port", "invalid"}
// Парсим с текущей стратегией (ContinueOnError)
err := fs.Parse(args)
fmt.Printf("With ContinueOnError: error=%v, port=%d\n", err, fs.Lookup("port").Value)
// Меняем стратегию на ExitOnError
fs.Init(fs.Name(), flag.ExitOnError)
fmt.Println("New error handling:", fs.ErrorHandling())
// При новой стратегии парсинг вызовет os.Exit(2)
fmt.Print("With ExitOnError: ")
fs.Parse(args) // В реальности здесь будет выход
}
Пример 3: Использование в кастомной обработке ошибок
package main
import (
"flag"
"fmt"
"os"
)
func main() {
fs := flag.NewFlagSet("config", flag.ContinueOnError)
fs.String("config", "", "config file path")
fs.Duration("timeout", 5*time.Second, "operation timeout")
err := fs.Parse(os.Args[1:])
switch fs.ErrorHandling() {
case flag.ContinueOnError:
if err != nil {
fmt.Printf("Configuration error: %v\n", err)
// Продолжаем работу с значениями по умолчанию
}
case flag.ExitOnError:
// Обработка уже выполнена flag.Parse
case flag.PanicOnError:
// panic уже вызван
}
// Основная логика приложения
fmt.Println("Using config:", fs.Lookup("config").Value)
}
Основные сценарии использования:
- Библиотеки - когда нужно передать управление ошибкой вызывающему коду
- Тестирование - проверка поведения при разных стратегиях
- Гибкие CLI-утилиты - изменение поведения в зависимости от режима работы
Советы:
- Для приложений обычно используют
ExitOnError
- Для библиотек лучше
ContinueOnError
PanicOnError редко используется на практике
func (*FlagSet) Float64
func (f *FlagSet) Float64(name string, value float64, usage string) *float64
Float64 определяет флаг float64 с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной float64, в которой хранится значение флага.
func (*FlagSet) Float64Var
func (f *FlagSet) Float64Var(p *float64, name string, value float64, usage string)
Float64Var определяет флаг float64 с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную float64, в которой хранится значение флага.
func (*FlagSet) Func
func (f *FlagSet) Func(name, usage string, fn func(string) error)
Func определяет флаг с указанным именем и строкой использования. Каждый раз, когда флаг встречается, вызывается fn со значением флага. Если fn возвращает ошибку, отличную от nil, она будет рассматриваться как ошибка разбора значения флага.
func (*FlagSet) Init
func (f *FlagSet) Init(name string, errorHandling ErrorHandling)
Init устанавливает имя и свойство обработки ошибок для набора флагов. По умолчанию нулевой FlagSet использует пустое имя и политику обработки ошибок ContinueOnError.
func (*FlagSet) Int
func (f *FlagSet) Int(name string, value int, usage string) *int
Int определяет флаг int с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной int, в которой хранится значение флага.
func (*FlagSet) Int64
func (f *FlagSet) Int64(name string, value int64, usage string) *int64
Int64 определяет флаг int64 с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной int64, в которой хранится значение флага.
func (*FlagSet) Int64Var
func (f *FlagSet) Int64Var(p *int64, name string, value int64, usage string)
Int64Var определяет флаг int64 с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную int64, в которой хранится значение флага.
func (*FlagSet) IntVar
func (f *FlagSet) IntVar(p *int, name string, value int, usage string)
IntVar определяет флаг int с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную int, в которой будет храниться значение флага.
func (*FlagSet) Lookup
func (f *FlagSet) Lookup(name string) *Flag
Lookup возвращает структуру Flag с указанным именем флага, возвращая nil, если такой флаг не существует.
func (*FlagSet) NArg
func (f *FlagSet) NArg() int
NArg — количество аргументов, оставшихся после обработки флагов.
func (*FlagSet) NFlag
func (f *FlagSet) NFlag() int
NFlag возвращает количество установленных флагов.
func (*FlagSet) Name
func (f *FlagSet) Name() string
Name возвращает имя набора флагов.
func (*FlagSet) Output
func (f *FlagSet) Output() io.Writer
Output возвращает место назначения для сообщений об использовании и ошибках. os.Stderr возвращается, если выход не был установлен или был установлен в nil.
func (*FlagSet) Parse
func (f *FlagSet) Parse(arguments []string) error
Parse анализирует определения флагов из списка аргументов, который не должен включать имя команды. Должен вызываться после того, как все флаги в FlagSet определены, и до того, как программа получит доступ к флагам. Возвращаемое значение будет ErrHelp, если -help или -h были установлены, но не определены.
func (*FlagSet) Parsed
func (f *FlagSet) Parsed() bool
Parsed сообщает, был ли вызван f.Parse.
func (*FlagSet) PrintDefaults
func (f *FlagSet) PrintDefaults()
PrintDefaults выводит в стандартный поток ошибок (если не настроено иначе) значения по умолчанию всех определенных флагов командной строки в наборе. Дополнительную информацию см. в документации по глобальной функции PrintDefaults.
func (*FlagSet) Set
func (f *FlagSet) Set(name, value string) error
Set устанавливает значение флага с указанным именем.
func (*FlagSet) SetOutput
func (f *FlagSet) SetOutput(output io.Writer)
SetOutput устанавливает место назначения для сообщений об использовании и ошибках. Если output равно nil, используется os.Stderr.
func (*FlagSet) String
func (f *FlagSet) String(name string, value string, usage string) *string
String определяет строковый флаг с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес строковой переменной, в которой хранится значение флага.
func (*FlagSet) StringVar
func (f *FlagSet) StringVar(p *string, name string, value string, usage string)
StringVar определяет строковый флаг с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на строковую переменную, в которой будет храниться значение флага.
func (*FlagSet) TextVar
func (f *FlagSet) TextVar(p encoding.TextUnmarshaler, name string, value encoding.TextMarshaler, usage string)
TextVar определяет флаг с указанным именем, значением по умолчанию и строкой использования. Аргумент p должен быть указателем на переменную, которая будет хранить значение флага, и p должен реализовывать encoding.TextUnmarshaler. Если флаг используется, его значение будет передано методу UnmarshalText p. Тип значения по умолчанию должен быть таким же, как тип p.
func (*FlagSet) Uint
func (f *FlagSet) Uint(name string, value uint, usage string) *uint
Uint определяет флаг uint с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной uint, которая хранит значение флага.
func (*FlagSet) Uint64
func (f *FlagSet) Uint64(name string, value uint64, usage string) *uint64
Uint64 определяет флаг uint64 с указанным именем, значением по умолчанию и строкой использования. Возвращаемое значение — адрес переменной uint64, в которой хранится значение флага.
func (*FlagSet) Uint64Var
func (f *FlagSet) Uint64Var(p *uint64, name string, value uint64, usage string)
Uint64Var определяет флаг uint64 с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную uint64, в которой хранится значение флага.
func (*FlagSet) UintVar
func (f *FlagSet) UintVar(p *uint, name string, value uint, usage string)
UintVar определяет флаг uint с указанным именем, значением по умолчанию и строкой использования. Аргумент p указывает на переменную uint, в которой будет храниться значение флага.
func (*FlagSet) Var
func (f *FlagSet) Var(value Value, name string, usage string)
Var определяет флаг с указанным именем и строкой использования. Тип и значение флага представлены первым аргументом типа Value, который обычно содержит пользовательскую реализацию Value. Например, вызывающая сторона может создать флаг, который преобразует строку, разделенную запятыми, в набор строк, предоставив набору методы Value; в частности, Set разложит строку, разделенную запятыми, на набор.
func (*FlagSet) Visit
func (f *FlagSet) Visit(fn func(*Flag))
Visit посещает флаги в лексикографическом порядке, вызывая fn для каждого из них. Он посещает только те флаги, которые были установлены.
func (*FlagSet) VisitAll
func (f *FlagSet) VisitAll(fn func(*Flag))
VisitAll посещает флаги в лексикографическом порядке, вызывая fn для каждого из них. Он посещает все флаги, даже те, которые не были установлены.
type Getter
type Getter interface {
Value
Get() any
}
Getter — это интерфейс, который позволяет извлекать содержимое Value. Он оборачивает интерфейс Value, а не является его частью, поскольку появился после Go 1 и его правил совместимости. Все типы Value, предоставляемые этим пакетом, удовлетворяют интерфейсу Getter, за исключением типа, используемого Func.
Пример
Объяснение интерфейса Getter в пакете flag
Интерфейс Getter в пакете flag предоставляет способ получения значения флага в виде interface{} (тип any). Это расширение базового интерфейса Value, добавленное для обеспечения большей гибкости при работе с флагами.
Структура интерфейса:
type Getter interface {
Value
Get() any
}
Где:
Value - стандартный интерфейс для работы с флагами (требует методов String() и Set())
Get() any - метод, возвращающий текущее значение флага в виде пустого интерфейса
Пример 1: Проверка значения флага
package main
import (
"flag"
"fmt"
"time"
)
func main() {
// Создаем несколько флагов разных типов
flag.Int("port", 8080, "server port")
flag.Duration("timeout", 5*time.Second, "operation timeout")
flag.String("name", "default", "service name")
flag.Parse()
// Получаем значения через Getter
printFlagValue("port")
printFlagValue("timeout")
printFlagValue("name")
}
func printFlagValue(name string) {
if f := flag.Lookup(name); f != nil {
if getter, ok := f.Value.(flag.Getter); ok {
fmt.Printf("%s: %v (%T)\n", name, getter.Get(), getter.Get())
} else {
fmt.Printf("%s: does not implement Getter\n", name)
}
} else {
fmt.Printf("%s: flag not found\n", name)
}
}
Пример 2: Кастомный тип с Getter
package main
import (
"flag"
"fmt"
"strings"
)
type StringSlice []string
func (s *StringSlice) String() string {
return strings.Join(*s, ",")
}
func (s *StringSlice) Set(value string) error {
*s = append(*s, value)
return nil
}
func (s *StringSlice) Get() any {
return []string(*s)
}
func main() {
var hosts StringSlice
flag.Var(&hosts, "host", "target host (can be specified multiple times)")
flag.Parse()
if g, ok := flag.Lookup("host").Value.(flag.Getter); ok {
fmt.Printf("Hosts: %v\n", g.Get())
}
}
Пример 3: Использование с разными типами флагов
package main
import (
"flag"
"fmt"
"reflect"
)
func main() {
// Разные типы флагов
intFlag := flag.Int("num", 42, "an integer")
boolFlag := flag.Bool("verbose", false, "verbose output")
stringFlag := flag.String("msg", "hello", "a message")
flag.Parse()
// Проверяем реализацию Getter для каждого флага
flags := []string{"num", "verbose", "msg"}
for _, name := range flags {
f := flag.Lookup(name)
if getter, ok := f.Value.(flag.Getter); ok {
val := getter.Get()
fmt.Printf("%s: %v (type: %v)\n", name, val, reflect.TypeOf(val))
}
}
}
Когда использовать Getter:
- Когда нужно работать с флагами разных типов единообразно
- Для создания обобщенных функций обработки флагов
- При реализации кастомных типов флагов с возможностью извлечения значения
Важные замечания:
- Почти все стандартные типы флагов реализуют
Getter
- Исключение - флаги, созданные через
flag.Func()
- Для проверки реализации используйте type assertion:
if getter, ok := flag.Value.(flag.Getter); ok {
value := getter.Get()
// ...
}
Этот интерфейс особенно полезен при создании сложных CLI-приложений, где требуется гибкая работа с флагами разных типов.
type Value
type Value интерфейс {
String() string
Set(string) error
}
Value — это интерфейс к динамическому значению, хранящемуся в флаге. (Значение по умолчанию представлено в виде строки.)
Если Value имеет метод IsBoolFlag() bool, возвращающий true, парсер командной строки делает -name эквивалентным -name=true, а не использует следующий аргумент командной строки.
Set вызывается один раз, в порядке командной строки, для каждого присутствующего флага. Пакет flag может вызывать метод String с нулевым значением приемника, таким как указатель nil.
Пример
package main
import (
"flag"
"fmt"
"net/url"
)
// URLValue - кастомный тип для парсинга URL в флагах
type URLValue struct {
URL *url.URL // Хранит указатель на разобранный URL
}
// String реализует интерфейс flag.Value
func (v URLValue) String() string {
if v.URL != nil {
return v.URL.String()
}
return ""
}
// Set реализует интерфейс flag.Value
func (v *URLValue) Set(s string) error {
u, err := url.Parse(s)
if err != nil {
return fmt.Errorf("failed to parse URL: %w", err)
}
*v.URL = *u // Копируем разобранный URL
return nil
}
// Get реализует интерфейс flag.Getter
func (v URLValue) Get() any {
return v.URL
}
// Глобальная переменная для хранения разобранного URL
var targetURL = &url.URL{}
func main() {
// Создаем новый набор флагов
fs := flag.NewFlagSet("URLParser", flag.ExitOnError)
// Регистрируем наш кастомный флаг
fs.Var(&URLValue{targetURL}, "url", "URL to parse (e.g. 'https://example.com/path')")
// Парсим аргументы (в реальном приложении используйте os.Args[1:])
err := fs.Parse([]string{"-url", "https://golang.org/pkg/flag/"})
if err != nil {
fmt.Printf("Error parsing flags: %v\n", err)
return
}
// Выводим разобранные компоненты URL
fmt.Printf("Parsed URL components:\n")
fmt.Printf("{scheme: %q, host: %q, path: %q}\n",
targetURL.Scheme,
targetURL.Host,
targetURL.Path)
}
Комментарии по коду:
-
Интерфейс Value:
String() используется для вывода текущего значения
Set() парсит строку и сохраняет значение
-
Интерфейс Getter:
- Позволяет получить значение как
interface{}
- Полезно для обобщенной обработки флагов
-
Особенности реализации:
- Используем указатель на
url.URL для модификации
- Метод
Set() должен быть с pointer receiver
- Всегда проверяем ошибки парсинга URL
-
Использование в реальных приложениях:
// Вместо fs.Parse([]string{...}) используйте:
fs.Parse(os.Args[1:])
-
Дополнительные улучшения:
- Можно добавить валидацию URL (например, требовать HTTPS)
- Можно добавить метод
IsSet() для проверки установки значения
Пример запуска:
go run main.go -url "https://example.com/api/v1?param=value"
Вывод:
Parsed URL components:
{scheme: "https", host: "example.com", path: "/api/v1"}
Этот код теперь представляет собой надежную реализацию кастомного флага для работы с URL в Go-приложениях.
4.8 - Полный справочник по пакету fmt в Go
Полное описание пакета fmt в Go с примерами применения и разными потоками вывода
Пакет fmt (format) — один из самых часто используемых в Go. Он предоставляет функции для форматированного ввода/вывода, аналогичные printf и scanf в C.
1. Основные функции вывода
1.1. Вывод в стандартный поток (stdout)
fmt.Print("Hello, ", "World!") // Без перевода строки
fmt.Println("Hello, World!") // С переводом строки
fmt.Printf("Name: %s, Age: %d\n", "Alice", 25) // Форматированный вывод
1.2. Вывод в строку (Sprint, Sprintf, Sprintln)
s1 := fmt.Sprint("Hello, ", "World!") // => "Hello, World!"
s2 := fmt.Sprintf("Name: %s, Age: %d", "Bob", 30) // => "Name: Bob, Age: 30"
s3 := fmt.Sprintln("Line with newline") // => "Line with newline\n"
1.3. Вывод в файл или io.Writer (Fprint, Fprintf, Fprintln)
file, _ := os.Create("output.txt")
fmt.Fprint(file, "Hello, file!") // Запись в файл
fmt.Fprintf(file, "Value: %v", 42) // Форматированная запись
fmt.Fprintln(file, "End of line") // Запись с переводом строки
2. Форматирование вывода (Printf, Sprintf, Fprintf)
2.1. Основные спецификаторы
| Спецификатор |
Описание |
Пример (%v) |
Вывод (42) |
%v |
Универсальный формат |
%v |
42 |
%+v |
С полями структур |
%+v |
{X:42} |
%#v |
Go-синтаксис |
%#v |
42 (int) |
%T |
Тип переменной |
%T |
int |
%% |
Символ % |
%% |
% |
2.2. Форматирование разных типов данных
// Целые числа
fmt.Printf("%d %b %o %x %X\n", 42, 42, 42, 42, 42) // 42 101010 52 2a 2A
// Вещественные числа
fmt.Printf("%f %.2f %e %g\n", 3.1415, 3.1415, 3.1415, 3.1415) // 3.141500 3.14 3.141500e+00 3.1415
// Строки и символы
fmt.Printf("%s %q %c\n", "Go", "Go", 'G') // Go "Go" G
// Логические значения
fmt.Printf("%t\n", true) // true
2.3. Ширина и точность
// Ширина поля (выравнивание)
fmt.Printf("|%5d|%-5d|\n", 42, 42) // | 42|42 |
// Точность для float
fmt.Printf("%.2f\n", 3.141592) // 3.14
// Комбинация ширины и точности
fmt.Printf("%10.2f\n", 3.141592) // " 3.14"
2.4. Позиционные аргументы
fmt.Printf("%[2]d %[1]d\n", 10, 20) // 20 10
3. Ввод данных (Scan, Scanf, Scanln)
3.1. Чтение из стандартного ввода (stdin)
var name string
var age int
fmt.Print("Enter name and age: ")
fmt.Scan(&name, &age) // Читает до пробела/перевода строки
fmt.Printf("Hello, %s (%d)\n", name, age)
3.2. Форматированный ввод (Scanf)
var a, b int
fmt.Scanf("%d,%d", &a, &b) // Ввод "10,20" → a=10, b=20
3.3. Чтение строки (Scanln)
var input string
fmt.Scanln(&input) // Читает до перевода строки
3.4. Чтение из строки (Sscan, Sscanf, Sscanln)
var x, y int
fmt.Sscanf("10 20", "%d %d", &x, &y) // x=10, y=20
3.5. Чтение из файла (Fscan, Fscanf, Fscanln)
file, _ := os.Open("input.txt")
var value int
fmt.Fscanf(file, "%d", &value) // Чтение числа из файла
4. Дополнительные функции
4.1. Форматирование ошибок (Errorf)
err := fmt.Errorf("invalid value: %d", 42) // Создает error
4.2. Вывод в io.Writer с форматированием (Fprintf)
var buf bytes.Buffer
fmt.Fprintf(&buf, "Value: %d", 42) // Запись в буфер
4.3. Сканирование из io.Reader (Fscanf)
reader := strings.NewReader("100")
var num int
fmt.Fscanf(reader, "%d", &num) // num = 100
5. Полезные примеры
5.1. Вывод таблицы
fmt.Printf("|%-10s|%10s|\n", "Name", "Age")
fmt.Printf("|%-10s|%10d|\n", "Alice", 25)
fmt.Printf("|%-10s|%10d|\n", "Bob", 30)
Вывод:
|Name | Age|
|Alice | 25|
|Bob | 30|
5.2. Чтение нескольких значений
var a, b int
fmt.Print("Enter two numbers: ")
fmt.Scan(&a, &b)
fmt.Println("Sum:", a+b)
5.3. Форматирование JSON
data := map[string]interface{}{"name": "Alice", "age": 25}
jsonStr := fmt.Sprintf("%+v", data) // {"age":25, "name":"Alice"}
Дополнительный раздел: Форматирование различных типов данных в Go
1. Форматирование базовых типов (Printf, Sprintf, Fprintf)
1.1. Целые числа (int, int32, int64, uint)
num := 42
fmt.Printf("Десятичное: %d\n", num) // 42
fmt.Printf("Двоичное: %b\n", num) // 101010
fmt.Printf("Восьмеричное: %o\n", num) // 52
fmt.Printf("Шестнадцатеричное (нижний регистр): %x\n", num) // 2a
fmt.Printf("Шестнадцатеричное (верхний регистр): %X\n", num) // 2A
fmt.Printf("С ведущими нулями: %05d\n", num) // 00042
1.2. Вещественные числа (float32, float64)
pi := 3.1415926535
fmt.Printf("По умолчанию: %f\n", pi) // 3.141593
fmt.Printf("С точностью 2 знака: %.2f\n", pi) // 3.14
fmt.Printf("Экспоненциальная запись: %e\n", pi) // 3.141593e+00
fmt.Printf("Автовыбор формата: %g\n", pi) // 3.1415926535
fmt.Printf("Ширина 10 символов: %10.2f\n", pi) // " 3.14"
1.3. Строки (string) и символы (rune)
str := "Hello"
fmt.Printf("Строка: %s\n", str) // Hello
fmt.Printf("Строка в кавычках: %q\n", str) // "Hello"
fmt.Printf("Ширина 10 символов: %10s\n", str) // " Hello"
fmt.Printf("Символ: %c\n", 'A') // A
fmt.Printf("Юникод-символ: %U\n", 'Я') // U+042F
1.4. Логические значения (bool)
flag := true
fmt.Printf("Логическое: %t\n", flag) // true
1.5. Указатели (*int, *string)
x := 42
ptr := &x
fmt.Printf("Указатель: %p\n", ptr) // 0xc0000180a8
fmt.Printf("Значение по указателю: %v\n", *ptr) // 42
2. Форматирование структур (struct) и массивов (slice, array)
2.1. Вывод структуры
type Person struct {
Name string
Age int
}
p := Person{"Alice", 25}
fmt.Printf("Универсальный формат: %v\n", p) // {Alice 25}
fmt.Printf("С полями: %+v\n", p) // {Name:Alice Age:25}
fmt.Printf("Go-синтаксис: %#v\n", p) // main.Person{Name:"Alice", Age:25}
2.2. Форматирование слайсов и массивов
arr := [3]int{1, 2, 3}
slice := []string{"a", "b", "c"}
fmt.Printf("Массив: %v\n", arr) // [1 2 3]
fmt.Printf("Слайс: %v\n", slice) // [a b c]
fmt.Printf("Go-синтаксис: %#v\n", slice) // []string{"a", "b", "c"}
3. Форматирование с помощью fmt.Append (Go 1.19+)
Функция fmt.Append позволяет форматировать данные и добавлять их в слайс байт ([]byte).
3.1. Базовое использование
buf := []byte("Данные: ")
buf = fmt.Append(buf, 42, " ", true)
fmt.Println(string(buf)) // Данные: 42 true
3.2. Форматированный вывод (Appendf)
buf := []byte("Число: ")
buf = fmt.Appendf(buf, "%05d", 42)
fmt.Println(string(buf)) // Число: 00042
3.3. Добавление в буфер (bytes.Buffer)
var buffer bytes.Buffer
buffer.WriteString("Строка: ")
fmt.Fprintf(&buffer, "%q", "Hello")
fmt.Println(buffer.String()) // Строка: "Hello"
4. Специальные форматы
4.1. Вывод времени (time.Time)
t := time.Now()
fmt.Printf("RFC3339: %v\n", t.Format(time.RFC3339)) // 2024-03-15T14:20:10Z
fmt.Printf("Кастомный формат: %02d.%02d.%04d\n", t.Day(), t.Month(), t.Year()) // 15.03.2024
4.2. Форматирование JSON
data := map[string]any{"name": "Alice", "age": 25}
jsonStr := fmt.Sprintf("%+v", data) // map[age:25 name:Alice]
4.3. Ширина и выравнивание
fmt.Printf("|%10s|\n", "left") // | left|
fmt.Printf("|%-10s|\n", "right") // |right |
fmt.Printf("|%10.2f|\n", 3.1415) // | 3.14|
5. Полезные примеры
5.1. Таблица с выравниванием
fmt.Printf("|%-10s|%10s|\n", "Name", "Age")
fmt.Printf("|%-10s|%10d|\n", "Alice", 25)
fmt.Printf("|%-10s|%10d|\n", "Bob", 30)
Вывод:
|Name | Age|
|Alice | 25|
|Bob | 30|
5.2. Форматирование ошибок
err := fmt.Errorf("ошибка: значение %d недопустимо", 100)
fmt.Println(err) // ошибка: значение 100 недопустимо
5.3. Чтение и запись в файл
// Запись
file, _ := os.Create("data.txt")
fmt.Fprintf(file, "Число: %d", 42)
file.Close()
// Чтение
file, _ = os.Open("data.txt")
var num int
fmt.Fscanf(file, "Число: %d", &num)
fmt.Println(num) // 42
4.9 - Описание пакета io языка программирования Go
Пакет io предоставляет базовые интерфейсы для примитивов ввода-вывода.
Его основная задача - обернуть существующие реализации таких примитивов, например, в пакете os, в общие публичные интерфейсы, которые абстрагируют эту функциональность, а также некоторые другие связанные с ней примитивы.
Поскольку эти интерфейсы и примитивы обертывают операции нижнего уровня с различными реализациями, если клиенты не проинформированы об ином, не следует полагать, что они безопасны для параллельного выполнения.
Константы
const (
SeekStart = 0 // поиск относительно начала файла
SeekCurrent = 1 // поиск относительно текущего смещения
SeekEnd = 2 // поиск относительно конца)
Значения whence поиска.
Переменные
var EOF = errors.New(«EOF»)
EOF — это ошибка, возвращаемая Read, когда входные данные больше не доступны. (Read должен возвращать сам EOF, а не ошибку, оборачивающую EOF, потому что вызывающие функции будут проверять EOF с помощью ==.) Функции должны возвращать EOF только для сигнализации о корректном завершении ввода. Если EOF возникает неожиданно в структурированном потоке данных, соответствующей ошибкой является либо ErrUnexpectedEOF, либо какая-либо другая ошибка, дающая более подробную информацию.
var ErrClosedPipe = errors.New(«io: read/write on closed pipe»)
ErrClosedPipe — это ошибка, используемая для операций чтения или записи в закрытом канале.
var ErrNoProgress = errors.New(«multiple Read calls return no data or error»)
ErrNoProgress возвращается некоторыми клиентами Reader, когда много вызовов Read не возвращают никаких данных или ошибок, что обычно является признаком неисправной реализации Reader.
var ErrShortBuffer = errors.New(«короткий буфер»)
ErrShortBuffer означает, что для чтения потребовался буфер большего размера, чем был предоставлен.
var ErrShortWrite = errors.New(«короткая запись»)
ErrShortWrite означает, что запись приняла меньше байтов, чем было запрошено, но не вернула явную ошибку.
var ErrUnexpectedEOF = errors.New(«неожиданный EOF»)
ErrUnexpectedEOF означает, что EOF был обнаружен в середине чтения блока фиксированного размера или структуры данных.
4.9.1 - Описание функций пакета io
Спецификация функций пакета io с примерами на языке Go
func Copy
func Copy(dst Writer, src Reader) (written int64, err error)
Copy копирует данные из src в dst до тех пор, пока не будет достигнут EOF в src или не произойдет ошибка. Функция возвращает количество скопированных байтов и первую ошибку, возникшую при копировании, если таковая имеется.
Успешная функция Copy возвращает err == nil, а не err == EOF. Поскольку функция Copy определена для чтения из src до EOF, она не рассматривает EOF из Read как ошибку, о которой следует сообщать.
Если src реализует WriterTo, копирование реализуется вызовом src.WriteTo(dst). В противном случае, если dst реализует ReaderFrom, копирование реализуется вызовом dst.ReadFrom(src).
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
if _, err := io.Copy(os.Stdout, r); err != nil {
log.Fatal(err)
}
}
Output:
some io.Reader stream to be read
func CopyBuffer
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
CopyBuffer идентична Copy, за исключением того, что она проходит через предоставленный буфер (если он требуется), а не выделяет временный. Если buf равна nil, выделяется один; в противном случае, если она имеет нулевую длину, CopyBuffer вызывает панику.
Если src реализует WriterTo или dst реализует ReaderFrom, buf не будет использоваться для выполнения копирования.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r1 := strings.NewReader("first reader\n")
r2 := strings.NewReader("second reader\n")
buf := make([]byte, 8)
// buf is used here...
if _, err := io.CopyBuffer(os.Stdout, r1, buf); err != nil {
log.Fatal(err)
}
// ... reused here also. No need to allocate an extra buffer.
if _, err := io.CopyBuffer(os.Stdout, r2, buf); err != nil {
log.Fatal(err)
}
}
Output:
first reader
second reader
func CopyN
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
CopyN копирует n байт (или до возникновения ошибки) из src в dst. Он возвращает количество скопированных байтов и самую раннюю ошибку, возникшую во время копирования. При возвращении written == n тогда и только тогда, когда err == nil.
Если dst реализует ReaderFrom, копирование реализуется с его помощью.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read")
if _, err := io.CopyN(os.Stdout, r, 4); err != nil {
log.Fatal(err)
}
}
func Pipe
func Pipe() (*PipeReader, *PipeWriter)
Pipe создает синхронный конвейер в памяти. Его можно использовать для соединения кода, ожидающего io.Reader, с кодом, ожидающим io.Writer.
Чтение и запись в канале сопоставляются один к одному, за исключением случаев, когда для потребления одной записи требуется несколько чтений. То есть каждая запись в PipeWriter блокируется до тех пор, пока не будет удовлетворено одно или несколько чтений из PipeReader, которые полностью потребляют записанные данные. Данные копируются непосредственно из записи в соответствующее чтение (или чтения); внутренней буферизации нет.
Безопасно вызывать Read и Write параллельно друг с другом или с Close. Параллельные вызовы Read и параллельные вызовы Write также безопасны: отдельные вызовы будут последовательно блокироваться.
Пример
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
r, w := io.Pipe()
go func() {
fmt.Fprint(w, "some io.Reader stream to be read\n")
w.Close()
}()
if _, err := io.Copy(os.Stdout, r); err != nil {
log.Fatal(err)
}
}
Output:
some io.Reader stream to be read
func ReadAll
func ReadAll(r Reader) ([]byte, error)
ReadAll читает из r до ошибки или EOF и возвращает прочитанные данные. Успешный вызов возвращает err == nil, а не err == EOF. Поскольку ReadAll определено для чтения из src до EOF, оно не рассматривает EOF из Read как ошибку, о которой следует сообщать.
Пример
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("Go is a general-purpose language designed with systems programming in mind.")
b, err := io.ReadAll(r)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", b)
}
Output:
Go is a general-purpose language designed with systems programming in mind.
func ReadAtLeast
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
ReadAtLeast читает из r в buf, пока не прочитает как минимум min байт. Он возвращает количество скопированных байтов и ошибку, если было прочитано меньше байтов. Ошибка EOF возникает только в том случае, если не было прочитано ни одного байта. Если EOF возникает после чтения менее min байтов, ReadAtLeast возвращает ErrUnexpectedEOF. Если min больше длины buf, ReadAtLeast возвращает ErrShortBuffer. При возвращении n >= min, если и только если err == nil. Если r возвращает ошибку, прочитав не менее min байтов, ошибка игнорируется.
Пример
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
buf := make([]byte, 14)
if _, err := io.ReadAtLeast(r, buf, 4); err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// buffer smaller than minimal read size.
shortBuf := make([]byte, 3)
if _, err := io.ReadAtLeast(r, shortBuf, 4); err != nil {
fmt.Println("error:", err)
}
// minimal read size bigger than io.Reader stream
longBuf := make([]byte, 64)
if _, err := io.ReadAtLeast(r, longBuf, 64); err != nil {
fmt.Println("error:", err)
}
}
Output:
some io.Reader
error: short buffer
error: unexpected EOF
func ReadFull
func ReadFull(r Reader, buf []byte) (n int, err error)
ReadFull читает ровно len(buf) байтов из r в buf. Он возвращает количество скопированных байтов и ошибку, если было прочитано меньше байтов. Ошибка EOF возникает только в том случае, если не было прочитано ни одного байта. Если EOF возникает после чтения некоторых, но не всех байтов, ReadFull возвращает ErrUnexpectedEOF. При возвращении n == len(buf) тогда и только тогда, когда err == nil. Если r возвращает ошибку после чтения по крайней мере len(buf) байтов, ошибка игнорируется.
Пример
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
buf := make([]byte, 4)
if _, err := io.ReadFull(r, buf); err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
// minimal read size bigger than io.Reader stream
longBuf := make([]byte, 64)
if _, err := io.ReadFull(r, longBuf); err != nil {
fmt.Println("error:", err)
}
}
Output:
some
error: unexpected EOF
func WriteString
func WriteString(w Writer, s string) (n int, err error)
WriteString записывает содержимое строки s в w, который принимает набор байтов. Если w реализует StringWriter, [StringWriter.WriteString] вызывается напрямую. В противном случае [Writer.Write] вызывается ровно один раз.
Пример
package main
import (
"io"
"log"
"os"
)
func main() {
if _, err := io.WriteString(os.Stdout, "Hello World"); err != nil {
log.Fatal(err)
}
}
4.9.2 - Описание типов пакета io языка программирования GO
Описание типов и их функций пакета io языка программирования GO
type ByteReader
type ByteReader interface {
ReadByte() (byte, error)
}
ByteReader — это интерфейс, который оборачивает метод ReadByte.
ReadByte считывает и возвращает следующий байт из ввода или любую возникшую ошибку. Если ReadByte возвращает ошибку, входной байт не был обработан, и возвращаемое значение байта не определено.
ReadByte предоставляет эффективный интерфейс для обработки по одному байту за раз. Reader, который не реализует ByteReader, можно обернуть с помощью bufio.NewReader, чтобы добавить этот метод.
type ByteScanner
type ByteScanner interface {
ByteReader
UnreadByte() error
}
ByteScanner — это интерфейс, который добавляет метод UnreadByte к базовому методу ReadByte.
UnreadByte заставляет следующий вызов ReadByte возвращать последний прочитанный байт. Если последняя операция не была успешным вызовом ReadByte, UnreadByte может вернуть ошибку, не прочитать последний прочитанный байт (или байт, предшествующий последнему непрочитанному байту), или (в реализациях, поддерживающих интерфейс Seeker) перейти на один байт перед текущим смещением.
type ByteWriter
type ByteWriter interface {
WriteByte(c byte) error
}
ByteWriter — это интерфейс, который оборачивает метод WriteByte.
type Closer
type Closer interface {
Close() error
}
Closer — это интерфейс, который оборачивает базовый метод Close.
Поведение Close после первого вызова не определено. Конкретные реализации могут документировать свое собственное поведение.
type LimitedReader
type LimitedReader struct {
R Reader // базовый читатель
N int64 // максимальное количество оставшихся байтов
}
LimitedReader читает из R, но ограничивает количество возвращаемых данных до N байтов. Каждый вызов Read обновляет N, чтобы отразить новое количество оставшихся данных. Read возвращает EOF, когда N <= 0 или когда базовый R возвращает EOF.
func (*LimitedReader) Read
func (l *LimitedReader) Read(p []byte) (n int, err error)
Пример
package main
import (
"fmt"
"io"
"strings"
)
func main() {
// Создаем источник данных - строка длиной 100 символов
data := strings.Repeat("abcdefghij", 10) // 100 байт
reader := strings.NewReader(data)
// Создаем LimitedReader, который прочитает только первые 35 байт
limitedReader := &io.LimitedReader{
R: reader, // базовый reader
N: 35, // лимит в 35 байт
}
// Буфер для чтения
buf := make([]byte, 10) // читаем по 10 байт за раз
var totalRead int
for {
// Читаем данные
n, err := limitedReader.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("\nДостигнут лимит или конец данных")
} else {
fmt.Println("\nОшибка чтения:", err)
}
break
}
totalRead += n
fmt.Printf("Прочитано %d байт: %q\n", n, buf[:n])
fmt.Printf("Осталось прочитать: %d байт\n", limitedReader.N)
}
fmt.Println("Всего прочитано:", totalRead, "байт")
fmt.Println("Осталось данных в основном reader:", reader.Len(), "байт")
}
Прочитано 10 байт: "abcdefghij"
Осталось прочитать: 25 байт
Прочитано 10 байт: "abcdefghij"
Осталось прочитать: 15 байт
Прочитано 10 байт: "abcdefghij"
Осталось прочитать: 5 байт
Прочитано 5 байт: "abcde"
Осталось прочитать: 0 байт
Достигнут лимит или конец данных
Всего прочитано: 35 байт
Осталось данных в основном reader: 65 байт
type OffsetWriter
type OffsetWriter struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Объяснение OffsetWriter
OffsetWriter в Go — это обёртка вокруг WriterAt, которая позволяет записывать данные, начиная с указанного смещения в базовом потоке. Он автоматически управляет позицией записи и предоставляет три ключевые функции: Write (пишет с текущей позиции), WriteAt (пишет по конкретному смещению без изменения позиции) и Seek (меняет текущую позицию). Это особенно полезно для записи в определённые места файлов (например, при обновлении заголовков или работе с бинарными форматами), когда нужно контролировать точное расположение данных без ручного управления позицией в базовом WriterAt.
Пример с использованием всех методов OffsetWriter:
package main
import (
"fmt"
"io"
"os"
)
func main() {
// Создаем временный файл
tmpFile, err := os.CreateTemp("", "offset-writer-demo")
if err != nil {
panic(err)
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
// Заполняем файл пробельными символами для наглядности
_, err = tmpFile.WriteString("=== Начало файла ===\n" +
strings.Repeat(".", 100) + "\n=== Конец файла ===")
if err != nil {
panic(err)
}
// Создаем OffsetWriter с начальным смещением 20 байт
ow := io.NewOffsetWriter(tmpFile, 20)
// 1. Используем Write - пишет с текущего смещения
fmt.Println("\n1. Запись через Write()")
_, err = ow.Write([]byte("ПЕРВАЯ ЗАПИСЬ"))
if err != nil {
panic(err)
}
printFileContent(tmpFile.Name())
// 2. Используем Seek для изменения позиции
fmt.Println("\n2. Seek(10, io.SeekCurrent)")
newPos, err := ow.Seek(10, io.SeekCurrent) // Перемещаемся на +10 байт от текущей позиции
if err != nil {
panic(err)
}
fmt.Printf("Новая позиция: %d\n", newPos)
// 3. Еще одна запись через Write
_, err = ow.Write([]byte("ВТОРАЯ ЗАПИСЬ"))
if err != nil {
panic(err)
}
printFileContent(tmpFile.Name())
// 4. Используем WriteAt - пишет по абсолютному смещению (не меняет текущую позицию)
fmt.Println("\n4. WriteAt() по смещению 5")
_, err = ow.WriteAt([]byte("ТРЕТЬЯ"), 5)
if err != nil {
panic(err)
}
printFileContent(tmpFile.Name())
// 5. Проверяем текущую позицию после WriteAt
pos, _ := ow.Seek(0, io.SeekCurrent)
fmt.Printf("\n5. Текущая позиция после WriteAt: %d\n", pos)
// 6. Возвращаемся в начало и пишем еще
fmt.Println("\n6. Seek(0, io.SeekStart) + Write()")
ow.Seek(0, io.SeekStart)
ow.Write([]byte("НАЧАЛО"))
printFileContent(tmpFile.Name())
}
func printFileContent(filename string) {
data, _ := os.ReadFile(filename)
fmt.Println("Содержимое файла:")
fmt.Println(string(data))
fmt.Println("Длина:", len(data), "байт")
fmt.Println(strings.Repeat("-", 50))
}
Разбор функциональности:
-
Write(p []byte):
- Записывает данные с текущей позиции
- Автоматически увеличивает текущую позицию
-
Seek(offset, whence):
whence может быть:
io.SeekStart - от начала файла
io.SeekCurrent - от текущей позиции
io.SeekEnd - от конца файла
- Возвращает новую позицию
-
WriteAt(p []byte, off int64):
- Записывает данные по абсолютному смещению
off
- Не изменяет текущую позицию записи
- Полезен для точечных модификаций
Пример вывода:
1. Запись через Write()
Содержимое файла:
=== Начало файла ===
ПЕРВАЯ ЗАПИСЬ....................
=== Конец файла ===
Длина: 132 байт
--------------------------------------------------
2. Seek(10, io.SeekCurrent)
Новая позиция: 42
3. Запись через Write()
Содержимое файла:
=== Начало файла ===
ПЕРВАЯ ЗАПИСЬ..........ВТОРАЯ ЗАПИСЬ.....
=== Конец файла ===
Длина: 154 байт
--------------------------------------------------
4. WriteAt() по смещению 5
Содержимое файла:
=== Начало файла ===
ПЕРТЬЯ ЗАПИСЬ..........ВТОРАЯ ЗАПИСЬ.....
=== Конец файла ===
Длина: 154 байт
--------------------------------------------------
5. Текущая позиция после WriteAt: 42
6. Seek(0, io.SeekStart) + Write()
Содержимое файла:
=== Начало файла ===
НАЧАЛО ЗАПИСЬ..........ВТОРАЯ ЗАПИСЬ.....
=== Конец файла ===
Длина: 154 байт
--------------------------------------------------
Практические сценарии использования:
-
Редактирование файлов:
// Исправление заголовка в существующем файле
ow := io.NewOffsetWriter(file, 0)
ow.WriteAt(correctHeader, 0)
-
Работа с бинарными форматами:
// Запись данных в определенные секции файла
ow.Seek(headerSize, io.SeekStart)
ow.Write(dataChunk)
-
Многопоточная запись:
// Каждая горутина пишет в свой раздел
go func() {
sectionWriter := io.NewOffsetWriter(file, sectionOffset)
sectionWriter.Write(sectionData)
}()
func NewOffsetWriter
func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter
NewOffsetWriter возвращает OffsetWriter, который записывает в w, начиная с смещения off.
func (*OffsetWriter) Seek
func (o *OffsetWriter) Seek(offset int64, whence int) (int64, error)
func (*OffsetWriter) Write
func (o *OffsetWriter) Write(p []byte) (n int, err error)
func (*OffsetWriter) WriteAt
func (o *OffsetWriter) WriteAt(p []byte, off int64) (n int, err error)
type PipeReader
type PipeReader struct {
// содержит отфильтрованные или неэкспортируемые поля
}
PipeReader — это читающая часть канала (pipe).
Объяснение PipeReader и PipeWriter
PipeReader представляет читающую часть именованного канала (pipe).
PipeWriter представляет записывающую часть именованного канала (pipe).
package main
import (
"fmt"
"io"
"time"
)
func main() {
// Создаем pipe
pipeReader, pipeWriter := io.Pipe()
// Горутина для чтения из канала
go func() {
buf := make([]byte, 256)
// 1. Обычное чтение
n, err := pipeReader.Read(buf)
if err != nil {
fmt.Printf("Ошибка чтения: %v\n", err)
return
}
fmt.Printf("Прочитано %d байт: %s\n", n, buf[:n])
// 2. Попытка чтения после закрытия записи
n, err = pipeReader.Read(buf)
if err == io.EOF {
fmt.Println("Канал закрыт (EOF)")
} else if err != nil {
fmt.Printf("Ошибка чтения: %v\n", err)
}
// 3. Чтение после CloseWithError
_, err = pipeReader.Read(buf)
if err != nil {
fmt.Printf("Ошибка после CloseWithError: %v\n", err)
}
}()
// Горутина для записи в канал
go func() {
// Записываем данные
_, err := pipeWriter.Write([]byte("Тестовые данные"))
if err != nil {
fmt.Printf("Ошибка записи: %v\n", err)
}
// Закрываем запись (обычное закрытие)
pipeWriter.Close()
// Даем время на обработку
time.Sleep(100 * time.Millisecond)
// Пытаемся записать после закрытия чтения
_, err = pipeWriter.Write([]byte("Новые данные"))
if err == io.ErrClosedPipe {
fmt.Println("Попытка записи в закрытый канал (ErrClosedPipe)")
}
}()
// Даем время на выполнение
time.Sleep(200 * time.Millisecond)
// Закрываем читатель с ошибкой
err := pipeReader.CloseWithError(fmt.Errorf("кастомная ошибка"))
if err != nil {
fmt.Printf("Ошибка при CloseWithError: %v\n", err)
}
// Пытаемся записать после CloseWithError
_, err = pipeWriter.Write([]byte("Последние данные"))
if err != nil {
fmt.Printf("Ошибка записи после CloseWithError: %v\n", err)
}
time.Sleep(100 * time.Millisecond)
}
Разбор методов:
-
Read():
- Блокируется, пока не появятся данные или не закроется записывающая часть
- При закрытии записывающей части возвращает
io.EOF
- При
CloseWithError возвращает указанную ошибку
-
Close():
- Закрывает читающую часть
- Последующие записи будут возвращать
io.ErrClosedPipe
-
CloseWithError():
- Закрывает читающую часть с указанной ошибкой
- Последующие операции чтения вернут эту ошибку
- Не перезаписывает предыдущие ошибки
Пример вывода:
Прочитано 28 байт: Тестовые данные
Канал закрыт (EOF)
Попытка записи в закрытый канал (ErrClosedPipe)
Ошибка после CloseWithError: кастомная ошибка
Ошибка записи после CloseWithError: io: read/write on closed pipe
Практическое применение:
- Связь между горутинами
- Преобразование данных “на лету”
- Тестирование кода, работающего с интерфейсами
io.Reader/io.Writer
- Реализация прокси-серверов и middleware
func (*PipeReader) Close
func (r *PipeReader) Close() error
Close закрывает читатель; последующие записи в записывающую половину канала будут возвращать ошибку ErrClosedPipe.
func (*PipeReader) CloseWithError
func (r *PipeReader) CloseWithError(err error) error
CloseWithError закрывает читатель; последующие записи в записывающую половину канала будут возвращать ошибку err.
CloseWithError никогда не перезаписывает предыдущую ошибку, если она существует, и всегда возвращает nil.
func (*PipeReader) Read
func (r *PipeReader) Read(data []byte) (n int, err error)
Read реализует стандартный интерфейс Read: он считывает данные из канала, блокируя до тех пор, пока не появится записывающее устройство или не будет закрыта записывающая часть. Если записывающая часть закрыта с ошибкой, эта ошибка возвращается как err; в противном случае err равен EOF.
type PipeWriter
type PipeWriter struct {
// содержит отфильтрованные или неэкспортируемые поля
}
PipeWriter — это записывающая часть канала (pipe).
func (*PipeWriter) Close
func (w *PipeWriter) Close() error
Close закрывает записывающее устройство; последующие чтения из читающей половины канала не будут возвращать байты и EOF.
func (*PipeWriter) CloseWithError
func (w *PipeWriter) CloseWithError(err error) error
CloseWithError закрывает запись; последующие чтения из части трубы, отвечающей за чтение, не будут возвращать байты и будут возвращать ошибку err или EOF, если err равна nil.
CloseWithError никогда не перезаписывает предыдущую ошибку, если она существует, и всегда возвращает nil.
func (*PipeWriter) Write
func (w *PipeWriter) Write(data []byte) (n int, err error)
Write реализует стандартный интерфейс Write: он записывает данные в канал, блокируя его до тех пор, пока один или несколько читателей не потребят все данные или читающая сторона не будет закрыта. Если читающая сторона закрыта с ошибкой, эта ошибка возвращается как err; в противном случае err равна ErrClosedPipe.
type ReadCloser
type ReadCloser интерфейс {
Reader
Closer
}
ReadCloser — это интерфейс, который группирует основные методы Read и Close.
Объяснение ReadCloser
ReadCloser - это просто комбинация двух возможностей:
- Чтение данных (как у обычного
Reader)
- Закрытие ресурса (как у
Closer)
Это интерфейс для объектов, которые:
- Могут отдавать данные (например, файл, сетевое соединение)
- И требуют закрытия после использования (чтобы освободить ресурсы)
Примеры использования:
- Открыли файл → читаем из него → закрываем
- Установили сетевое соединение → получаем данные → разрываем соединение
NopCloser - это “адаптер”:
- Берет обычный
Reader (который не умеет закрываться)
- Возвращает
ReadCloser, где метод Close() ничего не делает (“no-op” - операция-пустышка)
Зачем это нужно?
Когда функция требует ReadCloser, а у вас есть только Reader (который не нужно закрывать)
Пример:
// Есть строка (она реализует Reader)
data := strings.NewReader("Привет, мир!")
// Но нам нужно передать ReadCloser
rc := io.NopCloser(data)
// Теперь можно использовать везде, где требуется ReadCloser
// При вызове rc.Close() ничего не произойдет
Где применяется:
- Когда работаем с API, требующим
ReadCloser
- Когда нужно подставить “фейковый” closer для тестирования
- При работе с данными в памяти, которые не требуют очистки
func NopCloser
func NopCloser(r Reader) ReadCloser
NopCloser возвращает ReadCloser с методом Close, не выполняющим никаких действий, который оборачивает предоставленный Reader r. Если r реализует WriterTo, возвращаемый ReadCloser будет реализовывать WriterTo, перенаправляя вызовы r.
type ReadSeekCloser
type ReadSeekCloser интерфейс {
Reader
Seeker
Closer
}
ReadSeekCloser — интерфейс, который группирует основные методы Read, Seek и Close.
Объяснение ReadSeekCloser, ReadSeeker, ReadWriteCloser…
1. ReadSeekCloser
Назначение: Объединяет чтение, произвольный доступ и закрытие ресурса.
Пример с файлом:
func processFile(rsc io.ReadSeekCloser) error {
data := make([]byte, 100)
// Чтение
if _, err := rsc.Read(data); err != nil {
return err
}
// Перемещение в начало
if _, err := rsc.Seek(0, io.SeekStart); err != nil {
return err
}
// Закрытие
defer rsc.Close()
return nil
}
// Использование
file, _ := os.Open("data.txt")
processFile(file) // *os.File реализует ReadSeekCloser
Зачем: Нужен для работы с ресурсами (файлы, сетевые потоки), где требуется:
- Чтение данных
- Перемещение по содержимому
- Обязательное закрытие
2. ReadSeeker
Назначение: Чтение + перемещение по данным без закрытия.
Пример с буфером:
func analyze(data io.ReadSeeker) {
// Первое чтение
buf := make([]byte, 10)
data.Read(buf)
// Возврат в начало
data.Seek(0, io.SeekStart)
}
// Использование
buffer := strings.NewReader("abcdefghij")
analyze(buffer) // strings.Reader реализует ReadSeeker
Зачем: Для данных, где нужно:
- Многократное чтение
- Навигация (например, парсинг заголовков)
3. ReadWriteCloser
Назначение: Чтение + запись + закрытие.
Пример с сетевым соединением:
func handleConnection(conn io.ReadWriteCloser) {
defer conn.Close()
// Чтение
buf := make([]byte, 1024)
conn.Read(buf)
// Ответ
conn.Write([]byte("OK"))
}
// Использование (псевдокод)
// conn, _ := net.Dial("tcp", "example.com:80")
// handleConnection(conn) // net.Conn реализует ReadWriteCloser
Зачем: Для двусторонних ресурсов:
- Файлы с записью
- Сетевые соединения
- Драйверы устройств
4. ReadWriteSeeker
Назначение: Чтение + запись + навигация.
Пример с файлом логов:
func updateLog(rws io.ReadWriteSeeker, msg string) {
// Перемещение в конец
rws.Seek(0, io.SeekEnd)
// Запись
rws.Write([]byte(msg))
}
// Использование
file, _ := os.OpenFile("log.txt", os.O_RDWR, 0644)
updateLog(file, "New entry\n")
Зачем: Для редактируемых ресурсов:
- Файлы баз данных
- Логи
- Память с произвольным доступом
5. ReadWriter
Назначение: Только чтение + запись.
Пример с буфером в памяти:
func process(rw io.ReadWriter) {
rw.Write([]byte("ping"))
response, _ := io.ReadAll(rw)
fmt.Println(string(response))
}
// Использование
var buf bytes.Buffer
buf.WriteString("pong")
process(&buf) // bytes.Buffer реализует ReadWriter
Зачем: Для простых двусторонних потоков:
- Буферы
- Шифрование/сжатие “на лету”
- Тестирование
Ключевые отличия:
| Тип |
Read |
Write |
Seek |
Close |
Примеры использования |
ReadSeekCloser |
✓ |
✗ |
✓ |
✓ |
Файлы, сетевые потоки |
ReadSeeker |
✓ |
✗ |
✓ |
✗ |
Парсинг данных |
ReadWriteCloser |
✓ |
✓ |
✗ |
✓ |
Сокеты, открытые файлы на запись |
ReadWriteSeeker |
✓ |
✓ |
✓ |
✗ |
Файлы БД, лог-файлы |
ReadWriter |
✓ |
✓ |
✗ |
✗ |
Буферы, преобразователи данных |
type ReadSeeker
type ReadSeeker интерфейс {
Reader
Seeker
}
ReadSeeker — интерфейс, который группирует основные методы Read и Seek.
type ReadWriteCloser
type ReadWriteCloser интерфейс {
Reader
Writer
Closer
}
ReadWriteCloser — это интерфейс, который группирует основные методы Read, Write и Close.
type ReadWriteSeeker
type ReadWriteSeeker интерфейс {
Reader
Writer
Seeker
}
ReadWriteSeeker — интерфейс, который группирует основные методы Read, Write и Seek.
type ReadWriter
type ReadWriter интерфейс {
Reader
Writer
}
ReadWriter — интерфейс, который группирует основные методы Read и Write.
type Reader
type Reader interface {
Read(p []byte) (n int, err error)
}
Reader — это интерфейс, который оборачивает базовый метод Read.
Read считывает до len(p) байт в p. Он возвращает количество прочитанных байтов (0 <= n <= len(p)) и любую возникшую ошибку. Даже если Read возвращает n < len(p), он может использовать все p в качестве временного пространства во время вызова. Если некоторые данные доступны, но не len(p) байтов, Read по умолчанию возвращает то, что доступно, вместо того, чтобы ждать больше.
Когда Read встречает ошибку или условие конца файла после успешного чтения n > 0 байтов, он возвращает количество прочитанных байтов. Он может вернуть ошибку (не nil) из того же вызова или вернуть ошибку (и n == 0) из последующего вызова. Примером этого общего случая является то, что Reader, возвращающий ненулевое количество байтов в конце входного потока, может возвращать либо err == EOF, либо err == nil. Следующий Read должен возвращать 0, EOF.
Вызывающие функции должны всегда обрабатывать возвращенные n > 0 байтов, прежде чем рассматривать ошибку err. Это позволяет правильно обрабатывать ошибки ввода-вывода, возникающие после чтения некоторых байтов, а также оба допустимых поведения EOF.
Если len(p) == 0, Read всегда должен возвращать n == 0. Он может возвращать ошибку, отличную от nil, если известно о каком-либо условии ошибки, таком как EOF.
Реализации Read не рекомендуется возвращать нулевое количество байтов с ошибкой nil, за исключением случаев, когда len(p) == 0. Вызывающие должны рассматривать возвращение 0 и nil как указание на то, что ничего не произошло; в частности, это не указывает на EOF.
Реализации не должны сохранять p.
func LimitReader
func LimitReader(r Reader, n int64) Reader
LimitReader возвращает Reader, который читает из r, но останавливается с EOF после n байтов. Базовой реализацией является *LimitedReader.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
lr := io.LimitReader(r, 4)
if _, err := io.Copy(os.Stdout, lr); err != nil {
log.Fatal(err)
}
}
func MultiReader
func MultiReader(readers ...Reader) Reader
MultiReader возвращает Reader, который является логическим соединением предоставленных входных считывателей. Они считываются последовательно. Как только все входы вернут EOF, Read вернет EOF. Если какой-либо из считывателей вернет ошибку, отличную от nil и EOF, Read вернет эту ошибку.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r1 := strings.NewReader("first reader ")
r2 := strings.NewReader("second reader ")
r3 := strings.NewReader("third reader\n")
r := io.MultiReader(r1, r2, r3)
if _, err := io.Copy(os.Stdout, r); err != nil {
log.Fatal(err)
}
}
Output:
first reader second reader third reader
func TeeReader
func TeeReader(r Reader, w Writer) Reader
TeeReader возвращает Reader, который записывает в w то, что он читает из r. Все чтения из r, выполняемые через него, сопоставляются с соответствующими записями в w. Внутренней буферизации нет — запись должна быть завершена до завершения чтения. Любая ошибка, возникшая во время записи, сообщается как ошибка чтения.
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
var r io.Reader = strings.NewReader("some io.Reader stream to be read\n")
r = io.TeeReader(r, os.Stdout)
// Everything read from r will be copied to stdout.
if _, err := io.ReadAll(r); err != nil {
log.Fatal(err)
}
}
Output:
some io.Reader stream to be read
type ReaderAt
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
ReaderAt — это интерфейс, который оборачивает базовый метод ReadAt.
ReadAt считывает len(p) байт в p, начиная с смещения off в базовом источнике ввода. Он возвращает количество прочитанных байтов (0 <= n <= len(p)) и любую возникшую ошибку.
Когда ReadAt возвращает n < len(p), он возвращает не нулевую ошибку, объясняющую, почему не было возвращено больше байтов. В этом отношении ReadAt более строг, чем Read.
Даже если ReadAt возвращает n < len(p), он может использовать все p в качестве временного пространства во время вызова. Если некоторые данные доступны, но не len(p) байтов, ReadAt блокируется до тех пор, пока не будут доступны все данные или не произойдет ошибка. В этом отношении ReadAt отличается от Read.
Если n = len(p) байтов, возвращаемых ReadAt, находятся в конце источника ввода, ReadAt может вернуть либо err == EOF, либо err == nil.
Если ReadAt читает из источника ввода с смещением поиска, ReadAt не должен влиять на базовое смещение поиска и не должен подвергаться его влиянию.
Клиенты ReadAt могут выполнять параллельные вызовы ReadAt на одном и том же источнике ввода.
Реализации не должны сохранять p.
Объяснение ReaderAt
Интерфейс ReaderAt в Go
Интерфейс ReaderAt определён в пакете io и позволяет читать данные из произвольного смещения (offset) без изменения состояния “читателя”.
Сигнатура метода
ReadAt(p []byte, off int64) (n int, err error)
p []byte — буфер, куда записываются прочитанные данные.
off int64 — смещение (в байтах) от начала источника данных.
- Возвращает:
n — количество прочитанных байтов.
err — ошибку (например, io.EOF при достижении конца данных).
Пример использования ReaderAt
1. Чтение файла с произвольного смещения
package main
import (
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("example.txt")
if err != nil {
panic(err)
}
defer file.Close()
// Читаем 10 байт, начиная с 5-го байта
buf := make([]byte, 10)
n, err := file.ReadAt(buf, 5)
if err != nil && err != io.EOF {
panic(err)
}
fmt.Printf("Прочитано %d байт: %q\n", n, buf[:n])
}
Вывод (если example.txt содержит "Hello, world!"):
Прочитано 10 байт: ", world!"
2. Реализация собственного ReaderAt
Допустим, у нас есть структура, хранящая данные в памяти:
type MemoryReader struct {
data []byte
}
func (r *MemoryReader) ReadAt(p []byte, off int64) (n int, err error) {
if off >= int64(len(r.data)) {
return 0, io.EOF
}
n = copy(p, r.data[off:])
if n < len(p) {
err = io.EOF
}
return
}
func main() {
reader := &MemoryReader{data: []byte("RandomAccessData")}
buf := make([]byte, 5)
// Читаем 5 байт с 7-й позиции
n, err := reader.ReadAt(buf, 7)
if err != nil && err != io.EOF {
panic(err)
}
fmt.Printf("Прочитано: %q\n", buf[:n]) // "Access"
}
Назначение ReaderAt
- Произвольный доступ (random access) — чтение с любого места без последовательного перемещения.
- Потокобезопасность — метод
ReadAt можно вызывать из разных горутин (если реализация поддерживает).
- Используется в:
- Файловых системах (
os.File реализует ReaderAt).
- Бинарных форматах (например, чтение заголовков архивов).
- Базах данных (чтение данных по смещению).
Отличие от Reader
Reader (io.Reader) |
ReaderAt (io.ReaderAt) |
Читает последовательно (Read меняет состояние). |
Читает с любого места (ReadAt не меняет состояние). |
| Используется в потоковых данных (сети, pipes). |
Используется для произвольного доступа (файлы, память). |
Пример: http.Response.Body. |
Пример: os.File. |
Вывод
ReaderAt полезен, когда нужно читать данные с произвольных позиций, например, при работе с файлами или бинарными структурами. В отличие от Reader, он не зависит от текущей позиции и поддерживает конкурентный доступ.
type ReaderFrom
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
ReaderFrom — это интерфейс, который оборачивает метод ReadFrom.
ReadFrom читает данные из r до EOF или ошибки. Возвращаемое значение n — это количество прочитанных байтов. Любая ошибка, кроме EOF, возникшая во время чтения, также возвращается.
Функция Copy использует ReaderFrom, если он доступен.
type RuneReader
type RuneReader interface {
ReadRune() (r rune, size int, err error)
}
RuneReader — это интерфейс, который оборачивает метод ReadRune.
ReadRune считывает один закодированный символ Unicode и возвращает руну и ее размер в байтах. Если символ недоступен, будет установлено err.
type RuneScanner
type RuneScanner interface {
RuneReader
UnreadRune() error
}
RuneScanner — это интерфейс, который добавляет метод UnreadRune к базовому методу ReadRune.
UnreadRune заставляет следующий вызов ReadRune возвращать последнюю прочитанную руну. Если последняя операция не была успешным вызовом ReadRune, UnreadRune может вернуть ошибку, не прочитать последнюю прочитанную руну (или руну, предшествующую последней непрочитанной руне), или (в реализациях, поддерживающих интерфейс Seeker) перейти к началу руны перед текущим смещением.
type SectionReader
type SectionReader struct {
// содержит отфильтрованные или неэкспортированные поля
}
SectionReader реализует Read, Seek и ReadAt на секции базового ReaderAt.
Объяснение SectionReader
Назначение SectionReader
SectionReader в Go (пакет io) позволяет читать только определённую часть (секцию) данных из базового источника, реализующего ReaderAt. Это полезно, когда нужно:
- Ограничить чтение определённым диапазоном байтов
- Работать с частью файла или данных как с самостоятельным потоком
- Читать данные с произвольных позиций (
ReadAt) и перемещаться по ним (Seek)
Пример использования всех методов
package main
import (
"fmt"
"io"
"strings"
)
func main() {
// Исходные данные
data := "Hello, World! This is a SectionReader example."
baseReader := strings.NewReader(data)
// Создаём SectionReader (база: data, смещение: 7, длина: 12 байтов)
section := io.NewSectionReader(baseReader, 7, 12)
// 1. Чтение всего раздела (Read)
buf := make([]byte, 12)
n, _ := section.Read(buf)
fmt.Printf("Read: %q (%d bytes)\n", buf[:n], n) // "World! This"
// 2. Чтение с позиции (ReadAt)
n, _ = section.ReadAt(buf, 2)
fmt.Printf("ReadAt(2): %q\n", buf[:n]) // "rld! This "
// 3. Перемещение (Seek)
section.Seek(5, io.SeekStart) // Перемещаемся на 5 байт от начала
n, _ = section.Read(buf)
fmt.Printf("After Seek(5): %q\n", buf[:n]) // "! This"
// 4. Получение параметров секции (Outer)
r, off, n := section.Outer()
fmt.Printf("Base: %T, Offset: %d, Size: %d\n", r, off, n)
// 5. Размер секции (Size)
fmt.Println("Section size:", section.Size()) // 12
}
Вывод:
Read: "World! This" (12 bytes)
ReadAt(2): "rld! This "
After Seek(5): "! This"
Base: *strings.Reader, Offset: 7, Size: 12
Section size: 12
Разбор методов
| Метод |
Описание |
NewSectionReader() |
Создаёт читатель для диапазона [off, off+n) из базового ReaderAt |
Read() |
Читает данные последовательно (меняет текущую позицию) |
ReadAt() |
Читает данные с указанного смещения (не меняет текущую позицию) |
Seek() |
Перемещает текущую позицию (io.SeekStart/SeekCurrent/SeekEnd) |
Size() |
Возвращает максимальный размер секции в байтах |
Outer() |
Возвращает исходный ReaderAt, смещение и размер, переданные при создании |
Типичные сценарии использования
-
Чтение заголовков файлов
// Читаем первые 512 байт (например, заголовок ZIP)
headerSection := io.NewSectionReader(file, 0, 512)
-
Обработка частей больших файлов
// Читаем блок с 1024 по 2048 байт
chunk := io.NewSectionReader(file, 1024, 1024)
-
Виртуализация подмножества данных
// Работаем с частью данных как с самостоятельным io.Reader
processData(section)
SectionReader особенно полезен при работе с бинарными форматами (архивы, медиафайлы), где нужно читать данные по чанкам или с определённых смещений.
func NewSectionReader
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader
NewSectionReader возвращает SectionReader, который читает из r, начиная с смещения off, и останавливается с EOF после n байтов.
func (*SectionReader) Outer
func (s *SectionReader) Outer() (r ReaderAt, off int64, n int64)
Outer возвращает базовый ReaderAt и смещения для секции.
Возвращаемые значения совпадают с теми, которые были переданы в NewSectionReader при создании SectionReader.
func (*SectionReader) Read
func (s *SectionReader) Read(p []byte) (n int, err error)
Пример
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
s := io.NewSectionReader(r, 5, 17)
buf := make([]byte, 9)
if _, err := s.Read(buf); err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
}
func (*SectionReader) ReadAt
func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error)
Пример
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
s := io.NewSectionReader(r, 5, 17)
buf := make([]byte, 6)
if _, err := s.ReadAt(buf, 10); err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", buf)
}
func (*SectionReader) Seek
func (s *SectionReader) Seek(offset int64, whence int) (int64, error)
Пример
package main
import (
"io"
"log"
"os"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
s := io.NewSectionReader(r, 5, 17)
if _, err := s.Seek(10, io.SeekStart); err != nil {
log.Fatal(err)
}
if _, err := io.Copy(os.Stdout, s); err != nil {
log.Fatal(err)
}
}
func (*SectionReader) Size
func (s *SectionReader) Size() int64
Size возвращает размер секции в байтах.
Пример
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
s := io.NewSectionReader(r, 5, 17)
fmt.Println(s.Size())
}
type Seeker
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
Seeker — это интерфейс, который оборачивает базовый метод Seek.
Seek устанавливает смещение для следующего Read или Write в offset, интерпретируемое в соответствии с whence: SeekStart означает относительно начала файла, SeekCurrent означает относительно текущего смещения, а SeekEnd означает относительно конца (например, offset = -2 указывает предпоследний байт файла). Seek возвращает новое смещение относительно начала файла или ошибку, если таковая имеется.
Переход к смещению перед началом файла является ошибкой. Переход к любому положительному смещению может быть разрешен, но если новое смещение превышает размер базового объекта, поведение последующих операций ввода-вывода зависит от реализации.
Объяснение Seeker
Назначение интерфейса Seeker в пакете io
Интерфейс Seeker определяет метод Seek(), который позволяет произвольно перемещаться по потоку данных (файлу, буферу в памяти, сетевому соединению и т.д.). Это ключевая возможность для работы с данными не только последовательно, но и в произвольном порядке.
Для чего нужен Seeker?
-
Произвольный доступ к данным
Возможность “перепрыгивать” к любому месту в потоке без чтения предыдущих данных.
-
Чтение/запись в разные части файла
Например, обновление заголовка файла после записи основного содержимого.
-
Реализация сложных протоколов
Парсинг структур, где нужно “заглядывать” вперед и возвращаться назад.
-
Оптимизация работы с большими файлами
Чтение только нужных фрагментов без загрузки всего файла в память.
Детали метода Seek()
Seek(offset int64, whence int) (int64, error)
| Параметр |
Значения (whence) |
Описание |
offset |
Любое число |
Смещение в байтах (может быть отрицательным для SeekCurrent/SeekEnd) |
whence |
io.SeekStart (0) |
Отсчёт от начала данных |
|
io.SeekCurrent (1) |
Отсчёт от текущей позиции |
|
io.SeekEnd (2) |
Отсчёт от конца данных |
Возвращает:
- Новую позицию (абсолютный offset от начала)
- Ошибку (например, попытка выйти за границы данных)
Примеры использования
1. Перемещение в файле
file, _ := os.Open("data.txt")
defer file.Close()
// Перемещаемся на 100-й байт от начала
pos, _ := file.Seek(100, io.SeekStart)
// Читаем 50 байт с этой позиции
buf := make([]byte, 50)
file.Read(buf)
2. Чтение с конца
// Перемещаемся на 10 байт назад от конца
pos, _ := file.Seek(-10, io.SeekEnd)
3. Относительное перемещение
// Текущая позиция: 200
// Перемещаемся на +50 байт вперёд
pos, _ := file.Seek(50, io.SeekCurrent) // Новый pos = 250
Где реализован Seeker?
Стандартные типы, поддерживающие интерфейс:
*os.File (файлы)
*bytes.Reader (буфер в памяти)
*strings.Reader (строка как поток)
*SectionReader (часть другого ReaderAt)
Ограничения
-
Не все источники поддерживают
Например, сетевые соединения (net.Conn) не реализуют Seeker.
-
Поведение зависит от типа
При работе с файлами на диске Seek эффективен, но для сжатых данных (например, ZIP) может требовать декомпрессии.
Комбинация с другими интерфейсами
Часто используется вместе с:
Reader → io.ReadSeeker
Writer → io.WriteSeeker
ReaderAt/WriterAt для произвольного доступа без изменения позиции
Пример объединённого интерфейса:
type ReadSeeker interface {
Reader
Seeker
}
Итог
Seeker добавляет потокам данных критически важную возможность — произвольный доступ. Это фундамент для:
- Работы с бинарными форматами (архивы, медиафайлы)
- Оптимизированного чтения больших файлов
- Реализации сложных алгоритмов парсинга
- Многопроходной обработки данных без переоткрытия источника
type StringWriter
type StringWriter interface {
WriteString(s string) (n int, err error)
}
StringWriter — это интерфейс, который оборачивает метод WriteString.
type WriteCloser
type WriteCloser interface {
Writer
Closer
}
WriteCloser — это интерфейс, который группирует базовые методы Write и Close.
type WriteSeeker
type WriteSeeker interface {
Writer
Seeker
}
WriteSeeker — это интерфейс, который группирует базовые методы Write и Seek.
type Writer
type Writer interface {
Write(p []byte) (n int, err error)
}
Writer — это интерфейс, который оборачивает базовый метод Write.
Write записывает len(p) байт из p в базовый поток данных. Он возвращает количество байт, записанных из p (0 <= n <= len(p)), и любую ошибку, которая привела к преждевременному прекращению записи. Write должен возвращать ошибку, отличную от nil, если он возвращает n < len(p). Write не должен изменять данные слайса, даже временно.
Реализации не должны сохранять p.
var Discard Writer = discard{}
Discard — это Writer, на котором все вызовы Write выполняются успешно, не выполняя никаких действий.
Объяснение Writer
Интерфейс Writer в Go — это простой контракт для записи данных куда-либо. Он требует реализации всего одного метода:
Write(p []byte) (n int, err error).
Как это работает?
-
Вы передаёте данные
Метод принимает байтовый слайс (p []byte), который нужно записать (например: текст, файл, сетевой пакет).
-
Он пытается записать
Записывает часть или все данные из p в целевое место (файл, память, сеть и т.д.).
-
Возвращает результат
n — сколько байт удалось записать (может быть меньше, чем len(p) при ошибке).
err — ошибка (например, диск заполнен, соединение разорвано).
Аналогия из жизни
Представьте, что Writer — это лестница в доме:
- Люди постепенно заходят на лестницу и поднимаются вверх (
p []byte).
- На последнем этаже большой зал, люди заполняют его.
- Если зал заполнился полностью людьми, то часть останется на лестнице и в зал не войдут (
n < len(p) и err != nil).
Где используется?
Примеры реализаций:
-
Запись в файл
file, _ := os.Create("log.txt")
file.Write([]byte("Hello!")) // Реализует Writer
-
Отправка данных по сети
conn, _ := net.Dial("tcp", "example.com:80")
conn.Write([]byte("GET / HTTP/1.1\r\n\r\n"))
-
Буфер в памяти
var buf bytes.Buffer
buf.Write([]byte("Сохраняем в RAM"))
-
Игнорирование данных (Discard)
io.Discard.Write([]byte("Эти данные никуда не пойдут"))
Правила для Write
-
Не изменяет p
Даже временно. Ваши исходные данные останутся целыми.
-
Не сохраняет p
После завершения метода реализация не должна хранить ссылку на переданные данные.
-
Ошибка = неполная запись
Если вернулось n < len(p), метод обязан вернуть ошибку.
Особый случай: Discard
Это “пустышка”, которая реализует Writer, но просто выбрасывает все записываемые данные:
io.Discard.Write([]byte("Это исчезнет")) // Никуда не запишется
Зачем нужно? Например:
- Когда нужно прочитать данные, но не сохранять их.
- Для тестирования, чтобы имитировать запись без реальных операций.
Пример с кастомным Writer
Создадим простой Writer, который пишет данные в консоль:
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(p []byte) (int, error) {
n, err := fmt.Print(string(p))
return n, err
}
func main() {
var writer Writer = ConsoleWriter{}
writer.Write([]byte("Привет, Writer!"))
}
Вывод:
func MultiWriter
func MultiWriter(writers ...Writer) Writer
MultiWriter создает писатель, который дублирует свои записи во всех предоставленных писателях, аналогично команде Unix tee(1).
Каждая запись записывается в каждый из перечисленных writer, по одному за раз. Если перечисленный writer возвращает ошибку, вся операция записи останавливается и возвращает ошибку; она не продолжается по списку.
Пример
package main
import (
"fmt"
"io"
"log"
"strings"
)
func main() {
r := strings.NewReader("some io.Reader stream to be read\n")
var buf1, buf2 strings.Builder
w := io.MultiWriter(&buf1, &buf2)
if _, err := io.Copy(w, r); err != nil {
log.Fatal(err)
}
fmt.Print(buf1.String())
fmt.Print(buf2.String())
}
Output:
some io.Reader stream to be read
some io.Reader stream to be read
type WriterAt
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}
WriterAt — это интерфейс, который оборачивает базовый метод WriteAt.
WriteAt записывает len(p) байт из p в базовый поток данных со смещением off. Он возвращает количество байт, записанных из p (0 <= n <= len(p)), и любую ошибку, которая привела к преждевременному прекращению записи. WriteAt должен возвращать ошибку, отличную от nil, если он возвращает n < len(p).
Если WriteAt записывает в место назначения со смещением поиска, WriteAt не должен влиять на базовое смещение поиска и не должен подвергаться его влиянию.
Клиенты WriteAt могут выполнять параллельные вызовы WriteAt на одном и том же месте назначения, если диапазоны не пересекаются.
Реализации не должны сохранять p.
type WriterTo
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
WriterTo — это интерфейс, который оборачивает метод WriteTo.
WriteTo записывает данные в w до тех пор, пока не закончатся данные для записи или не произойдет ошибка. Возвращаемое значение n — это количество записанных байтов. Любая ошибка, возникшая во время записи, также возвращается.
Функция Copy использует WriterTo, если он доступен.
Объяснение WriterTo
Интерфейс WriterTo — это продвинутая версия записи данных, которая позволяет объекту самому решать, как эффективно отправить свои данные в любой Writer (файл, сеть, буфер и т.д.).
Чем отличается от обычного Writer?
| Особенность |
Writer (простой интерфейс) |
WriterTo (продвинутый интерфейс) |
| Кто управляет? |
Получатель (Writer) решает, как записать данные |
Источник данных сам решает, как отправить данные |
| Эффективность |
Может требовать промежуточных копирований |
Позволяет оптимизировать запись (например, отправлять данные кусками) |
| Использование |
Базовый уровень |
Оптимизированные сценарии (например, io.Copy) |
Как работает метод WriteTo?
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
-
Вызывается у объекта-источника
Объект (например, буфер или файл) получает целевой Writer (w), куда нужно записать данные.
-
Сам решает, как писать
Источник может:
- Отправить данные одним куском
- Разбить на части
- Использовать специальные оптимизации (например, DMA для дисков)
-
Возвращает результат
n — сколько байт было записано
err — ошибка (если что-то пошло не так)
Примеры использования
1. Стандартные типы с WriterTo
// bytes.Buffer реализует WriterTo
buf := bytes.NewBufferString("Hello!")
buf.WriteTo(os.Stdout) // Выведет "Hello!" в консоль
// strings.Reader тоже реализует WriterTo
reader := strings.NewReader("Text")
reader.WriteTo(file) // Запишет "Text" в файл
2. Кастомная реализация
Создадим объект, который знает, как эффективно записать свои данные:
type CustomData struct {
chunks [][]byte
}
func (d *CustomData) WriteTo(w io.Writer) (int64, error) {
var total int64
for _, chunk := range d.chunks {
n, err := w.Write(chunk)
total += int64(n)
if err != nil {
return total, err
}
}
return total, nil
}
// Использование:
data := CustomData{chunks: [][]byte{[]byte("Hello "), []byte("world!")}}
data.WriteTo(os.Stdout) // Выведет "Hello world!"
Зачем это нужно?
-
Оптимизация производительности
Объект может выбрать самый эффективный способ записи (например, избежать лишних копирований).
-
Гибкость
Разные типы данных могут по-разному реализовывать запись:
- Файл может отправлять данные кусками
- Буфер в памяти — одним вызовом
- Сетевое соединение — с контролем скорости
-
Интеграция с io.Copy
Функция io.Copy(dst, src) автоматически использует WriteTo, если он есть у источника:
// Если src реализует WriterTo, Copy вызовет src.WriteTo(dst)
io.Copy(file, buffer) // Будет эффективнее, чем обычное копирование
Особый случай: Discard
Как и с Writer, существует “пустышка”:
io.Discard.WriteTo(...) // Ничего не делает
Используется для тестирования или игнорирования данных.
4.9.3 - Пакет для работы с файловой системой io/fs
“Пакет fs определяет базовые интерфейсы для файловой системы. Файловая система может быть предоставлена операционной системой хоста, а также другими пакетами.”
Для поддержки тестирования реализаций файловых систем смотрите пакет testing/fstest.
Переменные
var (
ErrInvalid = errInvalid() // "недопустимый аргумент"
ErrPermission = errPermission() // "разрешение отклонено"
ErrExist = errExist() // "файл уже существует"
ErrNotExist = errNotExist() // "файл не существует"
ErrClosed = errClosed() // "файл уже закрыт"
)
Общие ошибки файловой системы. Ошибки, возвращаемые файловыми системами, можно проверить на соответствие этим ошибкам с помощью errors.Is.
var SkipAll = errors.New("пропустить все и остановить прогулку")
SkipAll используется как возвращаемое значение из WalkDirFunc, чтобы указать, что все оставшиеся файлы и каталоги должны быть пропущены. Ни одна функция не возвращает его в качестве ошибки.
var SkipDir = errors.New("пропустить этот каталог")
SkipDir используется в качестве возвращаемого значения WalkDirFunc, чтобы указать, что каталог, названный в вызове, должен быть пропущен. Ни одна функция не возвращает его в качестве ошибки.
Функции
func FormatDirEntry
func FormatDirEntry(dir DirEntry) string
FormatDirEntry возвращает отформатированную версию dir для удобства чтения. Реализации DirEntry могут вызывать эту функцию из метода String. Результаты для каталога с именем subdir и файла с именем hello.go:
func FormatFileInfo(info FileInfo) string
FormatFileInfo возвращает отформатированную версию info для удобства чтения. Реализации FileInfo могут вызывать эту функцию из метода String. Результатом для файла с именем «hello.go», размером 100 байт, режимом 0o644, созданного 1 января 1970 года в полдень, будет
-rw-r--r-- 100 1970-01-01 12:00:00 hello.go
func Glob
func Glob(fsys FS, pattern string) (matches []string, err error)
Glob возвращает имена всех файлов, соответствующих pattern, или nil, если нет соответствующих файлов. Синтаксис шаблонов такой же, как в path.Match. Шаблон может описывать иерархические имена, такие как usr/*/bin/ed.
Glob игнорирует ошибки файловой системы, такие как ошибки ввода-вывода при чтении каталогов. Единственная возможная возвращаемая ошибка — path.ErrBadPattern, сообщающая о неверном формате шаблона.
Если fs реализует GlobFS, Glob вызывает fs.Glob. В противном случае Glob использует ReadDir для обхода дерева каталогов и поиска совпадений с шаблоном.
func ReadFile
func ReadFile(fsys FS, name string) ([]byte, error)
ReadFile читает файл с указанным именем из файловой системы fs и возвращает его содержимое. Успешный вызов возвращает ошибку nil, а не io.EOF. (Поскольку ReadFile читает весь файл, ожидаемый EOF от последнего Read не рассматривается как ошибка, о которой следует сообщать).
Если fs реализует ReadFileFS, ReadFile вызывает fs.ReadFile. В противном случае ReadFile вызывает fs.Open и использует Read и Close на возвращенном File.
func ValidPath
func ValidPath(name string) bool
ValidPath сообщает, является ли данное имя пути действительным для использования в вызове Open.
Имена путей, передаваемые в open, представляют собой закодированные в UTF-8, не имеющие корня, разделенные косой чертой последовательности элементов пути, например «x/y/z». Имена путей не должны содержать элемент «.» или «..» или пустую строку, за исключением особого случая, когда имя «.» может использоваться для корневого каталога. Пути не должны начинаться или заканчиваться косой чертой: «/x» и «x/» являются недопустимыми.
Обратите внимание, что пути разделяются косой чертой во всех системах, даже в Windows. Пути, содержащие другие символы, такие как обратная косая черта и двоеточие, принимаются как действительные, но эти символы никогда не должны интерпретироваться реализацией FS как разделители элементов пути.
func WalkDir
func WalkDir(fsys FS, root string, fn WalkDirFunc) error
WalkDir проходит по дереву файлов, корнем которого является root, вызывая fn для каждого файла или каталога в дереве, включая root.
Все ошибки, возникающие при посещении файлов и каталогов, фильтруются fn: подробности см. в документации fs.WalkDirFunc.
Файлы просматриваются в лексическом порядке, что делает вывод детерминированным, но требует, чтобы WalkDir считывал весь каталог в память, прежде чем приступать к просмотру этого каталога.
WalkDir не следует по символьным ссылкам, найденным в каталогах, но если root сам является символьной ссылкой, то будет пройдена его цель.
Пример
package main
import (
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"time"
)
func main() {
// Создаем временную файловую систему для демонстрации
tmpDir, err := os.MkdirTemp("", "fs-example-*")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Создаем тестовые файлы и директории
createTestFS(tmpDir)
// Получаем файловую систему os.DirFS
fsys := os.DirFS(tmpDir)
// 1. Пример использования FormatDirEntry и FormatFileInfo
demoFormatFunctions(fsys)
// 2. Пример использования Glob
demoGlob(fsys)
// 3. Пример использования ReadFile
demoReadFile(fsys)
// 4. Пример использования ValidPath
demoValidPath()
// 5. Пример использования WalkDir
demoWalkDir(fsys)
}
func createTestFS(root string) {
// Создаем структуру:
// /tmp/fs-example-12345/
// ├── docs/
// │ ├── notes.md
// │ └── draft.txt
// ├── src/
// │ └── main.go
// └── README.txt
dirs := []string{
filepath.Join(root, "docs"),
filepath.Join(root, "src"),
}
files := map[string]string{
filepath.Join(root, "README.txt"): "Пример файла README",
filepath.Join(root, "docs", "notes.md"): "# Заметки\nПример содержимого",
filepath.Join(root, "docs", "draft.txt"): "Черновик документа",
filepath.Join(root, "src", "main.go"): "package main\n\nfunc main() {\n\tprintln(\"Hello\")\n}",
}
// Создаем директории
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Fatal(err)
}
}
// Создаем файлы
for path, content := range files {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
log.Fatal(err)
}
}
// Устанавливаем время модификации для демонстрации FormatFileInfo
modTime := time.Date(2023, time.January, 15, 12, 30, 0, 0, time.UTC)
for path := range files {
if err := os.Chtimes(path, modTime, modTime); err != nil {
log.Fatal(err)
}
}
}
func demoFormatFunctions(fsys fs.FS) {
fmt.Println("\n=== FormatDirEntry и FormatFileInfo ===")
entries, err := fs.ReadDir(fsys, ".")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
// FormatDirEntry
fmt.Printf("DirEntry: %s\n", fs.FormatDirEntry(entry))
// Получаем FileInfo для FormatFileInfo
info, err := entry.Info()
if err != nil {
log.Fatal(err)
}
fmt.Printf("FileInfo: %s\n", fs.FormatFileInfo(info))
}
}
func demoGlob(fsys fs.FS) {
fmt.Println("\n=== Glob ===")
// Ищем все .txt файлы
matches, err := fs.Glob(fsys, "*.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("Файлы .txt в корне:", matches)
// Ищем все .go файлы в любых поддиректориях
matches, err = fs.Glob(fsys, "*/*.go")
if err != nil {
log.Fatal(err)
}
fmt.Println("Файлы .go в поддиректориях:", matches)
// Ищем все .md файлы рекурсивно
matches, err = fs.Glob(fsys, "**/*.md")
if err != nil {
log.Fatal(err)
}
fmt.Println("Файлы .md рекурсивно:", matches)
}
func demoReadFile(fsys fs.FS) {
fmt.Println("\n=== ReadFile ===")
// Читаем содержимое файла
content, err := fs.ReadFile(fsys, "src/main.go")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Содержимое main.go:\n%s\n", content)
// Попытка чтения несуществующего файла
_, err = fs.ReadFile(fsys, "nonexistent.txt")
fmt.Println("Ошибка при чтении несуществующего файла:", err)
}
func demoValidPath() {
fmt.Println("\n=== ValidPath ===")
paths := []string{
"valid/path",
"invalid/../path",
"invalid\\path", // Обратные слеши не считаются разделителями
"",
"trailing/slash/",
}
for _, path := range paths {
fmt.Printf("%q valid: %t\n", path, fs.ValidPath(path))
}
}
func demoWalkDir(fsys fs.FS) {
fmt.Println("\n=== WalkDir ===")
fmt.Println("Содержимое файловой системы:")
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Printf("- %s\n", fs.FormatDirEntry(d))
return nil
})
if err != nil {
log.Fatal(err)
}
}
=== FormatDirEntry и FormatFileInfo ===
DirEntry: d docs/
FileInfo: drwxr-xr-x 4096 2023-01-15 12:30:00 docs
DirEntry: d src/
FileInfo: drwxr-xr-x 4096 2023-01-15 12:30:00 src
DirEntry: - README.txt
FileInfo: -rw-r--r-- 19 2023-01-15 12:30:00 README.txt
=== Glob ===
Файлы .txt в корне: [README.txt]
Файлы .go в поддиректориях: [src/main.go]
Файлы .md рекурсивно: [docs/notes.md]
=== ReadFile ===
Содержимое main.go:
package main
func main() {
println("Hello")
}
Ошибка при чтении несуществующего файла: open nonexistent.txt: file does not exist
=== ValidPath ===
"valid/path" valid: true
"invalid/../path" valid: false
"invalid\\path" valid: true
"" valid: false
"trailing/slash/" valid: false
=== WalkDir ===
Содержимое файловой системы:
- .
- d docs/
- - docs/draft.txt
- - docs/notes.md
- d src/
- - src/main.go
- - README.txt
Типы
type DirEntry
type DirEntry interface {
// Name возвращает имя файла (или подкаталога), описанного записью.
// Это имя является только конечным элементом пути (базовым именем), а не всем путем.
// Например, Name вернет «hello.go», а не «home/gopher/hello.go».
Name() string
// IsDir сообщает, описывает ли запись каталог.
IsDir() bool
// Type возвращает биты типа для запис
// Биты типа являются подмножеством обычных битов FileMode, возвращаемых методом FileMode.Type.
Type() FileMode
// Info возвращает FileInfo для файла или подкаталога, описанного записью.
// Возвращаемая информация FileInfo может быть взята из времени первоначального чтения каталога
// или из времени вызова Info. Если файл был удален или переименован
// после чтения каталога, Info может вернуть ошибку, удовлетворяющую errors.Is(err, ErrNotExist).
// Если запись обозначает символическую ссылку, Info сообщает информацию о самой ссылке,
// а не о цели ссылки.
Info() (FileInfo, error)
}
DirEntry — это запись, прочитанная из каталога (с помощью функции ReadDir или метода ReadDir класса ReadDirFile).
func FileInfoToDirEntry
func FileInfoToDirEntry(info FileInfo) DirEntry
FileInfoToDirEntry возвращает DirEntry, который возвращает информацию из info. Если info равно nil, FileInfoToDirEntry возвращает nil.
func ReadDir
func ReadDir(fsys FS, name string) ([]DirEntry, error)
ReadDir считывает указанный каталог и возвращает список записей каталога, отсортированных по имени файла.
Если fs реализует ReadDirFS, ReadDir вызывает fs.ReadDir. В противном случае ReadDir вызывает fs.Open и использует ReadDir и Close для возвращенного файла.
type FS
type FS interface {
// Open открывает указанный файл.
// [File.Close] должен быть вызван для освобождения всех связанных ресурсов.
//
// Когда Open возвращает ошибку, она должна быть типа *PathError
// с полем Op, установленным в «open», полем Path, установленным в name,
// и полем Err, описывающим проблему.
//
// Open должен отклонять попытки открыть имена, которые не удовлетворяют
// ValidPath(name), возвращая *PathError с Err, установленным в
// ErrInvalid или ErrNotExist.
Open(name string) (File, error)
}
FS обеспечивает доступ к иерархической файловой системе.
Интерфейс FS является минимальной реализацией, необходимой для файловой системы. Файловая система может реализовывать дополнительные интерфейсы, такие как ReadFileFS, для предоставления дополнительных или оптимизированных функций.
testing/fstest.TestFS может использоваться для проверки правильности реализации FS.
Объяснение FS
Объяснение интерфейса fs.FS
fs.FS - это базовый интерфейс в Go для работы с иерархическими файловыми системами. Он представляет минимальный контракт, который должна реализовать любая файловая система (реальная, виртуальная, в памяти и т.д.).
Ключевые особенности:
- Минималистичный дизайн - только один обязательный метод
Open()
- Абстракция - позволяет работать с разными ФС одинаковым способом
- Расширяемость - через дополнительные интерфейсы (
ReadFileFS, GlobFS и др.)
Основной метод:
Open(name string) (File, error)
- Открывает файл по имени
- Возвращает объект, реализующий
fs.File
- В случае ошибки возвращает
*fs.PathError
Пример реализации и использования
1. Создаем простую in-memory файловую систему
package main
import (
"io/fs"
"log"
"os"
"time"
)
// MemoryFS - простая in-memory реализация fs.FS
type MemoryFS struct {
files map[string]*MemoryFile
}
type MemoryFile struct {
name string
content []byte
mode fs.FileMode
modTime time.Time
}
func (m *MemoryFile) Stat() (fs.FileInfo, error) {
return &MemoryFileInfo{m}, nil
}
func (m *MemoryFile) Read(p []byte) (int, error) {
// Реализация чтения
}
func (m *MemoryFile) Close() error {
return nil
}
// MemoryFileInfo реализует fs.FileInfo
type MemoryFileInfo struct {
file *MemoryFile
}
func (m *MemoryFileInfo) Name() string { return m.file.name }
func (m *MemoryFileInfo) Size() int64 { return int64(len(m.file.content)) }
func (m *MemoryFileInfo) Mode() fs.FileMode { return m.file.mode }
func (m *MemoryFileInfo) ModTime() time.Time { return m.file.modTime }
func (m *MemoryFileInfo) IsDir() bool { return m.file.mode.IsDir() }
func (m *MemoryFileInfo) Sys() interface{} { return nil }
// Open реализует fs.FS
func (mfs *MemoryFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrInvalid,
}
}
file, exists := mfs.files[name]
if !exists {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
}
return file, nil
}
2. Используем нашу реализацию
func main() {
// Инициализируем нашу ФС
mfs := &MemoryFS{
files: map[string]*MemoryFile{
"hello.txt": {
name: "hello.txt",
content: []byte("Hello, MemoryFS!"),
mode: 0644,
modTime: time.Now(),
},
"dir": {
name: "dir",
mode: fs.ModeDir | 0755,
modTime: time.Now(),
},
},
}
// Пример 1: Открываем и читаем файл
file, err := mfs.Open("hello.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
info, _ := file.Stat()
fmt.Printf("Файл: %s, размер: %d\n", info.Name(), info.Size())
// Пример 2: Пытаемся открыть несуществующий файл
_, err = mfs.Open("missing.txt")
if err != nil {
fmt.Printf("Ошибка: %v\n", err) // Выведет PathError
}
// Пример 3: Используем с другими функциями пакета fs
content, err := fs.ReadFile(mfs, "hello.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Содержимое: %s\n", content)
}
3. Пример вывода:
Файл: hello.txt, размер: 15
Ошибка: open missing.txt: file does not exist
Содержимое: Hello, MemoryFS!
Где используется fs.FS?
-
В стандартной библиотеке:
os.DirFS - доступ к реальной файловой системе
embed.FS - доступ к встроенным файлам
http.FileSystem - интеграция с веб-сервером
-
В популярных библиотеках:
- Виртуальные файловые системы
- Работа с архивами (zip, tar)
- Тестирование (testing/fstest)
-
В пользовательских реализациях:
- ФС в памяти
- ФС поверх облачного хранилища
- ФС для специализированных форматов
Преимущества такого подхода:
- Единообразие - один интерфейс для разных ФС
- Тестируемость - легко подменять реальную ФС на mock
- Гибкость - можно комбинировать разные реализации
Интерфейс fs.FS стал стандартным способом работы с файловыми системами в Go 1.16+, заменив множество специализированных решений.
func Sub
func Sub(fsys FS, dir string) (FS, error)
Sub возвращает FS, соответствующую поддереву с корнем в fsys’s dir.
Если dir равна «.», Sub возвращает fsys без изменений. В противном случае, если fs реализует SubFS, Sub возвращает fsys.Sub(dir). В противном случае Sub возвращает новую реализацию FS sub, которая фактически реализует sub.Open(name) как fsys.Open(path.Join(dir, name)). Реализация также соответствующим образом преобразует вызовы ReadDir, ReadFile и Glob.
Обратите внимание, что Sub(os.DirFS(«/»), „prefix“) эквивалентно os.DirFS(«/prefix») и что ни одно из них не гарантирует отсутствие доступа операционной системы за пределами «/prefix», поскольку реализация os.DirFS не проверяет символьные ссылки внутри «/prefix», которые указывают на другие каталоги. То есть os.DirFS не является общей заменой механизма безопасности типа chroot, и Sub не меняет этот факт.
type File
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
File предоставляет доступ к одному файлу. Интерфейс File является минимальной реализацией, требуемой для файла. Файлы каталогов также должны реализовывать ReadDirFile. Файл может реализовывать io.ReaderAt или io.Seeker в качестве оптимизаций.
type FileInfo
type FileInfo interface {
Name() string // базовое имя файла
Size() int64 // длина в байтах для обычных файлов; зависит от системы для других
Mode() FileMode // биты режима файла
ModTime() time.Time // время изменения
IsDir() bool // сокращение для Mode().IsDir()
Sys() any // базовый источник данных (может возвращать nil)
}
FileInfo описывает файл и возвращается Stat.
func Stat
func Stat(fsys FS, name string) (FileInfo, error)
Stat возвращает FileInfo, описывающий файл с указанным именем из файловой системы.
Если fs реализует StatFS, Stat вызывает fs.Stat. В противном случае Stat открывает файл для его статистики.
type FileMode
FileMode представляет режим файла и биты разрешений. Биты имеют одинаковое определение во всех системах, поэтому информация о файлах может быть перенесена из одной системы в другую. Не все биты применимы ко всем системам. Единственный обязательный бит — ModeDir для каталогов.
const (
// Одиночные буквы являются аббревиатурами,
// используемыми для форматирования методом String.
ModeDir FileMode = 1 << (32 - 1 - iota) // d: является каталогом
ModeAppend // a: только для добавления
ModeExclusive // l: исключительное использование
ModeTemporary // T: временный файл; только Plan 9
ModeSymlink // L: символическая ссылка
ModeDevice // D: файл устройства
ModeNamedPipe // p: именованный канал (FIFO)
ModeSocket // S: сокет домена Unix
ModeSetuid // u: setuid
ModeSetgid // g: setgid
ModeCharDevice // c: символьное устройство Unix, когда установлен ModeDevice
ModeSticky // t: sticky
ModeIrregular // ?: нерегулярный файл; ничего больше не известно об этом файле
// Маска для битов типа. Для обычных файлов не будет установлено ничего.
ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular
ModePerm FileMode = 0777 // биты разрешений Unix)
Определенные биты режима файла являются наиболее значимыми битами FileMode. Девять наименее значимых битов являются стандартными разрешениями Unix rwxrwxrwx. Значения этих битов следует рассматривать как часть общедоступного API и могут использоваться в протоколах передачи данных или представлениях диска: они не должны изменяться, хотя могут быть добавлены новые биты.
func (FileMode) IsDir
func (m FileMode) IsDir() bool
IsDir сообщает, описывает ли m каталог. То есть, он проверяет, установлен ли бит ModeDir в m.
func (FileMode) IsRegular
func (m FileMode) IsRegular() bool
IsRegular сообщает, описывает ли m обычный файл. То есть, он проверяет, что биты типа режима не установлены.
func (FileMode) Perm
func (m FileMode) Perm() FileMode
Perm возвращает биты разрешений Unix в m (m & ModePerm).
func (FileMode) String
func (m FileMode) String() string
func (FileMode) Type
func (m FileMode) Type() FileMode
Type возвращает биты типа в m (m & ModeType).
type GlobFS
type GlobFS interface {
FS
// Glob возвращает имена всех файлов, соответствующих шаблону,
// предоставляя реализацию функции верхнего уровня
// Glob.
Glob(pattern string) ([]string, error)
}
GlobFS — это файловая система с методом Glob.
type PathError
type PathError struct {
Op string
Path string
Err error
}
PathError регистрирует ошибку, а также операцию и путь к файлу, которые ее вызвали.
func (*PathError) Error
func (e *PathError) Error() string
func (*PathError) Timeout
func (e *PathError) Timeout() bool
Timeout сообщает, является ли эта ошибка тайм-аутом.
func (*PathError) Unwrap
func (e *PathError) Unwrap() error
type ReadDirFS
type ReadDirFS interface {
FS
// ReadDir читает указанный каталог
// и возвращает список записей каталога, отсортированных по имени файла.
ReadDir(name string) ([]DirEntry, error)
}
ReadDirFS — это интерфейс, реализованный файловой системой, который обеспечивает оптимизированную реализацию ReadDir.
type ReadDirFile
type ReadDirFile interface {
file
// ReadDir считывает содержимое каталога и возвращает
// массив из n значений DirEntry в порядке каталога.
// Последующие вызовы для того же файла будут возвращать дальнейшие значения DirEntry.
//
// Если n > 0, ReadDir возвращает не более n структур DirEntry.
// В этом случае, если ReadDir возвращает пустой срез, он вернет
// не нулевую ошибку с объяснением причины.
// В конце каталога ошибкой будет io.EOF.
// (ReadDir должен возвращать io.EOF сам, а не ошибку, оборачивающую io.EOF.)
//
// Если n <= 0, ReadDir возвращает все значения DirEntry из каталога
// в одном срезе. В этом случае, если ReadDir завершается успешно (считывает все
// до конца каталога), он возвращает срез и ошибку nil.
// Если он встречает ошибку до конца каталога,
// ReadDir возвращает список DirEntry, прочитанный до этого момента, и ошибку, отличную от nil.
ReadDir(n int) ([]DirEntry, error)
}
ReadDirFile — это файл каталога, записи которого можно прочитать с помощью метода ReadDir. Каждый файл каталога должен реализовывать этот интерфейс. (Любой файл может реализовывать этот интерфейс, но в этом случае ReadDir должен возвращать ошибку для некаталогов.)
type ReadFileFS
type ReadFileFS interface {
FS
// ReadFile читает указанный файл и возвращает его содержимое.
// Успешный вызов возвращает ошибку nil, а не io.EOF.
// (Поскольку ReadFile считывает весь файл, ожидаемый EOF
// от последнего Read не рассматривается как ошибка, о которой следует сообщать.)
//
// Вызывающему разрешается изменять возвращаемый байтовый срез.
// Этот метод должен возвращать копию базовых данных.
ReadFile(name string) ([]byte, error)
}
ReadFileFS — это интерфейс, реализованный файловой системой, который обеспечивает оптимизированную реализацию ReadFile.
type StatFS
type StatFS interface {
FS
// Stat возвращает FileInfo, описывающий файл.
// Если происходит ошибка, она должна быть типа *PathError.
Stat(name string) (FileInfo, error)
}
StatFS — это файловая система с методом Stat.
type SubFS
type SubFS interface {
FS
// Sub возвращает FS, соответствующий поддереву с корнем в dir.
Sub(dir string) (FS, error)
}
SubFS — это файловая система с методом Sub.
type WalkDirFunc
type WalkDirFunc func(path string, d DirEntry, err error) error
WalkDirFunc — это тип функции, вызываемой WalkDir для посещения каждого файла или каталога.
Аргумент path содержит аргумент WalkDir в качестве префикса. То есть, если WalkDir вызывается с корневым аргументом «dir» и находит файл с именем «a» в этом каталоге, функция walk будет вызвана с аргументом «dir/a».
Аргумент d — это DirEntry для указанного пути.
Результат error, возвращаемый функцией, контролирует продолжение работы WalkDir. Если функция возвращает специальное значение SkipDir, WalkDir пропускает текущий каталог (path, если d.IsDir() равно true, в противном случае — родительский каталог path). Если функция возвращает специальное значение SkipAll, WalkDir пропускает все оставшиеся файлы и каталоги. В противном случае, если функция возвращает ошибку, отличную от nil, WalkDir полностью останавливается и возвращает эту ошибку.
Аргумент err сообщает об ошибке, связанной с путем, сигнализируя, что WalkDir не будет проходить в этот каталог. Функция может решить, как обработать эту ошибку; как описано ранее, возврат ошибки приведет к тому, что WalkDir прекратит прохождение всего дерева.
WalkDir вызывает функцию с аргументом err, отличным от nil, в двух случаях.
Во-первых, если первоначальная Stat в корневом каталоге завершается с ошибкой, WalkDir вызывает функцию с path, установленным в root, d, установленным в nil, и err, установленным в ошибку из fs.Stat.
Во-вторых, если метод ReadDir каталога (см. ReadDirFile) завершается с ошибкой, WalkDir вызывает функцию с path, установленным в путь каталога, d, установленным в DirEntry, описывающий каталог, и err, установленным в ошибку из ReadDir. Во втором случае функция вызывается дважды с путем к каталогу: первый вызов происходит до попытки чтения каталога, и err установлен в nil, что дает функции возможность вернуть SkipDir или SkipAll и полностью избежать ReadDir. Второй вызов происходит после неудачного ReadDir и сообщает об ошибке из ReadDir. (Если ReadDir завершается успешно, второго вызова не происходит.)
Различия между WalkDirFunc и path/filepath.WalkFunc заключаются в следующем:
Второй аргумент имеет тип DirEntry вместо FileInfo.
Функция вызывается перед чтением каталога, чтобы SkipDir или SkipAll могли полностью обойти чтение каталога или пропустить все оставшиеся файлы и каталоги соответственно.
Если чтение каталога завершилось неудачно, функция вызывается второй раз для этого каталога, чтобы сообщить об ошибке.
4.10 - Пакет Sync для Go
Пакет sync предоставляет базовые примитивы синхронизации, такие как взаимоисключающие блокировки.
За исключением типов Once и WaitGroup, большинство из них предназначены для использования низкоуровневыми библиотечными процедурами. Синхронизацию более высокого уровня лучше выполнять через каналы и коммуникации.
Значения, содержащие типы, определенные в этом пакете, не должны копироваться.
Функции
func OnceFunc
func OnceFunc(f func()) func()
OnceFunc возвращает функцию, которая вызывает f только один раз. Возвращаемая функция может вызываться одновременно.
Если f вызывает панику, возвращаемая функция будет вызывать панику с тем же значением при каждом вызове.
func OnceValue
func OnceValue[T any](f func() T) func() T
OnceValue возвращает функцию, которая вызывает f только один раз и возвращает значение, возвращаемое f. Возвращаемая функция может вызываться одновременно.
Если f вызывает панику, возвращаемая функция будет вызывать панику с тем же значением при каждом вызове.
Пример
В этом примере OnceValue используется для выполнения «дорогостоящего» вычисления только один раз, даже при одновременном использовании.
package main
import (
"fmt"
"sync"
)
func main() {
once := sync.OnceValue(func() int {
sum := 0
for i := 0; i < 1000; i++ {
sum += i
}
fmt.Println("Computed once:", sum)
return sum
})
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
const want = 499500
got := once()
if got != want {
fmt.Println("want", want, "got", got)
}
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Output:
Computed once: 499500
func OnceValues
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
OnceValues возвращает функцию, которая вызывает f только один раз и возвращает значения, возвращаемые f. Возвращаемая функция может вызываться одновременно.
Если f вызывает панику, возвращаемая функция будет вызывать панику с тем же значением при каждом вызове.
Пример
В этом примере используется OnceValues для однократного чтения файла.
package main
import (
"fmt"
"os"
"sync"
)
func main() {
once := sync.OnceValues(func() ([]byte, error) {
fmt.Println("Reading file once")
return os.ReadFile("example_test.go")
})
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
data, err := once()
if err != nil {
fmt.Println("error:", err)
}
_ = data // Ignore the data for this example
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Типы
type Cond
type Cond struct {
// L удерживается во время наблюдения или изменения условия
L Locker
// содержит отфильтрованные или неэкспортируемые поля
}
Cond реализует переменную условия, точку встречи для goroutines, ожидающих или объявляющих о наступлении события.
Каждый Cond имеет связанный с ним Locker L (часто *Mutex или *RWMutex), который должен удерживаться при изменении условия и при вызове метода Cond.Wait.
Cond не должен копироваться после первого использования.
В терминологии модели памяти Go, Cond организует так, что вызов Cond.Broadcast или Cond.Signal «синхронизируется перед» любым вызовом Wait, который он разблокирует.
Для многих простых случаев использования пользователям будет удобнее использовать каналы, чем Cond (Broadcast соответствует закрытию канала, а Signal — отправке по каналу).
Для получения дополнительной информации о заменах sync.Cond см. серию статей Роберто Клаписа о расширенных моделях параллелизма, а также доклад Брайана Миллса о моделях параллелизма.
func NewCond
func NewCond(l Locker) *Cond
NewCond возвращает новый Cond с Locker l.
func (*Cond) Broadcast
func (c *Cond) Broadcast()
Broadcast пробуждает все goroutines, ожидающие c.
Вызывающему разрешается, но не требуется, удерживать c.L во время вызова.
func (*Cond) Signal
Signal пробуждает одну goroutine, ожидающую c, если таковая имеется.
Вызывающему разрешается, но не требуется, удерживать c.L во время вызова.
Signal() не влияет на приоритет планирования goroutine; если другие goroutines пытаются заблокировать c.L, они могут быть пробуждены раньше «ожидающей» goroutine.
func (*Cond) Wait
Wait атомарно разблокирует c.L и приостанавливает выполнение вызывающего goroutine. После возобновления выполнения Wait блокирует c.L перед возвратом. В отличие от других систем, Wait не может вернуться, если его не разбудит Cond.Broadcast или Cond.Signal.
Поскольку c.L не блокируется во время ожидания Wait, вызывающий обычно не может предполагать, что условие будет выполнено, когда Wait вернется. Вместо этого вызывающий должен ждать в цикле:
c.L.Lock()
for !condition() {
c.Wait()
}...
использовать условие ...
c.L.Unlock()
Подробное объяснение типа Cond
Что такое Cond?
Cond (от слова “condition” - условие) - это примитив синхронизации из пакета sync, который позволяет горутинам ожидать или объявлять о наступлении некоторого события.
Проще говоря, Cond нужен для того, чтобы:
- Одни горутины могли “уснуть” и ждать какого-то условия
- Другие горутины могли их “разбудить”, когда это условие выполнится
Основные методы:
Wait() - блокирует горутину до получения уведомления
Signal() - пробуждает одну случайную горутину из ожидающих
Broadcast() - пробуждает все ожидающие горутины
Зачем это нужно?
Cond особенно полезен в ситуациях, где:
- Несколько горутин ожидают какого-то общего условия
- Состояние может измениться в любой момент
- Вы не хотите постоянно опрашивать условие в цикле (busy waiting)
Реальный пример: Ограниченная очередь
Допустим, у нас есть очередь с ограниченным размером, и мы хотим:
- Блокировать писателей, когда очередь полна
- Блокировать читателей, когда очередь пуста
package main
import (
"fmt"
"sync"
"time"
)
type BoundedQueue struct {
mu sync.Mutex
cond *sync.Cond
queue []int
size int
cap int
}
func NewBoundedQueue(capacity int) *BoundedQueue {
q := &BoundedQueue{
queue: make([]int, 0, capacity),
cap: capacity,
}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *BoundedQueue) Put(item int) {
q.mu.Lock()
defer q.mu.Unlock()
// Ждем, пока освободится место
for q.size == q.cap {
q.cond.Wait()
}
q.queue = append(q.queue, item)
q.size++
fmt.Printf("Добавлен элемент %d. Размер очереди: %d\n", item, q.size)
// Уведомляем ожидающих читателей
q.cond.Broadcast()
}
func (q *BoundedQueue) Get() int {
q.mu.Lock()
defer q.mu.Unlock()
// Ждем, пока появится элемент
for q.size == 0 {
q.cond.Wait()
}
item := q.queue[0]
q.queue = q.queue[1:]
q.size--
fmt.Printf("Извлечен элемент %d. Размер очереди: %d\n", item, q.size)
// Уведомляем ожидающих писателей
q.cond.Broadcast()
return item
}
func main() {
queue := NewBoundedQueue(3)
// Писатели
for i := 0; i < 5; i++ {
go func(val int) {
queue.Put(val)
}(i)
}
// Читатели
for i := 0; i < 5; i++ {
go func() {
time.Sleep(1 * time.Second)
queue.Get()
}()
}
time.Sleep(5 * time.Second)
}
Когда использовать Cond вместо каналов?
Cond полезен, когда:
- У вас сложное условие ожидания (не просто “есть данные”)
- Нужно уведомлять сразу несколько горутин
- Состояние может меняться часто и нужно минимизировать накладные расходы
Каналы лучше подходят для более простых случаев передачи данных между горутинами.
sync.Cond - это инструмент для сложных сценариев синхронизации, где горутинам нужно ждать выполнения определенных условий. Он особенно полезен при реализации структур данных с ограничениями (как в нашем примере с очередью), пулов ресурсов или других сценариев, где состояние может меняться и нужно эффективно уведомлять ожидающие горутины.
type Locker
type Locker интерфейс {
Lock()
Unlock()
}
Locker - это интерфейс из пакета sync, который определяет базовые методы для блокировки:
Подробное объяснение типа Locker
Этот интерфейс реализуют:
sync.Mutex - обычная мьютекс-блокировка
sync.RWMutex - блокировка с возможностью множественного чтения
- Любые другие типы, которые реализуют эти два метода
Зачем нужен Locker?
- Унификация работы с разными типами блокировок - вы можете писать функции, которые работают с любым типом блокировки
- Абстракция - позволяет не зависеть от конкретной реализации блокировки
- Тестирование - можно создавать mock-объекты для тестирования
Пример 1: Использование с sync.Mutex
package main
import (
"fmt"
"sync"
"time"
)
func increment(counter *int, locker sync.Locker) {
locker.Lock()
defer locker.Unlock()
*counter++
fmt.Println(*counter)
}
func main() {
var counter int
var mu sync.Mutex
for i := 0; i < 5; i++ {
go increment(&counter, &mu)
}
time.Sleep(1 * time.Second)
}
Пример 2: Собственная реализация Locker
type DebugLocker struct {
mu sync.Mutex
}
func (d *DebugLocker) Lock() {
fmt.Println("Lock acquired")
d.mu.Lock()
}
func (d *DebugLocker) Unlock() {
fmt.Println("Lock released")
d.mu.Unlock()
}
func main() {
var counter int
locker := &DebugLocker{}
for i := 0; i < 5; i++ {
go increment(&counter, locker)
}
time.Sleep(1 * time.Second)
}
Реальный пример использования
Допустим, у нас есть кэш, который может использовать разные виды блокировок:
type Cache struct {
locker sync.Locker
data map[string]string
}
func NewCache(locker sync.Locker) *Cache {
return &Cache{
locker: locker,
data: make(map[string]string),
}
}
func (c *Cache) Set(key, value string) {
c.locker.Lock()
defer c.locker.Unlock()
c.data[key] = value
}
func (c *Cache) Get(key string) (string, bool) {
c.locker.Lock()
defer c.locker.Unlock()
val, ok := c.data[key]
return val, ok
}
func main() {
// Можно использовать обычный Mutex
cache1 := NewCache(&sync.Mutex{})
// Или RWMutex для оптимизации чтения
cache2 := NewCache(&sync.RWMutex{})
// Или даже нашу DebugLocker
cache3 := NewCache(&DebugLocker{})
}Locker представляет объект, который можно заблокировать и разблокировать.
Когда использовать Locker?
- Когда ваша функция/метод должен работать с разными типами блокировок
- Когда вы хотите сделать код более гибким для тестирования
- Когда вы разрабатываете библиотеку и хотите оставить выбор блокировки пользователю
Locker - это простой интерфейс, который позволяет абстрагироваться от конкретного типа блокировки. Он делает ваш код более гибким и переиспользуемым, особенно когда речь идет о конкурентных операциях.
type Map
type Map struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Map похож на Go map[any]any, но безопасен для одновременного использования несколькими goroutines без дополнительной блокировки или координации. Загрузка, хранение и удаление выполняются за амортизированное постоянное время.
Тип Map является специализированным. В большинстве случаев следует использовать обычный Go map с отдельной блокировкой или координацией, чтобы обеспечить лучшую типовую безопасность и упростить поддержание других инвариантов наряду с содержимым карты.
Тип Map оптимизирован для двух распространенных случаев использования:
- когда запись для данного ключа записывается только один раз, но читается много раз, как в кэшах, которые только растут, или
- когда несколько goroutines читают, записывают и перезаписывают записи для непересекающихся наборов ключей.
В этих двух случаях использование Map может значительно уменьшить конфликты блокировок по сравнению с Go map в паре с отдельным Mutex или RWMutex.
Карта с нулевым значением пуста и готова к использованию. Карту нельзя копировать после первого использования.
В терминологии модели памяти Go карта организует так, что операция записи «синхронизируется перед» любой операцией чтения, которая наблюдает эффект записи, где операции чтения и записи определяются следующим образом.
- Map.Load, Map.LoadAndDelete, Map.LoadOrStore, Map.Swap, Map.CompareAndSwap и Map.CompareAndDelete — операции чтения;
- Map.Delete, Map.LoadAndDelete, Map.Store и Map.Swap — операции записи;
- Map.LoadOrStore — операция записи, когда она возвращает загруженный набор, установленный в false;
- Map.CompareAndSwap — операция записи, когда она возвращает помененный набор, установленный в true; и
- Map.CompareAndDelete — операция записи, когда она возвращает удаленный набор, установленный в true.
func (*Map) Clear
Clear удаляет все записи, в результате чего карта становится пустой.
func (*Map) CompareAndDelete
func (m *Map) CompareAndDelete(key, old any) (deleted bool)
CompareAndDelete удаляет запись для ключа, если его значение равно old. Старое значение должно быть сопоставимого типа.
Если в карте нет текущего значения для ключа, CompareAndDelete возвращает false (даже если старое значение является значением интерфейса nil).
Подробное объяснение типа Map и функции CompareAndDelete
Что делает CompareAndDelete?
Метод CompareAndDelete выполняет атомарную операцию:
- Проверяет, соответствует ли текущее значение для указанного ключа
old значению
- Если значения совпадают - удаляет запись из мапы
- Возвращает
true, если удаление произошло, и false в противном случае
Это операция “compare-and-delete” (сравнить и удалить), аналогичная атомарным операциям CAS (Compare-And-Swap).
Как это работает?
deleted := m.CompareAndDelete(key, oldValue)
- Если в мапе
key отсутствует → возвращает false
- Если в мапе
key есть, но значение ≠ oldValue → возвращает false
- Если в мапе
key есть и значение == oldValue → удаляет запись и возвращает true
Пример использования
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Добавляем значения в мапу
m.Store("counter", 42)
m.Store("flag", true)
m.Store("name", "Alice")
// Попытка удалить с неправильным старым значением
deleted := m.CompareAndDelete("counter", 100)
fmt.Printf("Удаление counter=100: %v\n", deleted) // false
// Удаление с правильным значением
deleted = m.CompareAndDelete("counter", 42)
fmt.Printf("Удаление counter=42: %v\n", deleted) // true
// Проверяем, что counter удален
_, ok := m.Load("counter")
fmt.Printf("counter существует: %v\n", ok) // false
// Попытка удалить несуществующий ключ
deleted = m.CompareAndDelete("nonexistent", nil)
fmt.Printf("Удаление nonexistent: %v\n", deleted) // false
// Пример с nil значением
m.Store("nil-value", nil)
deleted = m.CompareAndDelete("nil-value", nil)
fmt.Printf("Удаление nil-value: %v\n", deleted) // true
}
Реальный кейс использования
Представим систему, где несколько горутин обновляют статус задач:
type TaskStatus string
const (
Pending TaskStatus = "pending"
Running TaskStatus = "running"
Completed TaskStatus = "completed"
)
func completeTask(m *sync.Map, taskID string) bool {
// Пытаемся перевести задачу из running в completed
return m.CompareAndDelete(taskID, Running)
}
func main() {
var tasks sync.Map
// Инициализируем задачи
tasks.Store("task1", Pending)
tasks.Store("task2", Running)
tasks.Store("task3", Running)
// Горутины пытаются завершить задачи
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if completeTask(&tasks, "task2") {
fmt.Println("Задача task2 завершена")
}
}()
}
wg.Wait()
// Проверяем оставшиеся задачи
tasks.Range(func(key, value interface{}) bool {
fmt.Printf("%s: %s\n", key, value)
return true
})
}
Особенности работы
- Безопасность для concurrent-использования: метод можно вызывать из нескольких горутин без дополнительной синхронизации
- Сравнение значений: сравнение происходит через
==, поэтому для сложных типов нужно быть внимательным
- nil значения: даже если
old - nil, метод вернет false если ключа нет в мапе
Когда использовать?
CompareAndDelete полезен в сценариях:
- Удаление устаревших данных (когда значение соответствует ожидаемому)
- Реализация конечных автоматов (state machines)
- Оптимистичные блокировки (optimistic locking)
- Удаление элементов только при определенных условиях
Этот метод особенно полезен в конкурентных сценариях, где нужно атомарно проверить и удалить значение без явных блокировок.
func (*Map) CompareAndSwap
func (m *Map) CompareAndSwap(key, old, new any) (swapped bool)
CompareAndSwap меняет местами старое и новое значения для ключа, если значение, хранящееся в карте, равно old. Старое значение должно быть сравнимого типа.
func (*Map) Delete
func (m *Map) Delete(key any)
Delete удаляет значение для ключа.
func (*Map) Load
func (m *Map) Load(key any) (value any, ok bool)
Load возвращает значение, хранящееся в карте для ключа, или nil, если значение отсутствует. Результат ok указывает, было ли найдено значение в карте.
func (*Map) LoadAndDelete
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
LoadAndDelete удаляет значение для ключа, возвращая предыдущее значение, если оно есть. Результат loaded сообщает, присутствовал ли ключ.
func (*Map) LoadOrStore
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
Метод LoadOrStore выполняет атомарную операцию “загрузить или сохранить” и особенно полезен в конкурентных сценариях, когда несколько горутин могут одновременно пытаться работать с одними и теми же ключами.
Объяснеие, что происходит на самом деле
Как это работает?
actual, loaded := m.LoadOrStore(key, value)
Простой пример
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Пытаемся сохранить значение "apple" по ключу "fruit"
// Так как ключа нет - оно будет сохранено
actual, loaded := m.LoadOrStore("fruit", "apple")
fmt.Printf("Ключ 'fruit': actual=%v, loaded=%v\n", actual, loaded)
// Вывод: Ключ 'fruit': actual=apple, loaded=false
// Пытаемся сохранить "banana" по тому же ключу
// Но ключ уже существует - возвращается текущее значение
actual, loaded = m.LoadOrStore("fruit", "banana")
fmt.Printf("Ключ 'fruit': actual=%v, loaded=%v\n", actual, loaded)
// Вывод: Ключ 'fruit': actual=apple, loaded=true
// Проверяем текущее значение
value, _ := m.Load("fruit")
fmt.Println("Текущее значение для 'fruit':", value)
// Вывод: Текущее значение для 'fruit': apple
}
Реальный пример: кэширование результатов
Представим сервис, который кэширует результаты дорогих вычислений:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var cache sync.Map
func expensiveCalculation(id int) int {
// Имитация долгого вычисления
time.Sleep(time.Second)
return rand.Intn(1000)
}
func getCachedResult(id int) int {
// Пытаемся получить результат из кэша
result, loaded := cache.LoadOrStore(id, expensiveCalculation(id))
if loaded {
fmt.Printf("Результат для %d взят из кэша\n", id)
} else {
fmt.Printf("Результат для %d вычислен и сохранен\n", id)
}
return result.(int)
}
func main() {
rand.Seed(time.Now().UnixNano())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
getCachedResult(id % 3) // Используем только 3 разных ID
}(i)
}
wg.Wait()
}
Возможный вывод:
Результат для 0 вычислен и сохранен
Результат для 1 вычислен и сохранен
Результат для 2 вычислен и сохранен
Результат для 0 взят из кэша
Результат для 1 взят из кэша
Особенности LoadOrStore
- Атомарность: Операция выполняется атомарно, что делает её безопасной для использования из нескольких горутин
- Эффективность: Избегает “гонки” при инициализации значений
- Удобство: Заменяет распространённый паттерн “проверить-затем-сохранить”
Когда использовать?
LoadOrStore идеально подходит для:
- Кэширования результатов
- Инициализации синглтонов
- Создания элементов по требованию
- Любых сценариев, где нужно “получить или создать” значение атомарно
Этот метод особенно полезен в высоконагруженных системах, где несколько горутин могут одновременно запрашивать одни и те же ресурсы.
func (*Map) Range
func (m *Map) Range(f func(key, value any) bool)
Range вызывает f последовательно для каждого ключа и значения, присутствующих в карте. Если f возвращает false, range останавливает итерацию.
Range не обязательно соответствует какому-либо последовательному снимку содержимого карты: ни один ключ не будет посещен более одного раза, но если значение для любого ключа хранится или удаляется одновременно (в том числе f), Range может отражать любое сопоставление для этого ключа из любой точки во время вызова Range. Range не блокирует другие методы на приемнике; даже f может вызывать любой метод на m.
Range может быть O(N) с количеством элементов в карте, даже если f возвращает false после постоянного числа вызовов.
func (*Map) Store
func (m *Map) Store(key, value any)
Store устанавливает значение для ключа.
func (*Map) Swap
func (m *Map) Swap(key, value any) (previous any, loaded bool)
Swap заменяет значение ключа и возвращает предыдущее значение, если оно есть. Результат loaded сообщает, присутствовал ли ключ.
type Mutex
type Mutex struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Mutex — это блокировка взаимного исключения. Нулевое значение для Mutex — это разблокированный мьютекс.
Mutex не должен копироваться после первого использования.
В терминологии модели памяти Go n-й вызов Mutex.Unlock «синхронизирует перед» m-й вызов Mutex.Lock для любого n < m. Успешный вызов Mutex.TryLock эквивалентен вызову Lock. Неудачный вызов TryLock не устанавливает никаких отношений «синхронизирует перед».
Объяснение Mutex
Mutex (сокращение от “mutual exclusion” - взаимное исключение) - это примитив синхронизации, который позволяет только одной горутине за раз получать доступ к общему ресурсу.
Представьте его как дверь в туалет:
- Когда кто-то внутри, дверь заперта (Lock)
- Другие ждут снаружи (блокируются)
- Когда человек выходит, он открывает дверь (Unlock)
- Тогда следующий может войти
Основные методы:
Lock() - захватывает мьютекс (блокирует, если он уже захвачен)
Unlock() - освобождает мьютекс
TryLock() - пытается захватить мьютекс без блокировки (возвращает успех/неудачу)
Простой пример:
package main
import (
"fmt"
"sync"
"time"
)
var counter int
var mu sync.Mutex // Создаем мьютекс
func increment() {
mu.Lock() // Захватываем мьютекс
defer mu.Unlock() // Гарантируем освобождение
temp := counter
time.Sleep(1 * time.Millisecond) // Имитируем работу
counter = temp + 1
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Итоговое значение счетчика:", counter) // Всегда 100
}
Реальный пример: Банковский перевод
type BankAccount struct {
balance int
mu sync.Mutex
}
func (acc *BankAccount) Deposit(amount int) {
acc.mu.Lock()
defer acc.mu.Unlock()
acc.balance += amount
}
func (acc *BankAccount) Withdraw(amount int) bool {
acc.mu.Lock()
defer acc.mu.Unlock()
if acc.balance < amount {
return false
}
acc.balance -= amount
return true
}
func (acc *BankAccount) Transfer(to *BankAccount, amount int) bool {
// Важно: блокируем оба счета в одном порядке, чтобы избежать deadlock
acc.mu.Lock()
defer acc.mu.Unlock()
to.mu.Lock()
defer to.mu.Unlock()
if acc.balance < amount {
return false
}
acc.balance -= amount
to.balance += amount
return true
}
Когда использовать Mutex?
- Когда несколько горутин работают с общими данными
- Когда нужно гарантировать целостность данных
- Для защиты операций, которые должны выполняться атомарно
Важные правила:
- Всегда освобождайте мьютекс (лучше через
defer)
- Не копируйте мьютекс после использования
- Избегайте блокировок на долгое время
- Соблюдайте порядок блокировки нескольких мьютексов
TryLock пример:
func tryUpdate(data *string) {
var mu sync.Mutex
if mu.TryLock() {
defer mu.Unlock()
*data = "updated"
} else {
fmt.Println("Не удалось получить блокировку")
}
}
func (*Mutex) Lock
Lock блокирует m. Если блокировка уже используется, вызывающая goroutine блокируется до тех пор, пока мьютекс не станет доступным.
func (*Mutex) TryLock
func (m *Mutex) TryLock() bool
TryLock пытается заблокировать m и сообщает, удалось ли это.
Обратите внимание, что хотя правильные способы использования TryLock существуют, они редки, и использование TryLock часто является признаком более глубокой проблемы в конкретном использовании мьютексов.
func (*Mutex) Unlock
Unlock разблокирует m. Если m не заблокирован при входе в Unlock, возникает ошибка выполнения.
Заблокированный мьютекс не связан с конкретной горутиной. Одна горутина может заблокировать мьютекс, а затем организовать его разблокировку другой горутиной.
type Once
type Once struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Once — это объект, который выполнит ровно одно действие.
Once нельзя копировать после первого использования.
В терминологии модели памяти Go возврат из f «синхронизируется перед» возвратом из любого вызова once.Do(f).
Пример
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Этот код демонстрирует использование sync.Once в Go - структуры, которая гарантирует, что определённая функция будет выполнена ровно один раз, даже если её вызов происходит из нескольких горутин.
Разберём код по частям:
- Инициализация:
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
- Создаётся объект
sync.Once
- Определяется функция
onceBody, которая будет выполнена один раз
- Канал для синхронизации:
- Создаётся буферизированный канал для ожидания завершения всех горутин
- Запуск горутин:
for i := 0; i < 10; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
- Запускается 10 горутин
- Каждая вызывает
once.Do(onceBody)
- После выполнения отправляет сигнал в канал
done
- Ожидание завершения:
for i := 0; i < 10; i++ {
<-done
}
- Основная горутина ждёт 10 сигналов (по одному от каждой горутины)
Что произойдёт при выполнении:
- Все 10 горутин попытаются выполнить
onceBody через once.Do()
sync.Once гарантирует, что:
- Функция
onceBody будет выполнена только один раз
- Остальные горутин будут ждать завершения этого выполнения
- Все последующие вызовы будут проигнорированы
- В результате в консоли мы увидим только одно сообщение “Only once”
Практическое применение:
- Инициализация глобальных ресурсов
- Ленивая инициализация
- Создание синглтонов
- Однократное выполнение настройки
Этот пример наглядно показывает мощь sync.Once для решения задач, требующих однократного выполнения в конкурентной среде.
func (*Once) Do
func (o *Once) Do(f func())
Do вызывает функцию f, если и только если Do вызывается впервые для этого экземпляра Once. Другими словами, при условии
если once.Do(f) вызывается несколько раз, только первый вызов вызовет f, даже если f имеет разное значение при каждом вызове. Для выполнения каждой функции требуется новый экземпляр Once.
Do предназначен для инициализации, которая должна выполняться ровно один раз. Поскольку f является нулевым, может потребоваться использовать функциональный литерал для захвата аргументов функции, которая будет вызвана Do:
config.once.Do(func() { config.init(filename) })
Поскольку ни один вызов Do не возвращается до тех пор, пока не вернется один вызов f, если f вызывает Do, это приведет к тупиковой ситуации.
Если f вызывает панику, Do считает, что она вернулась; будущие вызовы Do возвращаются без вызова f.
type Pool
type Pool struct {
// New опционально указывает функцию для генерации
// значения, когда Get в противном случае вернул бы nil.
// Он не может быть изменен одновременно с вызовами Get.
New func() any
// содержит отфильтрованные или неэкспортируемые поля
}
Pool — это набор временных объектов, которые могут быть индивидуально сохранены и извлечены.
Любой элемент, хранящийся в Pool, может быть удален автоматически в любое время без уведомления. Если Pool содержит единственную ссылку, когда это происходит, элемент может быть деаллоцирован.
Pool безопасен для одновременного использования несколькими goroutines.
Цель пула — кэшировать выделенные, но неиспользуемые элементы для повторного использования в будущем, снимая нагрузку с сборщика мусора. То есть он упрощает создание эффективных, потокобезопасных списков свободных элементов. Однако он подходит не для всех списков свободных элементов.
Пул целесообразно использовать для управления группой временных элементов, которые незаметно совместно используются и потенциально повторно используются одновременно независимыми клиентами пакета. Пул позволяет амортизировать накладные расходы на выделение памяти между многими клиентами.
Примером правильного использования пула является пакет fmt, который поддерживает хранилище временных буферов вывода динамического размера. Хранилище масштабируется под нагрузкой (когда многие goroutines активно печатают) и сокращается в состоянии покоя.
С другой стороны, свободный список, поддерживаемый как часть короткоживущего объекта, не подходит для использования в пуле, поскольку в этом сценарии накладные расходы не амортизируются должным образом. Более эффективно, чтобы такие объекты реализовывали свой собственный свободный список.
Пул не должен копироваться после первого использования.
В терминологии модели памяти Go вызов Put(x) «синхронизируется перед» вызовом Pool.Get, возвращающим то же значение x. Аналогично, вызов New, возвращающий x, «синхронизируется перед» вызовом Get, возвращающим то же значение x.
Пример
package main
import (
"bytes"
"io"
"os"
"sync"
"time"
)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func timeNow() time.Time {
return time.Unix(1136214245, 0)
}
func Log(w io.Writer, key, val string) {
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
b.WriteString(timeNow().UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(val)
w.Write(b.Bytes())
bufPool.Put(b)
}
func main() {
Log(os.Stdout, "path", "/search?q=flowers")
}
Этот пример демонстрирует эффективное использование sync.Pool для оптимизации работы с временными объектами (в данном случае - bytes.Buffer).
1. Пул буферов (sync.Pool)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
- Создается пул объектов
bytes.Buffer
- Функция
New создает новый буфер, когда пул пуст
- Важно: возвращаются указатели (
new(bytes.Buffer)), чтобы избежать лишних аллокаций
2. Функция логирования
func Log(w io.Writer, key, val string) {
// 1. Получаем буфер из пула (или создаем новый)
b := bufPool.Get().(*bytes.Buffer)
// 2. Сбрасываем состояние буфера перед использованием
b.Reset()
// 3. Формируем строку лога
b.WriteString(timeNow().UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(val)
// 4. Выводим лог
w.Write(b.Bytes())
// 5. Возвращаем буфер в пул для повторного использования
bufPool.Put(b)
}
3. Тестовая реализация времени
func timeNow() time.Time {
return time.Unix(1136214245, 0) // Фиксированное время для демонстрации
}
Как это работает:
-
При первом вызове Log:
- Пул пуст, поэтому вызывается
New и создается новый bytes.Buffer
- Буфер используется и возвращается в пул
-
При последующих вызовах:
- Буфер берется из пула (без аллокации)
- Сбрасывается (
Reset())
- Используется повторно
Преимущества подхода:
-
Снижение нагрузки на GC:
- Буферы переиспользуются, а не создаются заново
- Меньше работы для сборщика мусора
-
Экономия памяти:
- Не нужно постоянно выделять/освобождать память
- Размер пула автоматически регулируется
-
Потокобезопасность:
sync.Pool безопасен для использования из нескольких горутин
Важные нюансы:
-
Обязательно сбрасывайте состояние:
- Перед использованием вызовите
Reset(), чтобы очистить предыдущие данные
-
Не сохраняйте объекты из пула:
- После
Put() считайте объект более недоступным
-
Подходит для часто создаваемых объектов:
- Идеально для объектов, которые:
- Дорого создавать
- Используются кратковременно
- Имеют примерно одинаковый размер
Реальный вывод программы:
2006-01-02T15:04:05Z path=/search?q=flowers
Этот паттерн особенно полезен в:
- Логгерах
- HTTP middleware
- Любом коде, где часто создаются временные буферы
func (*Pool) Get
Get выбирает произвольный элемент из пула, удаляет его из пула и возвращает вызывающему. Get может игнорировать пул и рассматривать его как пустой. Вызывающие не должны предполагать какую-либо связь между значениями, переданными в Pool.Put, и значениями, возвращаемыми Get.
Если Get в противном случае вернул бы nil, а p.New не равен nil, Get возвращает результат вызова p.New.
func (*Pool) Put
func (p *Pool) Put(x any)
Put добавляет x в пул.
type RWMutex
type RWMutex struct {
// содержит отфильтрованные или неэкспортируемые поля
}
RWMutex — это блокировка взаимного исключения для чтения/записи. Блокировка может удерживаться произвольным количеством читателей или одним записывающим устройством. Нулевое значение для RWMutex — это разблокированный мьютекс.
RWMutex не должен копироваться после первого использования.
Если какой-либо goroutine вызывает RWMutex.Lock, когда блокировка уже удерживается одним или несколькими читателями, одновременные вызовы RWMutex.RLock будут блокироваться до тех пор, пока записывающий не получит (и не освободит) блокировку, чтобы обеспечить доступность блокировки для записывающего. Обратите внимание, что это запрещает рекурсивную блокировку чтения. RWMutex.RLock не может быть повышен до RWMutex.Lock, а RWMutex.Lock не может быть понижен до RWMutex.RLock.
В терминологии модели памяти Go n-й вызов RWMutex.Unlock «синхронизируется перед» m-м вызовом Lock для любого n < m, так же как и для Mutex. Для любого вызова RLock существует n, такое что n-й вызов Unlock «синхронизируется перед» этим вызовом RLock, а соответствующий вызов RWMutex.RUnlock «синхронизируется перед» n+1-м вызовом Lock.
func (*RWMutex) Lock
func (rw *RWMutex) Lock()
Lock блокирует rw для записи. Если блокировка уже заблокирована для чтения или записи, Lock блокируется до тех пор, пока блокировка не станет доступной.
func (*RWMutex) RLock
func (rw *RWMutex) RLock()
RLock блокирует rw для чтения.
Его не следует использовать для рекурсивной блокировки чтения; заблокированный вызов Lock исключает возможность получения блокировки новыми читателями. См. документацию по типу RWMutex.
func (*RWMutex) RLocker
func (rw *RWMutex) RLocker() Locker
RLocker возвращает интерфейс Locker, который реализует методы [Locker.Lock] и [Locker.Unlock] путем вызова rw.RLock и rw.RUnlock.
func (*RWMutex) RUnlock
func (rw *RWMutex) RUnlock()
RUnlock отменяет один вызов RWMutex.RLock; это не влияет на других одновременных читателей. Если rw не заблокирован для чтения при входе в RUnlock, возникает ошибка выполнения.
func (*RWMutex) TryLock
func (rw *RWMutex) TryLock() bool
TryLock пытается заблокировать rw для записи и сообщает, удалось ли это.
Обратите внимание, что хотя правильное использование TryLock существует, оно встречается редко, и использование TryLock часто является признаком более глубокой проблемы в конкретном использовании мьютексов.
func (*RWMutex) TryRLock
func (rw *RWMutex) TryRLock() bool
TryRLock пытается заблокировать rw для чтения и сообщает, удалось ли это.
Обратите внимание, что хотя правильное использование TryRLock существует, оно встречается редко, и использование TryRLock часто является признаком более глубокой проблемы в конкретном использовании мьютексов.
func (*RWMutex) Unlock
func (rw *RWMutex) Unlock()
Unlock разблокирует rw для записи. Если rw не заблокирован для записи при входе в Unlock, возникает ошибка выполнения.
Как и в случае с мьютексами, заблокированный RWMutex не связан с конкретной горутиной. Одна горутина может RWMutex.RLock (RWMutex.Lock) RWMutex, а затем организовать, чтобы другая горутина RWMutex.RUnlock (RWMutex.Unlock) его.
type WaitGroup
type WaitGroup struct {
// содержит отфильтрованные или неэкспортируемые поля
}
WaitGroup ожидает завершения работы набора goroutines. Основной goroutine вызывает WaitGroup.Add, чтобы установить количество goroutines, которые необходимо дождаться. Затем каждый из goroutines запускается и вызывает WaitGroup.Done по завершении работы. В то же время WaitGroup.Wait можно использовать для блокировки до тех пор, пока все goroutines не завершат работу.
WaitGroup нельзя копировать после первого использования.
В терминологии модели памяти Go вызов WaitGroup.Done «синхронизирует перед» возвратом любого вызова Wait, который он разблокирует.
Пример
package main
import (
"sync"
)
type httpPkg struct{}
func (httpPkg) Get(url string) {}
var http httpPkg
func main() {
var wg sync.WaitGroup
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.example.com/",
}
for _, url := range urls {
// Increment the WaitGroup counter.
wg.Add(1)
// Launch a goroutine to fetch the URL.
go func(url string) {
// Decrement the counter when the goroutine completes.
defer wg.Done()
// Fetch the URL.
http.Get(url)
}(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()
}
Этот пример демонстрирует классический паттерн использования sync.WaitGroup для ожидания завершения группы горутин.
1. Структура и имитация HTTP-клиента
type httpPkg struct{}
func (httpPkg) Get(url string) {} // Пустой метод для демонстрации
var http httpPkg // Глобальная переменная "HTTP-клиента"
- Создается пустая структура
httpPkg с методом Get (имитация HTTP-запроса)
- Создается глобальная переменная
http этого типа
2. Основная функция
func main() {
var wg sync.WaitGroup // Создаем WaitGroup
urls := []string{ // Список URL для обработки
"http://www.golang.org/",
"http://www.google.com/",
"http://www.example.com/",
}
- Инициализируется
WaitGroup для отслеживания горутин
- Определяется список URL для обработки
3. Запуск горутин
for _, url := range urls {
wg.Add(1) // Увеличиваем счетчик WaitGroup перед запуском горутины
go func(url string) { // Запускаем горутину для каждого URL
defer wg.Done() // Уменьшаем счетчик при завершении (defer гарантирует выполнение)
http.Get(url) // "Выполняем" HTTP-запрос
}(url) // Важно: передаем url как параметр, чтобы избежать проблем с замыканием
}
- Для каждого URL:
wg.Add(1) - увеличиваем счетчик ожидаемых горутин
- Запускаем анонимную функцию как горутину
defer wg.Done() - гарантируем уменьшение счетчика при любом выходе из функции
- Выполняем “запрос” с помощью
http.Get
4. Ожидание завершения
wg.Wait() // Блокируемся, пока счетчик не станет 0
}
- Основная горутина блокируется, пока все запущенные горутины не вызовут
Done()
Как это работает:
- Изначально счетчик WaitGroup = 0
- Для каждого URL:
Add(1) увеличивает счетчик (становится 1, 2, 3…)
- Горутина запускается
- Каждая горутина при завершении вызывает
Done() (уменьшает счетчик)
Wait() блокируется, пока счетчик не вернется к 0
Важные моменты:
-
Порядок операций:
- Всегда вызывайте
Add() ДО запуска горутины
- Используйте
defer wg.Done() для надежности
-
Передача параметров:
- URL передается как аргумент в горутину (
go func(url string))
- Это избегает проблем с общим доступом к переменной цикла
-
Реальные применения:
- Параллельные HTTP-запросы
- Обработка файлов/данных в нескольких горутинах
- Любые задачи, которые можно распараллелить
Что произойдет при выполнении:
- Запустятся 3 горутины (по одной на каждый URL)
- Основная горутина заблокируется на
wg.Wait()
- Когда все 3 горутины завершатся (вызовут
Done())
- Основная горутина продолжит выполнение (и завершит программу)
Этот паттерн - основа для многих конкурентных операций в Go, где нужно дождаться завершения группы горутин.
func (*WaitGroup) Add
func (wg *WaitGroup) Add(delta int)
Add добавляет delta, которая может быть отрицательной, к счетчику WaitGroup. Если счетчик становится равным нулю, все goroutines, заблокированные WaitGroup.Wait, освобождаются. Если счетчик становится отрицательным, Add вызывает панику.
Обратите внимание, что вызовы с положительной дельтой, которые происходят, когда счетчик равен нулю, должны происходить перед Wait. Вызовы с отрицательной дельтой или вызовы с положительной дельтой, которые начинаются, когда счетчик больше нуля, могут происходить в любое время. Обычно это означает, что вызовы Add должны выполняться до оператора, создающего goroutine или другое событие, которое необходимо ожидать. Если WaitGroup повторно используется для ожидания нескольких независимых наборов событий, новые вызовы Add должны происходить после того, как все предыдущие вызовы Wait вернулись. См. пример WaitGroup.
func (*WaitGroup) Done
func (wg *WaitGroup) Done()
Done уменьшает счетчик WaitGroup на единицу.
func (*WaitGroup) Wait
func (wg *WaitGroup) Wait()
Wait блокируется до тех пор, пока счетчик WaitGroup не станет равным нулю.
4.10.1 - Описание пакета sync/atomic
Пакет atomic предоставляет низкоуровневые примитивы атомарной памяти, полезные для реализации алгоритмов синхронизации.
Для правильного использования этих функций требуется большая осторожность. За исключением специальных низкоуровневых приложений, синхронизацию лучше выполнять с помощью каналов или средств пакета sync. Делитесь памятью, общаясь; не общайтесь, делясь памятью.
Объяснение atomic
Низкоуровневые примитивы атомарной памяти в Go
Атомарные операции - это операции, которые выполняются полностью (как единое целое) без возможности прерывания их другими потоками. В Go они предоставляются пакетом sync/atomic и используются для безопасной работы с разделяемой памятью в многопоточных программах.
Основные понятия
- Атомарность - операция либо выполняется полностью, либо не выполняется вообще
- Гонка данных (data race) - ситуация, когда несколько горутин одновременно обращаются к одной переменной, и хотя бы одна из них выполняет запись
- Примитивы - базовые строительные блоки для синхронизации
Основные типы и функции
Пакет sync/atomic работает с:
int32, int64, uint32, uint64, uintptr
- Указателями (через
unsafe.Pointer)
Примеры использования
1. Атомарное увеличение счетчика
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
atomic.AddInt64(&counter, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter) // Гарантированно выведет 100
}
2. Атомарное чтение и запись
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var value int32
// Горутина, которая изменяет значение
go func() {
time.Sleep(time.Millisecond * 100)
atomic.StoreInt32(&value, 42)
}()
// Главная горутина ждет изменения значения
for {
if atomic.LoadInt32(&value) == 42 {
fmt.Println("Value has been updated to 42")
break
}
time.Sleep(time.Millisecond * 10)
}
}
3. Сравнение и замена (CAS - Compare And Swap)
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var value int32 = 10
// Пытаемся заменить 10 на 20
swapped := atomic.CompareAndSwapInt32(&value, 10, 20)
fmt.Println("Swapped:", swapped, "Value:", value) // Swapped: true Value: 20
// Пытаемся заменить 10 на 30 (но текущее значение уже 20)
swapped = atomic.CompareAndSwapInt32(&value, 10, 30)
fmt.Println("Swapped:", swapped, "Value:", value) // Swapped: false Value: 20
}
4. Атомарный указатель
package main
import (
"fmt"
"sync/atomic"
"unsafe"
)
type Data struct {
value int
}
func main() {
var ptr unsafe.Pointer
// Создаем данные
data1 := &Data{value: 42}
data2 := &Data{value: 100}
// Атомарно сохраняем указатель
atomic.StorePointer(&ptr, unsafe.Pointer(data1))
// Атомарно читаем указатель
current := (*Data)(atomic.LoadPointer(&ptr))
fmt.Println("Current value:", current.value) // 42
// Атомарно меняем указатель
atomic.StorePointer(&ptr, unsafe.Pointer(data2))
current = (*Data)(atomic.LoadPointer(&ptr))
fmt.Println("New value:", current.value) // 100
}
Когда использовать atomic
- Когда нужны простые операции над разделяемыми переменными
- Когда производительность критична (atomic обычно быстрее мьютексов)
- Для реализации более сложных примитивов синхронизации
Когда НЕ использовать atomic
- Когда логика сложная - лучше использовать мьютексы или каналы
- Когда нужно работать со сложными структурами данных
- Когда важна читаемость кода (atomic код может быть сложнее для понимания)
Атомарные операции - это низкоуровневый механизм, который требует аккуратного использования, но может быть очень эффективным в правильных сценариях.
Операция обмена, реализуемая функциями SwapT, является атомарным эквивалентом:
old = *addr
*addr = new
return old
Операция сравнения и замены, реализуемая функциями CompareAndSwapT, является атомарным эквивалентом:
if *addr == old {
*addr = new
return true
}
return false
Операция сложения, реализуемая функциями AddT, является атомарным эквивалентом:
*addr += delta
return *addr
Операции load и store, реализуемые функциями LoadT и StoreT, являются атомарными эквивалентами “return *addr” и “*addr = val”.
В терминологии модели памяти Go, если эффект атомарной операции A наблюдается атомарной операцией B, то A “синхронизируется раньше” B. Кроме того, все атомарные операции, выполняемые в программе, ведут себя так, как будто выполняются в некотором последовательно согласованном порядке. Это определение обеспечивает ту же семантику, что и последовательные атомики в C++ и волатильные переменные в Java.
Функции
func AddInt32
func AddInt32(addr *int32, delta int32) (new int32)
AddInt32 атомарно добавляет delta к *addr и возвращает новое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Add.
func AddInt64
func AddInt64(addr *int64, delta int64) (new int64)
AddInt64 атомарно добавляет delta к *addr и возвращает новое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Add (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func AddUint32
func AddUint32(addr *uint32, delta uint32) (new uint32)
AddUint32 атомарно добавляет дельту к *addr и возвращает новое значение. Чтобы вычесть из x знаковое положительное константное значение c, выполните AddUint32(&x, ^uint32(c-1)). В частности, чтобы уменьшить x, выполните AddUint32(&x, ^uint32(0)). Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Add.
func AddUint64
func AddUint64(addr *uint64, delta uint64) (new uint64)
AddUint64 атомарно добавляет delta к *addr и возвращает новое значение. Чтобы вычесть из x знаковое положительное константное значение c, выполните AddUint64(&x, ^uint64(c-1)). В частности, чтобы уменьшить x, выполните AddUint64(&x, ^uint64(0)). Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Add (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func AddUintptr
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
AddUintptr атомарно добавляет delta к *addr и возвращает новое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Add.
func AndInt32
func AndInt32(addr *int32, mask int32) (old int32)
AndInt32 атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.And.
func AndInt64
func AndInt64(addr *int64, mask int64) (old int64)
AndInt64 атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.And вместо этого.
func AndUint32
func AndUint32(addr *uint32, mask uint32) (old uint32)
AndUint32 атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.And вместо него.
func AndUint64
func AndUint64(addr *uint64, mask uint64) (old uint64)
AndUint64 атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.And вместо этого.
func AndUintptr
func AndUintptr(addr *uintptr, mask uintptr) (old uintptr)
AndUintptr атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.And вместо этого.
func CompareAndSwapInt32
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
CompareAndSwapInt32 выполняет операцию сравнения и замены для значения int32. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.CompareAndSwap.
func CompareAndSwapInt64
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
CompareAndSwapInt64 выполняет операцию сравнения и замены для значения int64. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.CompareAndSwap (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func CompareAndSwapPointer
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
CompareAndSwapPointer выполняет операцию сравнения и обмена для значения unsafe.Pointer. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Pointer.CompareAndSwap.
func CompareAndSwapUint32
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
CompareAndSwapUint32 выполняет операцию сравнения и замены для значения uint32. Рассмотрите возможность использования более удобной и менее подверженной ошибкам функции Uint32.CompareAndSwap.
func CompareAndSwapUint64
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
CompareAndSwapUint64 выполняет операцию сравнения и замены для значения uint64. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.CompareAndSwap (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func CompareAndSwapUintptr
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
CompareAndSwapUintptr выполняет операцию сравнения и замены для значения uintptr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.CompareAndSwap.
func LoadInt32
func LoadInt32(addr *int32) (val int32)
LoadInt32 атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Load.
func LoadInt64
func LoadInt64(addr *int64) (val int64)
LoadInt64 атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Load (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func LoadPointer
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
LoadPointer атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Pointer.Load.
func LoadUint32
func LoadUint32(addr *uint32) (val uint32)
LoadUint32 атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Load.
func LoadUint64
func LoadUint64(addr *uint64) (val uint64)
LoadUint64 атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Load (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func LoadUintptr
func LoadUintptr(addr *uintptr) (val uintptr)
LoadUintptr атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Load.
func OrInt32
func OrInt32(addr *int32, mask int32) (old int32)
OrInt32 атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, указанную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Or.
func OrInt64
func OrInt64(addr *int64, mask int64) (old int64)
OrInt64 атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Or вместо него.
func OrUint32
func OrUint32(addr *uint32, mask uint32) (старый uint32)
OrUint32 атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Or.
func OrUint64
func OrUint64(addr *uint64, mask uint64) (old uint64)
OrUint64 атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Or вместо него.
func OrUintptr
func OrUintptr(addr *uintptr, mask uintptr) (old uintptr)
OrUintptr атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Or.
func StoreInt32
func StoreInt32(addr *int32, val int32)
StoreInt32 атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Store.
func StoreInt64
func StoreInt64(addr *int64, val int64)
StoreInt64 атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Store (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func StorePointer
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
StorePointer атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Pointer.Store.
func StoreUint32
func StoreUint32(addr *uint32, val uint32)
StoreUint32 атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Store.
func StoreUint64
func StoreUint64(addr *uint64, val uint64)
StoreUint64 атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Store (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func StoreUintptr
func StoreUintptr(addr *uintptr, val uintptr)
StoreUintptr атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Store.
func SwapInt32
func SwapInt32(addr *int32, new int32) (old int32)
SwapInt32 атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Swap.
func SwapInt64
func SwapInt64(addr *int64, new int64) (old int64)
SwapInt64 атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Swap (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func SwapPointer
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
SwapPointer атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Pointer.Swap.
func SwapUint32
func SwapUint32(addr *uint32, new uint32) (old uint32)
SwapUint32 атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Swap.
func SwapUint64
func SwapUint64(addr *uint64, new uint64) (old uint64)
SwapUint64 атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Swap (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).
func SwapUintptr
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
SwapUintptr атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Swap.
Типы
type Bool
type Bool struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Bool — это атомарное булево значение. Нулевое значение — false.
func (*Bool) CompareAndSwap
func (x *Bool) CompareAndSwap(old, new bool) (swapped bool)
CompareAndSwap выполняет операцию сравнения и замены для булевого значения x.
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var flag atomic.Bool
flag.Store(false)
// Пытаемся изменить false → true (успешно)
swapped := flag.CompareAndSwap(false, true)
fmt.Println("Swapped (false→true):", swapped, "Flag:", flag.Load())
// Пытаемся изменить false → true (неуспешно, текущее значение уже true)
swapped = flag.CompareAndSwap(false, true)
fmt.Println("Swapped (false→true):", swapped, "Flag:", flag.Load())
}
func (*Bool) Load
func (x *Bool) Load() bool
Load атомарно загружает и возвращает значение, хранящееся в x.
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
var ready atomic.Bool
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(500 * time.Millisecond)
ready.Store(true)
}()
for !ready.Load() {
fmt.Println("Waiting...")
time.Sleep(100 * time.Millisecond)
}
fmt.Println("Ready!")
wg.Wait()
}
func (*Bool) Store
func (x *Bool) Store(val bool)
Store атомарно сохраняет val в x.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var initialized atomic.Bool
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
if initialized.Load() {
fmt.Printf("Worker %d: already initialized\n", id)
return
}
initialized.Store(true)
fmt.Printf("Worker %d: did initialization\n", id)
}(i)
}
wg.Wait()
}
func (*Bool) Swap
func (x *Bool) Swap(new bool) (old bool)
Swap атомарно сохраняет new в x и возвращает предыдущее значение.
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var toggle atomic.Bool
toggle.Store(false)
// Переключаем значение и получаем старое
old := toggle.Swap(true)
fmt.Println("Old value:", old, "New value:", toggle.Load())
// Переключаем ещё раз
old = toggle.Swap(false)
fmt.Println("Old value:", old, "New value:", toggle.Load())
}
type Int32/Int64/Uint32/Uint64/Uintptr
type Int32 struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Int32 — это атомарное целое число int32. Нулевое значение — ноль. Int64 — это атомарное целое число int64. Нулевое значение — ноль. Uint32 - это атомарный uint32. Нулевое значение равно нулю. Uint64 - это атомарный uint64. Нулевое значение равно нулю. Uintptr - это атомарный uintptr. Нулевое значение равно нулю.
Пример
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var num atomic.Int32
// Store - установка начального значения
num.Store(10)
fmt.Printf("Store(10): %d\n", num.Load())
// Add - атомарное добавление
newVal := num.Add(5)
fmt.Printf("Add(5): new=%d, loaded=%d\n", newVal, num.Load())
// Swap - замена значения
old := num.Swap(20)
fmt.Printf("Swap(20): old=%d, new=%d\n", old, num.Load())
// CompareAndSwap - условная замена
swapped := num.CompareAndSwap(20, 30)
fmt.Printf("CompareAndSwap(20→30): %t, value=%d\n", swapped, num.Load())
// Пытаемся сделать замену с неверным old значением
swapped = num.CompareAndSwap(20, 40)
fmt.Printf("CompareAndSwap(20→40): %t, value=%d\n", swapped, num.Load())
// Побитовые операции
// Устанавливаем значение для битовых операций
num.Store(0b00001111) // 15 в десятичной
// And - побитовое И
old = num.And(0b00110011) // 0b00000011 = 3
fmt.Printf("And(0b00110011): old=0b%08b, new=0b%08b (%d)\n",
old, num.Load(), num.Load())
// Or - побитовое ИЛИ
old = num.Or(0b11001100) // 0b11001111 = 207
fmt.Printf("Or(0b11001100): old=0b%08b, new=0b%08b (%d)\n",
old, num.Load(), num.Load())
// Load - просто чтение
current := num.Load()
fmt.Printf("Load(): %d (0b%08b)\n", current, current)
}
Вывод программы:
Store(10): 10
Add(5): new=15, loaded=15
Swap(20): old=15, new=20
CompareAndSwap(20→30): true, value=30
CompareAndSwap(20→40): false, value=30
And(0b00110011): old=0b00001111, new=0b00000011 (3)
Or(0b11001100): old=0b00000011, new=0b11001111 (207)
Load(): 207 (0b11001111)
Объяснение Uintptr
atomic.Uintptr в Go
atomic.Uintptr — это тип, добавленный в Go 1.19, который предоставляет атомарные операции для беззнаковых целых чисел размером с указатель (uintptr).
Основное назначение
-
Атомарные операции с указателями:
uintptr часто используется для низкоуровневых операций с указателями
atomic.Uintptr позволяет безопасно работать с ними в конкурентных сценариях
-
Реализация lock-free структур данных:
- Полезен при создании собственных конкурентных структур данных
- Позволяет избежать блокировок при определенных операциях
-
Работа с небезопасными указателями:
- Когда нужно сохранить
uintptr значение атомарно (например, при работе через unsafe)
Ключевые особенности
- Обеспечивает атомарность операций без использования мьютексов
- Поддерживает все стандартные атомарные операции:
func (u *Uintptr) Add(delta uintptr) (new uintptr)
func (u *Uintptr) Load() uintptr
func (u *Uintptr) Store(val uintptr)
func (u *Uintptr) Swap(new uintptr) (old uintptr)
func (u *Uintptr) CompareAndSwap(old, new uintptr) (swapped bool)
Пример использования
package main
import (
"fmt"
"sync/atomic"
"unsafe"
)
type Data struct {
value int
}
func main() {
var addr atomic.Uintptr
data := &Data{value: 42}
// Сохраняем указатель как uintptr
addr.Store(uintptr(unsafe.Pointer(data)))
// Атомарно загружаем и преобразуем обратно
ptr := (*Data)(unsafe.Pointer(addr.Load()))
fmt.Println("Data value:", ptr.value) // 42
// Атомарное добавление к значению (если используется как число)
var counter atomic.Uintptr
counter.Store(100)
newVal := counter.Add(50)
fmt.Println("Counter:", newVal) // 150
}
Когда использовать
- При работе с
unsafe.Pointer и низкоуровневыми операциями
- Для атомарного управления памятью или ресурсами
- В высокопроизводительных сценариях, где важна lock-free синхронизация
Отличие от других атомарных типов
Uintptr работает именно с размером указателя (архитектурно-зависимым)
- Более безопасная альтернатива прямому использованию
atomic.AddUintptr и подобных функций
- Типобезопасная обертка (по сравнению с использованием
atomic функций напрямую)
Этот тип особенно полезен в системном программировании и при реализации высокопроизводительных параллельных алгоритмов.
func (*Int32) Add
func (x *Int32) Add(delta int32) (new int32)
Add атомарно добавляет delta к x и возвращает новое значение.
func (*Int32) And
func (x *Int32) And(mask int32) (old int32)
And атомарно выполняет побитовую операцию AND над x, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение.
func (*Int32) CompareAndSwap
func (x *Int32) CompareAndSwap(old, new int32) (swapped bool)
CompareAndSwap выполняет операцию сравнения и замены для x.
func (*Int32) Load
func (x *Int32) Load() int32
Load атомарно загружает и возвращает значение, хранящееся в x.
func (*Int32) Or
func (x *Int32) Or(mask int32) (old int32)
Or атомарно выполняет операцию побитового ИЛИ над x, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение.
func (*Int32) Store
func (x *Int32) Store(val int32)
Store атомарно сохраняет val в x.
func (*Int32) Swap
func (x *Int32) Swap(new int32) (old int32)
Swap атомарно сохраняет new в x и возвращает предыдущее значение.
type Pointer
type Pointer[T any] struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Pointer — это атомарный указатель типа *T. Нулевое значение — nil *T.
Пример
package main
import (
"fmt"
"sync/atomic"
)
type Data struct {
value int
}
func main() {
var ptr atomic.Pointer[Data]
// Исходные данные
data1 := &Data{value: 10}
data2 := &Data{value: 20}
data3 := &Data{value: 30}
// Store - атомарное сохранение указателя
ptr.Store(data1)
printState("Store(data1)", &ptr)
// Load - атомарное чтение указателя
current := ptr.Load()
fmt.Printf("Load(): %+v\n", current)
// Swap - атомарная замена указателя
old := ptr.Swap(data2)
printState("Swap(data2)", &ptr)
fmt.Printf("Old value: %+v\n", old)
// CompareAndSwap успешный (текущее значение = data2)
swapped := ptr.CompareAndSwap(data2, data3)
printState("CompareAndSwap(data2→data3)", &ptr)
fmt.Printf("Swapped: %v\n", swapped)
// CompareAndSwap неудачный (текущее значение уже data3)
swapped = ptr.CompareAndSwap(data2, data1)
printState("CompareAndSwap(data2→data1) [expected fail]", &ptr)
fmt.Printf("Swapped: %v\n", swapped)
// Демонстрация конкурентного доступа
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
ptr.Store(&Data{value: i})
}
}()
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
_ = ptr.Load()
}
}()
wg.Wait()
fmt.Println("Concurrent access completed")
}
func printState(op string, p *atomic.Pointer[Data]) {
fmt.Printf("%-30s: %+v\n", op, p.Load())
}
Вывод программы:
Store(data1) : &{value:10}
Load(): &{value:10}
Swap(data2) : &{value:20}
Old value: &{value:10}
CompareAndSwap(data2→data3) : &{value:30}
Swapped: true
CompareAndSwap(data2→data1) [expected fail]: &{value:30}
Swapped: false
Concurrent access completed
func (*Pointer[T]) CompareAndSwap
func (x *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool)
CompareAndSwap выполняет операцию сравнения и замены для x.
func (*Pointer[T]) Load
func (x *Pointer[T]) Load() *T
Load атомарно загружает и возвращает значение, хранящееся в x.
func (*Pointer[T]) Store
func (x *Pointer[T]) Store(val *T)
Store атомарно сохраняет val в x.
func (*Pointer[T]) Swap
func (x *Pointer[T]) Swap(new *T) (old *T)
Swap атомарно сохраняет new в x и возвращает предыдущее значение.
type Value
type Value struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Value обеспечивает атомарную загрузку и хранение значения с постоянным типом. Нулевое значение для Value возвращает nil из Value.Load. После вызова Value.Store Value не должен копироваться.
Value не должен копироваться после первого использования.
Объяснение Value
atomic.Value — это контейнер для безопасного хранения и обмена значениями между горутинами. Главная особенность — все операции с ним атомарны (не требуют мьютексов).
Основные правила:
- После первого использования нельзя копировать Value
- Все хранимые значения должны быть одного типа
- Нельзя хранить nil
Примеры использования:
1. Базовое использование (Store/Load)
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var config atomic.Value
// Сохраняем конфиг
config.Store(map[string]string{
"host": "localhost",
"port": "8080",
})
// Читаем конфиг в другой горутине
go func() {
c := config.Load().(map[string]string)
fmt.Println("Config in goroutine:", c["host"])
}()
// Читаем в main
fmt.Println("Port:", config.Load().(map[string]string)["port"])
}
2. Swap (атомарная замена)
var lastUser atomic.Value
func updateUser(user string) {
old := lastUser.Swap(user)
fmt.Printf("User changed from %v to %v\n", old, user)
}
func main() {
updateUser("Alice") // User changed from <nil> to Alice
updateUser("Bob") // User changed from Alice to Bob
}
3. CompareAndSwap (условная замена)
var currentID atomic.Value
func updateID(expected, new int) bool {
return currentID.CompareAndSwap(expected, new)
}
func main() {
currentID.Store(100)
success := updateID(100, 200)
fmt.Println("Update 100→200:", success) // true
success = updateID(100, 300)
fmt.Println("Update 100→300:", success) // false (текущее значение уже 200)
}
4. Хранение структур
type Config struct {
Timeout int
Mode string
}
func main() {
var cfg atomic.Value
cfg.Store(Config{Timeout: 30, Mode: "production"})
// Обновление конфига
newCfg := cfg.Load().(Config)
newCfg.Timeout = 60
cfg.Store(newCfg)
}
Когда использовать:
- Для “горячей” замены конфигурации без блокировок
- При частом чтении и редком обновлении данных
- Для реализации кэшей с атомарным обновлением
На заметку
- При Load всегда нужно делать type assertion (
.(type))
- Тип значения нельзя менять после первого Store
- Для сложных структур лучше хранить указатели (но не nil)
Пример config
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
// ServerConfig содержит конфигурацию сервера
type ServerConfig struct {
Host string
Port int
Timeout time.Duration
Debug bool
}
var (
config atomic.Value // Текущая конфигурация
counter atomic.Int32 // Счетчик запросов
)
func loadConfig() ServerConfig {
// Имитация загрузки конфигурации (в реальности может быть из файла, БД и т.д.)
rand.Seed(time.Now().UnixNano())
return ServerConfig{
Host: fmt.Sprintf("server-%d.example.com", rand.Intn(3)+1),
Port: 8000 + rand.Intn(100),
Timeout: time.Duration(rand.Intn(5)+1) * time.Second,
Debug: rand.Intn(2) == 0,
}
}
func processRequest(workerID int) {
for {
// Имитация обработки запроса
time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
// Атомарно получаем текущую конфигурацию
conf := config.Load().(ServerConfig)
reqID := counter.Add(1)
fmt.Printf("Worker %d processing request #%d with config: %+v\n",
workerID, reqID, conf)
}
}
func configUpdater() {
for {
time.Sleep(5 * time.Second)
newConfig := loadConfig()
config.Store(newConfig)
fmt.Printf("\nConfig updated: %+v\n\n", newConfig)
}
}
func main() {
// Инициализация начальной конфигурации
config.Store(loadConfig())
fmt.Println("Initial config:", config.Load())
// Запускаем обновление конфигурации в фоне
go configUpdater()
// Запускаем воркеров для обработки запросов
for i := 1; i <= 5; i++ {
go processRequest(i)
}
// Даем поработать 30 секунд
time.Sleep(30 * time.Second)
fmt.Println("Server shutting down...")
}
Вывод программы:
Initial config: {Host:server-3.example.com Port:8034 Timeout:4s Debug:false}
Worker 5 processing request #1 with config: {Host:server-3.example.com Port:8034 Timeout:4s Debug:false}
Worker 3 processing request #2 with config: {Host:server-3.example.com Port:8034 Timeout:4s Debug:false}
Config updated: {Host:server-1.example.com Port:8067 Timeout:2s Debug:true}
Worker 1 processing request #3 with config: {Host:server-1.example.com Port:8067 Timeout:2s Debug:true}
Worker 4 processing request #4 with config: {Host:server-1.example.com Port:8067 Timeout:2s Debug:true}
Пример ReadMostly
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// ConcurrentMap реализует потокобезопасную map с помощью atomic.Value
type ConcurrentMap struct {
data atomic.Value // хранит map[string]string
mu sync.Mutex // только для записи
}
// NewConcurrentMap создает новую потокобезопасную map
func NewConcurrentMap() *ConcurrentMap {
cm := &ConcurrentMap{}
cm.data.Store(make(map[string]string))
return cm
}
// Get безопасно получает значение по ключу
func (cm *ConcurrentMap) Get(key string) (string, bool) {
currentMap := cm.data.Load().(map[string]string)
val, ok := currentMap[key]
return val, ok
}
// Set безопасно устанавливает значение по ключу
func (cm *ConcurrentMap) Set(key, value string) {
cm.mu.Lock()
defer cm.mu.Unlock()
// Копируем текущую map
oldMap := cm.data.Load().(map[string]string)
newMap := make(map[string]string, len(oldMap)+1)
// Копируем все элементы
for k, v := range oldMap {
newMap[k] = v
}
// Добавляем/обновляем элемент
newMap[key] = value
// Атомарно заменяем map
cm.data.Store(newMap)
}
// Delete безопасно удаляет элемент по ключу
func (cm *ConcurrentMap) Delete(key string) {
cm.mu.Lock()
defer cm.mu.Unlock()
oldMap := cm.data.Load().(map[string]string)
if _, exists := oldMap[key]; !exists {
return
}
newMap := make(map[string]string, len(oldMap))
for k, v := range oldMap {
if k != key {
newMap[k] = v
}
}
cm.data.Store(newMap)
}
func main() {
config := NewConcurrentMap()
// Записываем начальные значения
config.Set("host", "localhost")
config.Set("port", "8080")
config.Set("timeout", "30s")
// Запускаем 5 читателей
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 3; j++ {
time.Sleep(time.Millisecond * 100)
if val, ok := config.Get("host"); ok {
fmt.Printf("Reader %d: host=%s\n", id, val)
}
}
}(i)
}
// Запускаем 2 писателя
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 2; j++ {
time.Sleep(time.Millisecond * 200)
newHost := fmt.Sprintf("server-%d-%d", id, j)
config.Set("host", newHost)
fmt.Printf("Writer %d updated host to %s\n", id, newHost)
}
}(i)
}
// Запускаем удаление через некоторое время
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(time.Millisecond * 300)
config.Delete("timeout")
fmt.Println("Deleted timeout key")
}()
wg.Wait()
// Финальное состояние
fmt.Println("\nFinal config:")
if host, ok := config.Get("host"); ok {
fmt.Println("host:", host)
}
if port, ok := config.Get("port"); ok {
fmt.Println("port:", port)
}
if _, ok := config.Get("timeout"); !ok {
fmt.Println("timeout: (deleted)")
}
}
Вывод программы:
Reader 4: host=localhost
Reader 0: host=localhost
Reader 2: host=localhost
Reader 1: host=localhost
Writer 0 updated host to server-0-0
Reader 3: host=server-0-0
Reader 0: host=server-0-0
Reader 4: host=server-0-0
Writer 1 updated host to server-1-0
Reader 2: host=server-1-0
Deleted timeout key
Reader 1: host=server-1-0
Writer 0 updated host to server-0-1
Writer 1 updated host to server-1-1
Reader 3: host=server-1-1
Final config:
host: server-1-1
port: 8080
timeout: (deleted)
func (*Value) CompareAndSwap
func (v *Value) CompareAndSwap(old, new any) (swapped bool)
CompareAndSwap выполняет операцию сравнения и замены для Value.
Все вызовы CompareAndSwap для данного значения должны использовать значения одного и того же конкретного типа. CompareAndSwap несовместимого типа вызывает панику, как и CompareAndSwap(old, nil).
func (*Value) Load
func (v *Value) Load() (val any)
Load возвращает значение, установленное последним Store. Он возвращает nil, если для этого значения не было вызова Store.
func (*Value) Store
func (v *Value) Store(val any)
Store устанавливает значение Value v равным val. Все вызовы Store для данного Value должны использовать значения одного и того же конкретного типа. Store несовместимого типа вызывает панику, как и Store(nil).
func (*Value) Swap
func (v *Value) Swap(new any) (old any)
Swap сохраняет new в Value и возвращает предыдущее значение. Он возвращает nil, если Value пуст.
Все вызовы Swap для данного Value должны использовать значения одного и того же конкретного типа. Swap несовместимого типа вызывает панику, как и Swap(nil).
4.11 - Пакет os языка программирования Go
Пакет os предоставляет платформонезависимый интерфейс к функциональности операционной системы.
Пакет os предоставляет платформонезависимый интерфейс к функциональности операционной системы. Дизайн Unix-подобный, хотя обработка ошибок Go-подобная; неудачные вызовы возвращают значения типа error, а не номера ошибок. Часто в ошибке содержится больше информации. Например, если вызов, принимающий имя файла, не работает, например Open или Stat, ошибка будет включать имя файла, который не работает, и будет иметь тип *PathError, который может быть распакован для получения дополнительной информации.
Интерфейс os должен быть единообразным для всех операционных систем. Функции, которые не являются общедоступными, представлены в специфическом для системы пакете syscall.
Приведем простой пример: открытие файла и чтение его части.
file, err := os.Open("file.go") // For read access.
if err != nil {
log.Fatal(err)
}
Если открытие завершится неудачей, строка ошибки будет иметь понятный вид, например
open file.go: no such file or directory
Затем данные файла могут быть считаны в виде фрагмента байтов. Команды Read и Write берут количество байтов из длины фрагмента аргумента.
data := make([]byte, 100)
count, err := file.Read(data)
if err != nil {
log.Fatal(err)
}
fmt.Printf("read %d bytes: %q\n", count, data[:count])
Concurrency (Параллелизм)
Методы File соответствуют операциям файловой системы. Все они безопасны для одновременного использования. Максимальное количество одновременных операций над файлом может быть ограничено ОС или системой. Это число должно быть большим, но его превышение может снизить производительность или вызвать другие проблемы.
Константы
const (
// Должно быть указано ровно одно из значений O_RDONLY, O_WRONLY или O_RDWR.
O_RDONLY int = syscall.O_RDONLY // открыть файл только для чтения.
O_WRONLY int = syscall.O_WRONLY // открыть файл только для записи.
O_RDWR int = syscall.O_RDWR // открыть файл для чтения и записи.
// Остальные значения могут быть объединены с помощью оператора OR для управления поведением.
O_APPEND int = syscall.O_APPEND // добавлять данные в файл при записи.
O_CREATE int = syscall.O_CREAT // создать новый файл, если его нет.
O_EXCL int = syscall.O_EXCL // используется с O_CREATE, файл не должен существовать.
O_SYNC int = syscall.O_SYNC // открыть для синхронного ввода-вывода.
O_TRUNC int = syscall.O_TRUNC // усечь обычный файл с правом записи при открытии.
)
Флаги для OpenFile, оборачивающие флаги базовой системы. Не все флаги могут быть реализованы в данной системе.
Устарело
const (
SEEK_SET int = 0 // поиск относительно начала файла
SEEK_CUR int = 1 // поиск относительно текущего смещения
SEEK_END int = 2 // поиск относительно конца
)
Значения поиска.
Устарело: используйте io.SeekStart, io.SeekCurrent и io.SeekEnd.
Просмотреть исходный код
const (
PathSeparator = „/“ // разделитель путей, специфичный для ОС
PathListSeparator = „:“ // разделитель списка путей, специфичный для ОС)
const (
// Одиночные буквы являются аббревиатурами,
// используемыми для форматирования методом String.
ModeDir = fs.ModeDir // d: каталог
ModeAppend = fs.ModeAppend // a: только добавление
ModeExclusive = fs.ModeExclusive // l: исключительное использование
ModeTemporary = fs.ModeTemporary // T: временный файл; только Plan 9
ModeSymlink = fs.ModeSymlink // L: символическая ссылка
ModeDevice = fs.ModeDevice // D: файл устройства
ModeNamedPipe = fs.ModeNamedPipe // p: именованный канал (FIFO)
ModeSocket = fs.ModeSocket // S: сокет домена Unix
ModeSetuid = fs.ModeSetuid // u: setuid
ModeSetgid = fs.ModeSetgid // g: setgid
ModeCharDevice = fs.ModeCharDevice // c: символьное устройство Unix, когда установлен ModeDevice
ModeSticky = fs.ModeSticky // t: sticky
ModeIrregular = fs.ModeIrregular // ?: нерегулярный файл; ничего больше не известно об этом файле
// Маска для битов типа. Для обычных файлов не будет установлено ничего.
ModeType = fs.ModeType
ModePerm = fs.ModePerm // Биты разрешений Unix, 0o777
)
Определенные биты режима файла являются наиболее значимыми битами FileMode. Девять наименее значимых битов являются стандартными разрешениями Unix rwxrwxrwx. Значения этих битов следует рассматривать как часть общедоступного API и могут использоваться в протоколах передачи данных или представлениях диска: их нельзя изменять, хотя могут быть добавлены новые биты.
const DevNull = "/dev/null"
DevNull — это имя «нулевого устройства» операционной системы. В Unix-подобных системах оно имеет вид «/dev/null», в Windows — «NUL».
Переменные
var (
// ErrInvalid указывает на недействительный аргумент.
// Методы File возвращают эту ошибку, когда получатель равен nil.
ErrInvalid = fs.ErrInvalid // «недействительный аргумент»
ErrPermission = fs.ErrPermission // «разрешение отказано»
ErrExist = fs.ErrExist // «файл уже существует»
ErrNotExist = fs.ErrNotExist // «файл не существует»
ErrClosed = fs.ErrClosed // «файл уже закрыт»
ErrNoDeadline = errNoDeadline() // «тип файла не поддерживает срок»
ErrDeadlineExceeded = errDeadlineExceeded() // «тайм-аут ввода-вывода»
)
Портативные аналоги некоторых распространенных ошибок системных вызовов.
Ошибки, возвращаемые из этого пакета, можно проверить на наличие этих ошибок с помощью errors.Is.
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr"))
Stdin, Stdout и Stderr — открытые файлы, указывающие на дескрипторы стандартного ввода, стандартного вывода и стандартных ошибок.
Обратите внимание, что среда выполнения Go записывает в стандартный файл ошибок сообщения о паниках и сбоях; закрытие Stderr может привести к тому, что эти сообщения будут отправляться в другое место, например в файл, открытый позже.
Args содержит аргументы командной строки, начиная с имени программы.
var ErrProcessDone = errors.New("os: process already finished")
ErrProcessDone указывает, что процесс завершен.
4.11.1 - Описание функций пакета os
Основные функции пакета os языка программирования GO
func Chdir
func Chdir(dir string) error
Chdir изменяет текущий рабочий каталог на указанный каталог. Если произошла ошибка, она будет типа *PathError.
func Chmod
func Chmod(name string, mode FileMode) error
Chmod изменяет режим указанного файла на mode. Если файл является символической ссылкой, то изменяется режим цели ссылки. Если произошла ошибка, то она будет типа *PathError.
В зависимости от операционной системы используется разный поднабор битов режима.
В Unix используются биты разрешений режима ModeSetuid, ModeSetgid и ModeSticky.
В Windows используется только бит 0o200 (доступ на запись для владельца) режима; он контролирует, установлен ли или снят атрибут «только для чтения» файла. Остальные биты в настоящее время не используются. Для совместимости с Go 1.12 и более ранними версиями используйте режим, отличный от нуля. Используйте режим 0o400 для файла, доступного только для чтения, и 0o600 для файла, доступного для чтения и записи.
В Plan 9 используются биты разрешений режима ModeAppend, ModeExclusive и ModeTemporary.
Пример
package main
import (
"log"
"os"
)
func main() {
if err := os.Chmod("some-filename", 0644); err != nil {
log.Fatal(err)
}
}
func Chown
func Chown(name string, uid, gid int) error
Chown изменяет числовые значения uid и gid указанного файла. Если файл является символической ссылкой, он изменяет uid и gid цели ссылки. uid или gid равные -1 означают, что это значение не изменяется. Если происходит ошибка, она будет типа *PathError.
В Windows или Plan 9 Chown всегда возвращает ошибку syscall.EWINDOWS или EPLAN9, обернутую в *PathError.
func Chtimes
func Chtimes(name string, atime time.Time, mtime time.Time) error
Chtimes изменяет время доступа и изменения указанного файла, аналогично функциям Unix utime() или utimes(). Нулевое значение time.Time оставит время соответствующего файла без изменений.
Базовая файловая система может усечь или округлить значения до менее точной единицы времени. Если произошла ошибка, она будет типа *PathError.
Пример
package main
import (
"log"
"os"
"time"
)
func main() {
mtime := time.Date(2006, time.February, 1, 3, 4, 5, 0, time.UTC)
atime := time.Date(2007, time.March, 2, 4, 5, 6, 0, time.UTC)
if err := os.Chtimes("some-filename", atime, mtime); err != nil {
log.Fatal(err)
}
}
func Clearenv
Clearenv удаляет все переменные окружения.
func CopyFS
func CopyFS(dir string, fsys fs.FS) error
CopyFS копирует файловую систему fsys в каталог dir, создавая dir при необходимости.
Файлы создаются с режимом 0o666 плюс любые права на выполнение из источника, а каталоги создаются с режимом 0o777 (до umask).
CopyFS не перезаписывает существующие файлы. Если имя файла в fsys уже существует в месте назначения, CopyFS вернет ошибку, так что errors.Is(err, fs.ErrExist) будет true.
Символьные ссылки в fsys не поддерживаются. При копировании из символьной ссылки возвращается *PathError с Err, установленным в ErrInvalid.
Символьные ссылки в dir прослеживаются.
Новые файлы, добавленные в fsys (в том числе, если dir является подкаталогом fsys) во время работы CopyFS, не гарантированно будут скопированы.
Копирование останавливается при возникновении первой ошибки и возвращает ее.
func DirFS
func DirFS(dir string) fs.FS
DirFS возвращает файловую систему (fs.FS) для дерева файлов, корнем которого является каталог dir.
Обратите внимание, что DirFS(«/prefix») гарантирует только то, что вызовы Open, которые он делает в операционной системе, будут начинаться с «/prefix»: DirFS(«/prefix»).Open(„file“) равно os.Open(«/prefix/file»). Поэтому, если /prefix/file является символической ссылкой, указывающей за пределы дерева /prefix, то использование DirFS не останавливает доступ больше, чем использование os.Open. Кроме того, корень fs.FS, возвращаемый для относительного пути, DirFS(«prefix»), будет затронут последующими вызовами Chdir. Поэтому DirFS не является общей заменой механизма безопасности типа chroot, когда дерево каталогов содержит произвольное содержимое.
Используйте Root.FS, чтобы получить fs.FS, который предотвращает выход из дерева через символьные ссылки.
Каталог dir не должен быть «».
Результат реализует io/fs.StatFS, io/fs.ReadFileFS и io/fs.ReadDirFS.
func Environ
Environ возвращает копию строк, представляющих среду, в форме «ключ=значение».
func Executable
func Executable() (string, error)
Executable возвращает имя пути к исполняемому файлу, который запустил текущий процесс. Нет гарантии, что путь по-прежнему указывает на правильный исполняемый файл. Если для запуска процесса использовалась символьная ссылка, в зависимости от операционной системы результатом может быть символьная ссылка или путь, на который она указывала. Если требуется стабильный результат, может помочь path/filepath.EvalSymlinks.
Executable возвращает абсолютный путь, если не произошла ошибка.
Основной случай использования — поиск ресурсов, расположенных относительно исполняемого файла.
func Exit
Exit вызывает завершение текущей программы с заданным кодом состояния. По соглашению, код нуль означает успех, ненулевой — ошибку. Программа завершается немедленно; отложенные функции не выполняются.
Для обеспечения переносимости код статуса должен находиться в диапазоне [0, 125].
func Expand
func Expand(s string, mapping func(string) string) string
Expand заменяет ${var} или $var в строке на основе функции сопоставления. Например, os.ExpandEnv(s) эквивалентно os.Expand(s, os.Getenv).
Пример
package main
import (
"fmt"
"os"
)
func main() {
mapper := func(placeholderName string) string {
switch placeholderName {
case "DAY_PART":
return "morning"
case "NAME":
return "Gopher"
}
return ""
}
fmt.Println(os.Expand("Good ${DAY_PART}, $NAME!", mapper))
}
Output:
Good morning, Gopher!
func ExpandEnv
func ExpandEnv(s string) string
ExpandEnv заменяет ${var} или $var в строке в соответствии со значениями текущих переменных окружения. Ссылки на неопределенные переменные заменяются пустой строкой.
Пример
package main
import (
"fmt"
"os"
)
func main() {
os.Setenv("NAME", "gopher")
os.Setenv("BURROW", "/usr/gopher")
fmt.Println(os.ExpandEnv("$NAME lives in ${BURROW}."))
}
Output:
gopher lives in /usr/gopher.
func Getegid
Getegid возвращает числовой идентификатор эффективной группы вызывающего.
В Windows возвращает -1.
func Getenv
func Getenv(key string) string
Getenv извлекает значение переменной окружения, указанной ключом. Возвращает значение, которое будет пустым, если переменная отсутствует. Чтобы отличить пустое значение от не заданного значения, используйте LookupEnv.
Пример
package main
import (
"fmt"
"os"
)
func main() {
os.Setenv("NAME", "gopher")
os.Setenv("BURROW", "/usr/gopher")
fmt.Printf("%s lives in %s.\n", os.Getenv("NAME"), os.Getenv("BURROW"))
}
Output:
gopher lives in /usr/gopher.
func Geteuid
Geteuid возвращает числовой эффективный идентификатор пользователя вызывающего.
В Windows возвращает -1.
func Getgid
Getgid возвращает числовой идентификатор группы вызывающего.
В Windows возвращает -1.
func Getgroups
func Getgroups() ([]int, error)
Getgroups возвращает список числовых идентификаторов групп, к которым принадлежит вызывающий.
В Windows возвращает syscall.EWINDOWS. Возможную альтернативу см. в пакете os/user.
func Getpagesize
Getpagesize возвращает размер страницы памяти базовой системы.
func Getpid
Getpid возвращает идентификатор процесса вызывающего.
func Getppid
Getppid возвращает идентификатор процесса родителя вызывающего.
func Getuid
Getuid возвращает числовой идентификатор пользователя вызывающего.
В Windows возвращает -1.
func Getwd
func Getwd() (dir string, err error)
Getwd возвращает абсолютное имя пути, соответствующее текущему каталогу. Если к текущему каталогу можно перейти по нескольким путям (из-за символьных ссылок), Getwd может вернуть любой из них.
На платформах Unix, если переменная окружения PWD предоставляет абсолютное имя, и оно является именем текущего каталога, оно возвращается.
func Hostname
func Hostname() (name string, err error)
Hostname возвращает имя хоста, сообщаемое ядром.
func IsExist
func IsExist(err error) bool
IsExist возвращает булево значение, указывающее, известно ли, что файл или каталог уже существует. Оно удовлетворяется ErrExist, а также некоторыми ошибками системных вызовов.
Эта функция предшествует errors.Is. Она поддерживает только ошибки, возвращаемые пакетом os. В новом коде следует использовать errors.Is(err, fs.ErrExist).
func IsNotExist
func IsNotExist(err error) bool
IsNotExist возвращает булево значение, указывающее, известно ли, что аргумент сообщает о том, что файл или каталог не существует. Это условие выполняется для ErrNotExist, а также для некоторых ошибок системных вызовов.
Эта функция предшествует errors.Is. Она поддерживает только ошибки, возвращаемые пакетом os. В новом коде следует использовать errors.Is(err, fs.ErrNotExist).
func IsPathSeparator
func IsPathSeparator(c uint8) bool
IsPathSeparator сообщает, является ли c символом разделителя каталогов.
func IsPermission
func IsPermission(err error) bool
IsPermission возвращает булево значение, указывающее, известно ли, что аргумент сообщает об отказе в разрешении. Это условие выполняется ErrPermission, а также некоторыми ошибками системных вызовов.
Эта функция предшествует errors.Is. Она поддерживает только ошибки, возвращаемые пакетом os. В новом коде следует использовать errors.Is(err, fs.ErrPermission).
func IsTimeout
func IsTimeout(err error) bool
IsTimeout возвращает булево значение, указывающее, известно ли, что аргумент сообщает о возникновении таймаута.
Эта функция предшествует errors.Is, и понятие о том, указывает ли ошибка на тайм-аут, может быть неоднозначным. Например, ошибка Unix EWOULDBLOCK иногда указывает на тайм-аут, а иногда нет. В новом коде следует использовать errors.Is со значением, соответствующим вызову, возвращающему ошибку, например os.ErrDeadlineExceeded.
func Lchown
func Lchown(name string, uid, gid int) error
Lchown изменяет числовые значения uid и gid указанного файла. Если файл является символической ссылкой, то изменяются значения uid и gid самой ссылки. Если происходит ошибка, то она будет типа *PathError.
В Windows всегда возвращается ошибка syscall.EWINDOWS, обернутая в *PathError.
func Link
func Link(oldname, newname string) error
Link создает newname как жесткую ссылку на файл oldname. Если происходит ошибка, она будет типа *LinkError.
func LookupEnv
func LookupEnv(key string) (string, bool)
LookupEnv извлекает значение переменной окружения, указанной ключом. Если переменная присутствует в окружении, возвращается значение (которое может быть пустым) и булево значение true. В противном случае возвращаемое значение будет пустым, а булево значение будет false.
Пример
package main
import (
"fmt"
"os"
)
func main() {
show := func(key string) {
val, ok := os.LookupEnv(key)
if !ok {
fmt.Printf("%s not set\n", key)
} else {
fmt.Printf("%s=%s\n", key, val)
}
}
os.Setenv("SOME_KEY", "value")
os.Setenv("EMPTY_KEY", "")
show("SOME_KEY")
show("EMPTY_KEY")
show("MISSING_KEY")
}
Output:
SOME_KEY=value
EMPTY_KEY=
MISSING_KEY not set
func Mkdir
func Mkdir(name string, perm FileMode) error
Mkdir создает новый каталог с указанным именем и битами разрешений (до umask). Если произошла ошибка, она будет типа *PathError.
Пример
package main
import (
"log"
"os"
)
func main() {
err := os.Mkdir("testdir", 0750)
if err != nil && !os.IsExist(err) {
log.Fatal(err)
}
err = os.WriteFile("testdir/testfile.txt", []byte("Hello, Gophers!"), 0660)
if err != nil {
log.Fatal(err)
}
}
func MkdirAll
func MkdirAll(path string, perm FileMode) error
MkdirAll создает каталог с именем path, а также все необходимые родительские каталоги, и возвращает nil, или же возвращает ошибку. Биты прав доступа perm (до umask) используются для всех каталогов, которые создает MkdirAll. Если path уже является каталогом, MkdirAll ничего не делает и возвращает nil.
Пример
package main
import (
"log"
"os"
)
func main() {
err := os.MkdirAll("test/subdir", 0750)
if err != nil {
log.Fatal(err)
}
err = os.WriteFile("test/subdir/testfile.txt", []byte("Hello, Gophers!"), 0660)
if err != nil {
log.Fatal(err)
}
}
func MkdirTemp
func MkdirTemp(dir, pattern string) (string, error)
MkdirTemp создает новый временный каталог в каталоге dir и возвращает путь к новому каталогу. Имя нового каталога генерируется путем добавления случайной строки в конец pattern. Если pattern содержит “*”, случайная строка заменяет последний “*”. Каталог создается с режимом 0o700 (до umask). Если dir — пустая строка, MkdirTemp использует каталог по умолчанию для временных файлов, возвращаемый TempDir. Несколько программ или goroutines, вызывающих MkdirTemp одновременно, не будут выбирать один и тот же каталог. Ответственность за удаление каталога, когда он больше не нужен, лежит на вызывающем.
Пример
package main
import (
"log"
"os"
"path/filepath"
)
func main() {
dir, err := os.MkdirTemp("", "example")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(dir) // clean up
file := filepath.Join(dir, "tmpfile")
if err := os.WriteFile(file, []byte("content"), 0666); err != nil {
log.Fatal(err)
}
}
Пример suffix
package main
import (
"log"
"os"
"path/filepath"
)
func main() {
logsDir, err := os.MkdirTemp("", "*-logs")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(logsDir) // clean up
// Logs can be cleaned out earlier if needed by searching
// for all directories whose suffix ends in *-logs.
globPattern := filepath.Join(os.TempDir(), "*-logs")
matches, err := filepath.Glob(globPattern)
if err != nil {
log.Fatalf("Failed to match %q: %v", globPattern, err)
}
for _, match := range matches {
if err := os.RemoveAll(match); err != nil {
log.Printf("Failed to remove %q: %v", match, err)
}
}
}
func NewSyscallError
func NewSyscallError(syscall string, err error) error
NewSyscallError возвращает в качестве ошибки новый SyscallError с указанным именем системного вызова и подробностями ошибки. Для удобства, если err равен nil, NewSyscallError возвращает nil.
func Pipe
func Pipe() (r *File, w *File, err error)
Pipe возвращает соединенную пару файлов; считывает из r байты, записанные в w. Он возвращает файлы и ошибку, если таковая имеется.
func ReadFile
func ReadFile(name string) ([]byte, error)
ReadFile считывает файл с указанным именем и возвращает его содержимое. Успешный вызов возвращает err == nil, а не err == EOF. Поскольку ReadFile считывает весь файл, он не рассматривает EOF из Read как ошибку, о которой следует сообщать.
Пример
package main
import (
"log"
"os"
)
func main() {
data, err := os.ReadFile("testdata/hello")
if err != nil {
log.Fatal(err)
}
os.Stdout.Write(data)
}
func Readlink
func Readlink(name string) (string, error)
Readlink возвращает место назначения символической ссылки с указанным именем. Если произошла ошибка, она будет типа *PathError.
Если место назначения ссылки является относительным, Readlink возвращает относительный путь, не преобразуя его в абсолютный.
Пример
package main
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
)
func main() {
// First, we create a relative symlink to a file.
d, err := os.MkdirTemp("", "")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(d)
targetPath := filepath.Join(d, "hello.txt")
if err := os.WriteFile(targetPath, []byte("Hello, Gophers!"), 0644); err != nil {
log.Fatal(err)
}
linkPath := filepath.Join(d, "hello.link")
if err := os.Symlink("hello.txt", filepath.Join(d, "hello.link")); err != nil {
if errors.Is(err, errors.ErrUnsupported) {
// Allow the example to run on platforms that do not support symbolic links.
fmt.Printf("%s links to %s\n", filepath.Base(linkPath), "hello.txt")
return
}
log.Fatal(err)
}
// Readlink returns the relative path as passed to os.Symlink.
dst, err := os.Readlink(linkPath)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s links to %s\n", filepath.Base(linkPath), dst)
var dstAbs string
if filepath.IsAbs(dst) {
dstAbs = dst
} else {
// Symlink targets are relative to the directory containing the link.
dstAbs = filepath.Join(filepath.Dir(linkPath), dst)
}
// Check that the target is correct by comparing it with os.Stat
// on the original target path.
dstInfo, err := os.Stat(dstAbs)
if err != nil {
log.Fatal(err)
}
targetInfo, err := os.Stat(targetPath)
if err != nil {
log.Fatal(err)
}
if !os.SameFile(dstInfo, targetInfo) {
log.Fatalf("link destination (%s) is not the same file as %s", dstAbs, targetPath)
}
}
Output:
hello.link links to hello.txt
func Remove
func Remove(name string) error
Remove удаляет указанный файл или (пустой) каталог. Если происходит ошибка, она будет типа *PathError.
func RemoveAll
func RemoveAll(path string) error
RemoveAll удаляет путь и все вложенные в него элементы. Он удаляет все, что может, но возвращает первую встреченную ошибку. Если путь не существует, RemoveAll возвращает nil (без ошибки). Если произошла ошибка, она будет типа *PathError.
func Rename
func Rename(oldpath, newpath string) error
Rename переименовывает (перемещает) oldpath в newpath. Если newpath уже существует и не является каталогом, Rename заменяет его. Если newpath уже существует и является каталогом, Rename возвращает ошибку. Ограничения, специфичные для ОС, могут применяться, когда oldpath и newpath находятся в разных каталогах. Даже в пределах одного каталога на платформах, отличных от Unix, Rename не является атомарной операцией. Если произошла ошибка, она будет типа *LinkError.
func SameFile
func SameFile(fi1, fi2 FileInfo) bool
SameFile сообщает, описывают ли fi1 и fi2 один и тот же файл. Например, в Unix это означает, что поля device и inode двух базовых структур идентичны; в других системах решение может основываться на именах путей. SameFile применяется только к результатам, возвращаемым Stat этого пакета. В других случаях она возвращает false.
func Setenv
func Setenv(key, value string) error
Setenv устанавливает значение переменной среды, указанной ключом. Возвращает ошибку, если таковая имеется.
func Symlink
func Symlink(oldname, newname string) error
Symlink создает newname как символическую ссылку на oldname. В Windows символическая ссылка на несуществующий oldname создает файловую символическую ссылку; если oldname позже будет создан как каталог, символическая ссылка не будет работать. Если произойдет ошибка, она будет типа *LinkError.
func TempDir
TempDir возвращает каталог по умолчанию, который будет использоваться для временных файлов.
В системах Unix она возвращает $TMPDIR, если он не пуст, в противном случае — /tmp. В Windows она использует GetTempPath, возвращая первое непустое значение из %TMP%, %TEMP%, %USERPROFILE% или каталога Windows. В Plan 9 она возвращает /tmp.
Существование каталога и наличие прав доступа к нему не гарантируются.
func Truncate
func Truncate(name string, size int64) error
Truncate изменяет размер указанного файла. Если файл является символической ссылкой, изменяется размер цели ссылки. Если происходит ошибка, она будет типа *PathError.
func Unsetenv
func Unsetenv(key string) error
Unsetenv снимает установку одной переменной среды.
Пример
package main
import (
"os"
)
func main() {
os.Setenv("TMPDIR", "/my/tmp")
defer os.Unsetenv("TMPDIR")
}
func UserCacheDir
func UserCacheDir() (string, error)
UserCacheDir возвращает корневой каталог по умолчанию, который будет использоваться для кэшированных данных конкретного пользователя. Пользователи должны создать свой собственный подкаталог для конкретного приложения в этом каталоге и использовать его.
В системах Unix она возвращает $XDG_CACHE_HOME, как указано в https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html, если он не пустой, в противном случае — $HOME/.cache. В Darwin она возвращает $HOME/Library/Caches. В Windows она возвращает %LocalAppData%. В Plan 9 она возвращает $home/lib/cache.
Если местоположение не может быть определено (например, $HOME не определено) или путь в $XDG_CACHE_HOME является относительным, то будет возвращена ошибка.
Пример
import (
"log"
"os"
"path/filepath"
"sync"
)
func main() {
dir, dirErr := os.UserCacheDir()
if dirErr == nil {
dir = filepath.Join(dir, "ExampleUserCacheDir")
}
getCache := func(name string) ([]byte, error) {
if dirErr != nil {
return nil, &os.PathError{Op: "getCache", Path: name, Err: os.ErrNotExist}
}
return os.ReadFile(filepath.Join(dir, name))
}
var mkdirOnce sync.Once
putCache := func(name string, b []byte) error {
if dirErr != nil {
return &os.PathError{Op: "putCache", Path: name, Err: dirErr}
}
mkdirOnce.Do(func() {
if err := os.MkdirAll(dir, 0700); err != nil {
log.Printf("can't create user cache dir: %v", err)
}
})
return os.WriteFile(filepath.Join(dir, name), b, 0600)
}
// Read and store cached data.
// …
_ = getCache
_ = putCache
}
func UserConfigDir
func UserConfigDir() (string, error)
UserConfigDir возвращает корневой каталог по умолчанию, который используется для данных конфигурации, специфичных для пользователя. Пользователи должны создать свой собственный подкаталог для конкретного приложения в этом каталоге и использовать его.
В системах Unix она возвращает $XDG_CONFIG_HOME, как указано в https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html, если он не пустой, в противном случае — $HOME/.config. В Darwin она возвращает $HOME/Library/Application Support. В Windows она возвращает %AppData%. В Plan 9 она возвращает $home/lib.
Если местоположение не может быть определено (например, $HOME не определено) или путь в $XDG_CONFIG_HOME является относительным, то будет возвращена ошибка.
Пример
package main
import (
"bytes"
"log"
"os"
"path/filepath"
)
func main() {
dir, dirErr := os.UserConfigDir()
var (
configPath string
origConfig []byte
)
if dirErr == nil {
configPath = filepath.Join(dir, "ExampleUserConfigDir", "example.conf")
var err error
origConfig, err = os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
// The user has a config file but we couldn't read it.
// Report the error instead of ignoring their configuration.
log.Fatal(err)
}
}
// Use and perhaps make changes to the config.
config := bytes.Clone(origConfig)
// …
// Save changes.
if !bytes.Equal(config, origConfig) {
if configPath == "" {
log.Printf("not saving config changes: %v", dirErr)
} else {
err := os.MkdirAll(filepath.Dir(configPath), 0700)
if err == nil {
err = os.WriteFile(configPath, config, 0600)
}
if err != nil {
log.Printf("error saving config changes: %v", err)
}
}
}
}
func UserHomeDir
func UserHomeDir() (string, error)
UserHomeDir возвращает домашний каталог текущего пользователя.
В Unix, включая macOS, возвращает переменную среды $HOME. В Windows возвращает %USERPROFILE%. В Plan 9 возвращает переменную среды $home.
Если ожидаемая переменная не задана в среде, UserHomeDir возвращает либо значение по умолчанию, специфичное для платформы, либо ошибку, отличную от nil.
func WriteFile
func WriteFile(name string, data []byte, perm FileMode) error
WriteFile записывает данные в файл с указанным именем, создавая его при необходимости. Если файл не существует, WriteFile создает его с правами доступа perm (до umask); в противном случае WriteFile обрезает его перед записью, не изменяя права доступа. Поскольку WriteFile требует нескольких системных вызовов для завершения, сбой в середине операции может оставить файл в частично записанном состоянии.
Пример
package main
import (
"log"
"os"
)
func main() {
err := os.WriteFile("testdata/hello", []byte("Hello, Gophers!"), 0666)
if err != nil {
log.Fatal(err)
}
}
4.11.2 - Описание типов пакета os языка программирования Go
Основные типы пакета os языка программирования GO
type DirEntry
type DirEntry = fs.DirEntry
DirEntry — это запись, прочитанная из каталога (с помощью функции ReadDir или метода File.ReadDir).
func ReadDir
func ReadDir(name string) ([]DirEntry, error)
ReadDir считывает указанный каталог и возвращает все его записи, отсортированные по имени файла. Если при чтении каталога происходит ошибка, ReadDir возвращает записи, которые удалось прочитать до ошибки, вместе с ошибкой.
Пример
package main
import (
"fmt"
"log"
"os"
)
func main() {
files, err := os.ReadDir(".")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
fmt.Println(file.Name())
}
}
type File
type File struct {
// содержит отфильтрованные или неэкспортируемые поля
}
File представляет открытый файловый дескриптор.
Методы File безопасны для одновременного использования.
func Create
func Create(name string) (*File, error)
Create создает или обрезает файл с указанным именем. Если файл уже существует, он обрезается. Если файл не существует, он создается с режимом 0o666 (до umask). В случае успеха методы возвращенного File могут использоваться для ввода-вывода; связанный файловый дескриптор имеет режим O_RDWR. Каталог, содержащий файл, должен уже существовать. Если произошла ошибка, она будет типа *PathError.
func CreateTemp
func CreateTemp(dir, pattern string) (*File, error)
CreateTemp создает новый временный файл в каталоге dir, открывает файл для чтения и записи и возвращает полученный файл. Имя файла генерируется путем добавления случайной строки в конец pattern. Если pattern содержит "*", случайная строка заменяет последний "*". Файл создается с режимом 0o600 (до umask). Если dir — пустая строка, CreateTemp использует каталог по умолчанию для временных файлов, возвращаемый TempDir. Несколько программ или goroutines, вызывающих CreateTemp одновременно, не будут выбирать один и тот же файл. Вызывающий может использовать метод Name файла, чтобы найти путь к файлу. Вызывающий несет ответственность за удаление файла, когда он больше не нужен.
Пример
package main
import (
"log"
"os"
)
func main() {
f, err := os.CreateTemp("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(f.Name()) // clean up
if _, err := f.Write([]byte("content")); err != nil {
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
}
Пример suffix
package main
import (
"log"
"os"
)
func main() {
f, err := os.CreateTemp("", "example.*.txt")
if err != nil {
log.Fatal(err)
}
defer os.Remove(f.Name()) // clean up
if _, err := f.Write([]byte("content")); err != nil {
f.Close()
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
}
func NewFile
func NewFile(fd uintptr, name string) *File
NewFile возвращает новый File с заданным файловым дескриптором и именем. Возвращаемое значение будет nil, если fd не является действительным файловым дескриптором. В системах Unix, если файловый дескриптор находится в неблокирующем режиме, NewFile попытается вернуть опрашиваемый File (такой, для которого работают методы SetDeadline).
После передачи в NewFile fd может стать недействительным при тех же условиях, что описаны в комментариях к методу Fd, и применяются те же ограничения.
func Open
func Open(name string) (*File, error)
Open открывает указанный файл для чтения. В случае успеха методы возвращенного файла могут быть использованы для чтения; связанный файловый дескриптор имеет режим O_RDONLY. Если произошла ошибка, она будет типа *PathError.
func OpenFile
func OpenFile(name string, flag int, perm FileMode) (*File, error)
OpenFile — это обобщенный вызов open; большинство пользователей вместо него будут использовать Open или Create. Он открывает файл с указанным флагом (O_RDONLY и т. д.). Если файл не существует и передан флаг O_CREATE, он создается с режимом perm (до umask); содержащий его каталог должен существовать. В случае успеха методы возвращенного File можно использовать для ввода-вывода. Если произошла ошибка, она будет типа *PathError.
Пример
package main
import (
"log"
"os"
)
func main() {
f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
}
Пример добавление
package main
import (
"log"
"os"
)
func main() {
// If the file doesn't exist, create it, or append to the file
f, err := os.OpenFile("access.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
if _, err := f.Write([]byte("appended some data\n")); err != nil {
f.Close() // ignore error; Write error takes precedence
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
}
func OpenInRoot
func OpenInRoot(dir, name string) (*File, error)
OpenInRoot открывает файл name в каталоге dir. Это эквивалентно OpenRoot(dir) с последующим открытием файла в корневом каталоге.
OpenInRoot возвращает ошибку, если какой-либо компонент имени name ссылается на местоположение за пределами dir.
См. Root для получения подробной информации и ограничений.
func (*File) Chdir
func (f *File) Chdir() error
Chdir изменяет текущий рабочий каталог на файл, который должен быть каталогом. Если произошла ошибка, она будет типа *PathError.
func (*File) Chmod
func (f *File) Chmod(mode FileMode) error
Chmod изменяет режим файла на mode. Если произошла ошибка, она будет типа *PathError.
func (*File) Chown
func (f *File) Chown(uid, gid int) error
Chown изменяет числовые значения uid и gid указанного файла. Если произошла ошибка, она будет типа *PathError.
В Windows всегда возвращается ошибка syscall.EWINDOWS, обернутая в *PathError.
func (*File) Close
func (f *File) Close() error
Close закрывает файл, делая его недоступным для ввода-вывода. Для файлов, поддерживающих File.SetDeadline, все ожидающие операции ввода-вывода будут отменены и немедленно возвращены с ошибкой ErrClosed. Close вернет ошибку, если он уже был вызван.
func (*File) Fd
func (f *File) Fd() uintptr
Fd возвращает целое число Unix-дескриптора файла, ссылающегося на открытый файл. Если f закрыт, дескриптор файла становится недействительным. Если f подвергается сборке мусора, финализатор может закрыть дескриптор файла, сделав его недействительным; см. runtime.SetFinalizer для получения дополнительной информации о том, когда может быть запущен финализатор. В системах Unix это приведет к прекращению работы методов File.SetDeadline. Поскольку файловые дескрипторы могут быть повторно использованы, возвращенный файловый дескриптор может быть закрыт только с помощью метода File.Close f или его финализатором во время сборки мусора. В противном случае во время сборки мусора финализатор может закрыть не связанный файловый дескриптор с тем же (повторно используемым) номером.
В качестве альтернативы см. метод f.SyscallConn.
func (*File) Name
func (f *File) Name() string
Name возвращает имя файла, представленное в Open.
Безопасно вызывать Name после [Close].
func (*File) Read
func (f *File) Read(b []byte) (n int, err error)
Read считывает до len(b) байтов из File и сохраняет их в b. Он возвращает количество прочитанных байтов и любую возникшую ошибку. В конце файла Read возвращает 0, io.EOF.
func (*File) ReadAt
func (f *File) ReadAt(b []byte, off int64) (n int, err error)
ReadAt считывает len(b) байт из файла, начиная с байтового смещения off. Она возвращает количество прочитанных байтов и ошибку, если таковая имеется. ReadAt всегда возвращает ошибку, отличную от nil, когда n < len(b). В конце файла эта ошибка равна io.EOF.
func (*File) ReadDir
func (f *File) ReadDir(n int) ([]DirEntry, error)
ReadDir считывает содержимое каталога, связанного с файлом f, и возвращает массив значений DirEntry в порядке каталога. Последующие вызовы для того же файла будут возвращать последующие записи DirEntry в каталоге.
Если n > 0, ReadDir возвращает не более n записей DirEntry. В этом случае, если ReadDir возвращает пустой срез, он вернет ошибку с объяснением причины. В конце каталога ошибкой является io.EOF.
Если n <= 0, ReadDir возвращает все записи DirEntry, оставшиеся в каталоге. При успешном выполнении он возвращает ошибку nil (не io.EOF).
func (*File) ReadFrom
func (f *File) ReadFrom(r io.Reader) (n int64, err error)
ReadFrom реализует io.ReaderFrom.
func (*File) Readdir
func (f *File) Readdir(n int) ([]FileInfo, error)
Readdir считывает содержимое каталога, связанного с файлом, и возвращает массив из n значений FileInfo, как это было бы возвращено Lstat, в порядке каталога. Последующие вызовы для того же файла будут возвращать дополнительные FileInfos.
Если n > 0, Readdir возвращает не более n структур FileInfo. В этом случае, если Readdir возвращает пустой срез, он вернет не нулевую ошибку с объяснением причины. В конце каталога ошибкой является io.EOF.
Если n <= 0, Readdir возвращает все FileInfo из каталога в одном срезе. В этом случае, если Readdir выполняется успешно (считывает до конца каталога), он возвращает срез и ошибку nil. Если он встречает ошибку до конца каталога, Readdir возвращает FileInfo, прочитанные до этого момента, и ошибку, отличную от nil.
Большинству клиентов лучше подходит более эффективный метод ReadDir.
func (*File) Readdirnames
func (f *File) Readdirnames(n int) (names []string, err error)
Readdirnames считывает содержимое каталога, связанного с файлом, и возвращает срез из n имен файлов в каталоге в порядке их расположения в каталоге. Последующие вызовы для того же файла будут возвращать дальнейшие имена.
Если n > 0, Readdirnames возвращает не более n имен. В этом случае, если Readdirnames возвращает пустой срез, он возвращает не нулевую ошибку с объяснением причины. В конце каталога ошибкой является io.EOF.
Если n <= 0, Readdirnames возвращает все имена из каталога в одном фрагменте. В этом случае, если Readdirnames выполняется успешно (считывает до конца каталога), он возвращает фрагмент и ошибку nil. Если перед концом каталога возникает ошибка, Readdirnames возвращает имена, прочитанные до этого момента, и ошибку, отличную от nil.
func (*File) Seek
func (f *File) Seek(offset int64, whence int) (ret int64, err error)
Seek устанавливает смещение для следующего чтения или записи в файл, интерпретируемое в соответствии с whence: 0 означает относительно начала файла, 1 означает относительно текущего смещения, а 2 означает относительно конца. Возвращает новое смещение и ошибку, если она есть. Поведение Seek для файла, открытого с O_APPEND, не определено.
func (*File) SetDeadline
func (f *File) SetDeadline(t time.Time) error
SetDeadline устанавливает сроки чтения и записи для файла. Это эквивалентно вызову SetReadDeadline и SetWriteDeadline.
Только некоторые типы файлов поддерживают установку срока. Вызов SetDeadline для файлов, которые не поддерживают сроки, вернет ErrNoDeadline. В большинстве систем обычные файлы не поддерживают сроки, но каналы поддерживают.
Срок — это абсолютное время, по истечении которого операции ввода-вывода завершаются с ошибкой, а не блокируются. Срок применяется ко всем будущим и ожидающим операциям ввода-вывода, а не только к следующему вызову Read или Write. После превышения срока соединение можно обновить, установив срок в будущем.
Если срок превышен, вызов Read или Write или других методов ввода-вывода вернет ошибку, которая оборачивает ErrDeadlineExceeded. Это можно проверить с помощью errors.Is(err, os.ErrDeadlineExceeded). Эта ошибка реализует метод Timeout, и вызов метода Timeout вернет true, но есть и другие возможные ошибки, для которых Timeout вернет true, даже если срок не был превышен.
Таймаут простоя можно реализовать путем многократного продления срока после успешных вызовов Read или Write.
Нулевое значение t означает, что операции ввода-вывода не будут прерываться по истечении таймаута.
func (*File) SetReadDeadline
func (f *File) SetReadDeadline(t time.Time) error
SetReadDeadline устанавливает срок для будущих вызовов Read и любых заблокированных в данный момент вызовов Read. Нулевое значение t означает, что Read не будет прерываться по истечении времени ожидания. Не все файлы поддерживают установку сроков; см. SetDeadline.
func (*File) SetWriteDeadline
func (f *File) SetWriteDeadline(t time.Time) error
SetWriteDeadline устанавливает срок для любых будущих вызовов Write и любых заблокированных в данный момент вызовов Write. Даже если Write превышает время ожидания, он может вернуть n > 0, указывая, что часть данных была успешно записана. Нулевое значение t означает, что Write не превысит время ожидания. Не все файлы поддерживают установку сроков; см. SetDeadline.
func (*File) Stat
func (f *File) Stat() (FileInfo, error)
Stat возвращает структуру FileInfo, описывающую файл. Если произошла ошибка, она будет иметь тип *PathError.
func (*File) Sync
func (f *File) Sync() error
Sync фиксирует текущее содержимое файла в стабильном хранилище. Обычно это означает сброс в память файловой системы копии недавно записанных данных на диск.
func (*File) SyscallConn
func (f *File) SyscallConn() (syscall.RawConn, error)
SyscallConn возвращает необработанный файл. Это реализует интерфейс syscall.Conn.
func (*File) Truncate
func (f *File) Truncate(size int64) error
Truncate изменяет размер файла. Он не изменяет смещение ввода-вывода. Если происходит ошибка, она будет типа *PathError.
func (*File) Write
func (f *File) Write(b []byte) (n int, err error)
Write записывает len(b) байт из b в File. Он возвращает количество записанных байт и ошибку, если она есть. Write возвращает ошибку, отличную от nil, когда n != len(b).
func (*File) WriteAt
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
WriteAt записывает len(b) байт в файл, начиная с байтового смещения off. Он возвращает количество записанных байтов и ошибку, если она есть. WriteAt возвращает ошибку, отличную от nil, когда n != len(b).
Если файл был открыт с флагом O_APPEND, WriteAt возвращает ошибку.
func (*File) WriteString
func (f *File) WriteString(s string) (n int, err error)
WriteString похож на Write, но записывает содержимое строки s, а не фрагмент байтов.
func (*File) WriteTo
func (f *File) WriteTo(w io.Writer) (n int64, err error)
WriteTo реализует io.WriterTo.
type FileInfo
type FileInfo = fs.FileInfo
FileInfo описывает файл и возвращается Stat и Lstat.
func Lstat
func Lstat(name string) (FileInfo, error)
Lstat возвращает FileInfo, описывающий файл с указанным именем. Если файл является символической ссылкой, возвращаемый FileInfo описывает символическую ссылку. Lstat не пытается следовать по ссылке. Если происходит ошибка, она будет типа *PathError.
В Windows, если файл является точкой повторного анализа, которая является заменителем другого именованного объекта (например, символической ссылки или смонтированной папки), возвращаемый FileInfo описывает точку повторного анализа и не пытается ее разрешить.
func Stat
func Stat(name string) (FileInfo, error)
Stat возвращает FileInfo, описывающий именованный файл. Если произошла ошибка, она будет типа *PathError.
type FileMode
type FileMode = fs.FileMode
FileMode представляет режим файла и биты разрешений. Биты имеют одинаковое определение во всех системах, поэтому информация о файлах может быть перенесена из одной системы в другую. Не все биты применимы ко всем системам. Единственный обязательный бит — ModeDir для каталогов.
Пример ¶
type LinkError
type LinkError struct {
Op string
Old string
New string
Err error
}
LinkError записывает ошибку, возникшую во время системного вызова link, symlink или rename, а также пути, которые ее вызвали.
func (*LinkError) Error
func (e *LinkError) Error() string
func (*LinkError) Unwrap
func (e *LinkError) Unwrap() error
type PathError
type PathError = fs.PathError
PathError регистрирует ошибку, а также операцию и путь к файлу, которые ее вызвали.
type ProcAttr
type ProcAttr struct {
// Если Dir не пустой, дочерний процесс переходит в этот каталог перед
// созданием процесса.
Dir string
// Если Env не равно nil, оно возвращает переменные окружения для
// нового процесса в форме, возвращаемой Environ.
// Если оно равно nil, будет использован результат Environ.
Env []string
// Files указывает открытые файлы, унаследованные новым процессом.
// Первые три записи соответствуют стандартному вводу, стандартному выводу и
// стандартной ошибке. Реализация может поддерживать дополнительные записи,
// в зависимости от базовой операционной системы. Запись nil соответствует
// закрытию этого файла при запуске процесса.
// В системах Unix StartProcess изменит эти значения File
// в режим блокировки, что означает, что SetDeadline перестанет работать,
// а вызов Close не прервет Read или Write.
Files []*File
// Атрибуты создания процесса, специфичные для операционной системы.
// Обратите внимание, что установка этого поля означает, что ваша программа
// может не работать должным образом или даже не компилироваться в некоторых
// операционных системах.
Sys *syscall.SysProcAttr
}
ProcAttr содержит атрибуты, которые будут применены к новому процессу, запущенному StartProcess.
type Process
type Process struct {
Pid int
// содержит отфильтрованные или неэкспортируемые поля
}
Process хранит информацию о процессе, созданном StartProcess.
func FindProcess
func FindProcess(pid int) (*Process, error)
FindProcess ищет запущенный процесс по его pid.
Возвращаемый им Process можно использовать для получения информации о процессе базовой операционной системы.
В системах Unix FindProcess всегда выполняется успешно и возвращает Process для заданного pid, независимо от того, существует ли процесс. Чтобы проверить, действительно ли процесс существует, посмотрите, сообщает ли p.Signal(syscall.Signal(0)) об ошибке.
func StartProcess
func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)
StartProcess запускает новый процесс с программой, аргументами и атрибутами, указанными в name, argv и attr. Срез argv станет os.Args в новом процессе, поэтому он обычно начинается с имени программы.
Если вызывающая goroutine заблокировала поток операционной системы с помощью runtime.LockOSThread и изменила любое наследуемое состояние потока на уровне ОС (например, пространства имен Linux или Plan 9), новый процесс унаследует состояние потока вызывающего.
StartProcess является низкоуровневым интерфейсом. Пакет os/exec предоставляет интерфейсы более высокого уровня.
Если произошла ошибка, она будет иметь тип *PathError.
func (*Process) Kill
func (p *Process) Kill() error
Kill вызывает немедленный выход Process. Kill не ждет, пока Process действительно завершится. Это убивает только сам Process, а не другие процессы, которые он мог запустить.
func (*Process) Release
func (p *Process) Release() error
Release освобождает все ресурсы, связанные с процессом p, делая его непригодным для использования в будущем. Release нужно вызывать только в том случае, если Process.Wait не вызывается.
func (*Process) Signal
func (p *Process) Signal(sig Signal) error
Signal посылает сигнал процессу. Отправка Interrupt в Windows не реализована.
func (*Process) Wait
func (p *Process) Wait() (*ProcessState, error)
Wait ожидает завершения процесса, а затем возвращает ProcessState, описывающий его состояние, и ошибку, если она есть. Wait освобождает все ресурсы, связанные с процессом. В большинстве операционных систем процесс должен быть дочерним по отношению к текущему процессу, иначе будет возвращена ошибка.
type ProcessState
type ProcessState struct {
// содержит отфильтрованные или неэкспортируемые поля
}
ProcessState хранит информацию о процессе, как сообщается Wait.
func (*ProcessState) ExitCode
func (p *ProcessState) ExitCode() int
ExitCode возвращает код завершения завершившегося процесса или -1, если процесс не завершился или был прерван сигналом.
func (*ProcessState) Exited
func (p *ProcessState) Exited() bool
Exited сообщает, завершилась ли программа. В системах Unix это возвращает true, если программа завершилась из-за вызова exit, но false, если программа была прервана сигналом.
func (*ProcessState) Pid
func (p *ProcessState) Pid() int
Pid возвращает идентификатор завершившегося процесса.
func (*ProcessState) String
func (p *ProcessState) String() string
func (*ProcessState) Success
func (p *ProcessState) Success() bool
Success сообщает, завершилась ли программа успешно, например, со статусом завершения 0 в Unix.
func (*ProcessState) Sys
func (p *ProcessState) Sys() any
Sys возвращает системную информацию о завершении процесса. Преобразуйте ее в соответствующий базовый тип, например syscall.WaitStatus в Unix, чтобы получить доступ к ее содержимому.
func (*ProcessState) SysUsage
func (p *ProcessState) SysUsage() any
SysUsage возвращает системную информацию об использовании ресурсов завершенного процесса. Преобразуйте ее в соответствующий базовый тип, например *syscall.Rusage в Unix, чтобы получить доступ к ее содержимому. (В Unix *syscall.Rusage соответствует struct rusage, как определено в справочной странице getrusage(2).)
func (*ProcessState) SystemTime
func (p *ProcessState) SystemTime() time.Duration
SystemTime возвращает системное время ЦП завершенного процесса и его дочерних процессов.
func (*ProcessState) UserTime
func (p *ProcessState) UserTime() time.Duration
UserTime возвращает пользовательское время ЦП завершенного процесса и его дочерних процессов.
type Root
type Root struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Root может использоваться только для доступа к файлам в пределах одного дерева каталогов.
Методы Root могут обращаться только к файлам и каталогам, расположенным ниже корневого каталога. Если какой-либо компонент имени файла, переданного методу Root, ссылается на местоположение за пределами корня, метод возвращает ошибку. Имена файлов могут ссылаться на сам каталог (.).
Методы Root будут следовать символьным ссылкам, но символьные ссылки не могут ссылаться на местоположение за пределами корня. Символьные ссылки не должны быть абсолютными.
Методы Root не запрещают переход через границы файловой системы, монтирование Linux, специальные файлы /proc или доступ к файлам устройств Unix.
Методы Root можно безопасно использовать одновременно из нескольких goroutines.
На большинстве платформ создание Root открывает файловый дескриптор или дескриптор, ссылающийся на каталог. Если каталог перемещен, методы Root ссылаются на исходный каталог в его новом местоположении.
Поведение Root отличается на некоторых платформах:
- Когда GOOS=windows, имена файлов не могут ссылаться на зарезервированные Windows имена устройств, такие как NUL и COM1.
- Когда GOOS=js, Root уязвим для атак TOCTOU (time-of-check-time-of-use) при проверке символьных ссылок и не может гарантировать, что операции не выйдут за пределы корня.
- Когда GOOS=plan9 или GOOS=js, Root не отслеживает каталоги при переименовании. На этих платформах Root ссылается на имя каталога, а не на файловый дескриптор.
func OpenRoot
func OpenRoot(name string) (*Root, error)
OpenRoot открывает указанный каталог. Если произошла ошибка, она будет типа *PathError.
func (*Root) Close
func (r *Root) Close() error
Close закрывает Root. После вызова Close методы Root возвращают ошибки.
func (*Root) Create
func (r *Root) Create(name string) (*File, error)
Create создает или обрезает указанный файл в корне. Подробнее см. Create.
func (*Root) FS
func (r *Root) FS() fs.FS
FS возвращает файловую систему (fs.FS) для дерева файлов в корне.
Результат реализует io/fs.StatFS, io/fs.ReadFileFS и io/fs.ReadDirFS.
func (*Root) Lstat
func (r *Root) Lstat(name string) (FileInfo, error)
Lstat возвращает FileInfo, описывающий указанный файл в корневом каталоге. Если файл является символической ссылкой, возвращаемый FileInfo описывает символическую ссылку. Подробнее см. Lstat.
func (*Root) Mkdir
func (r *Root) Mkdir(name string, perm FileMode) error
Mkdir создает новый каталог в корне с указанным именем и битами разрешений (до umask). Подробнее см. Mkdir.
Если perm содержит биты, отличные от девяти младших битов (0o777), OpenFile возвращает ошибку.
func (*Root) Name
func (r *Root) Name() string
Name возвращает имя каталога, представленного OpenRoot.
Безопасно вызывать Name после [Close].
func (*Root) Open
func (r *Root) Open(name string) (*File, error)
Open открывает указанный файл в корне для чтения. Подробнее см. Open.
func (*Root) OpenFile
func (r *Root) OpenFile(name string, flag int, perm FileMode) (*File, error)
OpenFile открывает указанный файл в корне. Подробнее см. OpenFile.
Если perm содержит биты, отличные от девяти младших битов (0o777), OpenFile возвращает ошибку.
func (*Root) OpenRoot
func (r *Root) OpenRoot(name string) (*Root, error)
OpenRoot открывает указанный каталог в корневом каталоге. Если произошла ошибка, она будет типа *PathError.
func (*Root) Remove
func (r *Root) Remove(name string) error
Remove удаляет указанный файл или (пустой) каталог в корневом каталоге. Подробнее см. в разделе Remove.
func (*Root) Stat
func (r *Root) Stat(name string) (FileInfo, error)
Stat возвращает FileInfo, описывающий файл с указанным именем в корне. Подробнее см. Stat.
type Signal
type Signal interface {
String() string
Signal() // для отличия от других Stringers
}
Signal представляет сигнал операционной системы. Обычная базовая реализация зависит от операционной системы: в Unix это syscall.Signal.
var (
Interrupt Signal = syscall.SIGINT
Kill Signal = syscall.SIGKILL)
Единственные значения сигналов, которые гарантированно присутствуют в пакете os на всех системах, — это os.Interrupt (отправить процессу прерывание) и os.Kill (принудительно завершить процесс). В Windows отправка os.Interrupt процессу с помощью os.Process.Signal не реализована; вместо отправки сигнала будет возвращена ошибка.
type SyscallError
type SyscallError struct {
Syscall string
Err error
}
SyscallError регистрирует ошибку от конкретного системного вызова.
func (*SyscallError) Error
func (e *SyscallError) Error() string
func (*SyscallError) Timeout
func (e *SyscallError) Timeout() bool
Timeout сообщает, является ли эта ошибка тайм-аутом.
func (*SyscallError) Unwrap
func (e *SyscallError) Unwrap() error
4.11.3 - Описание функций и типов пакета os/exec языка программирования Go
Пакет exec запускает внешние команды. Он оборачивает os.StartProcess, чтобы упростить перенаправление stdin и stdout, подключение ввода-вывода с помощью труб и другие настройки.
В отличие от вызова библиотеки «system» из C и других языков, пакет os/exec намеренно не вызывает системную оболочку и не расширяет никакие шаблоны glob, а также не обрабатывает другие расширения, конвейеры или перенаправления, которые обычно выполняются оболочками. Пакет ведет себя больше как семейство функций «exec» в C. Чтобы расширить шаблоны glob, либо вызовите оболочку напрямую, позаботившись об экранировании опасных входных данных, либо используйте функцию Glob из пакета path/filepath. Для расширения переменных окружения используйте ExpandEnv из пакета os.
Обратите внимание, что примеры в этом пакете предполагают использование системы Unix. Они могут не работать в Windows и не работают в Go Playground, используемом golang.org и godoc.org.
Исполняемые файлы в текущем каталоге
Функции Command и LookPath ищут программу в каталогах, перечисленных в текущем пути, следуя соглашениям операционной системы хоста. На протяжении десятилетий операционные системы включали текущий каталог в этот поиск, иногда неявно, а иногда явно настраивая его таким образом по умолчанию. Современная практика такова, что включение текущего каталога обычно является неожиданным и часто приводит к проблемам безопасности.
Чтобы избежать этих проблем безопасности, начиная с Go 1.19, этот пакет не будет разрешать программу, используя явную или неявную запись пути относительно текущего каталога. То есть, если вы запустите LookPath(“go”), он не вернет ./go в Unix и .\go.exe в Windows, независимо от того, как настроен путь. Вместо этого, если обычные алгоритмы пути приведут к такому ответу, эти функции возвращают ошибку err, удовлетворяющую errors.Is(err, ErrDot).
Рассмотрим, например, эти два фрагмента программы:
path, err := exec.LookPath("prog")
if err != nil {
log.Fatal(err)
}
use(path)
cmd := exec.Command("prog")
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
Они не найдут и не запустят ./prog или .\prog.exe, независимо от того, как настроен текущий путь.
Код, который всегда хочет запускать программу из текущего каталога, можно переписать так, чтобы вместо “prog” он говорил “./prog”.
Код, который настаивает на включении результатов из записей относительных путей, может вместо этого отменить ошибку с помощью проверки errors.Is:
path, err := exec.LookPath("prog")
if errors.Is(err, exec.ErrDot) {
err = nil
}
if err != nil {
log.Fatal(err)
}
use(path)
cmd := exec.Command("prog")
if errors.Is(cmd.Err, exec.ErrDot) {
cmd.Err = nil
}
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
Установка переменной окружения GODEBUG=execerrdot=0 полностью отключает генерацию ErrDot, временно восстанавливая поведение, существовавшее до Go 1.19, для программ, которые не могут применить более целевые исправления. В будущих версиях Go поддержка этой переменной может быть удалена.
Прежде чем добавлять такие переопределения, убедитесь, что вы понимаете последствия этого для безопасности. Дополнительные сведения см. на сайте https://go.dev/blog/path-security.
Переменные
var ErrDot = errors.New("cannot run executable found relative to current directory")
ErrDot указывает, что поиск пути привел к появлению исполняемого файла в текущем каталоге из-за ‘.’ в пути, либо неявно, либо явно. Подробности см. в документации к пакету.
Обратите внимание, что функции этого пакета не возвращают ErrDot напрямую. Для проверки того, связана ли возвращаемая ошибка err с этим условием, в коде следует использовать errors.Is(err, ErrDot), а не err == ErrDot.
var ErrNotFound = errors.New("исполняемый файл не найден в $PATH")
ErrNotFound - это ошибка, возникающая, если при поиске по пути не удалось найти исполняемый файл.
var ErrWaitDelay = errors.New("exec: WaitDelay expired before I/O complete")
ErrWaitDelay возвращается Cmd.Wait, если процесс завершается с успешным кодом состояния, но его выходные трубы не закрыты до истечения WaitDelay команды.
Функции
func LookPath
func LookPath(file string) (string, error)
LookPath ищет исполняемый файл с именем file в каталогах, указанных переменной окружения PATH. Если файл содержит косую черту, то поиск ведется напрямую и PATH не используется. В противном случае, при успехе, результатом будет абсолютный путь.
В старых версиях Go LookPath мог возвращать путь относительно текущего каталога. Начиная с Go 1.19, LookPath будет возвращать этот путь вместе с ошибкой, удовлетворяющей errors.Is(err, ErrDot). Более подробную информацию см. в документации к пакету.
Пример
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
path, err := exec.LookPath("fortune")
if err != nil {
log.Fatal("installing fortune is in your future")
}
fmt.Printf("fortune is available at %s\n", path)
}
Типы
type Cmd
type Cmd struct {
// Path — путь к выполняемой команде.
//
// Это единственное поле, которое должно иметь значение, отличное от нуля.
// Если Path является относительным, оно оценивается относительно
// Dir.
Path string
// Args содержит аргументы командной строки, включая команду как Args[0].
// Если поле Args пустое или равно nil, Run использует {Path}.
//
// В типичном случае Path и Args задаются вызовом Command.
Args []string
// Env задает среду процесса.
// Каждая запись имеет вид «ключ=значение».
// Если Env равно nil, новый процесс использует среду текущего процесса.
//
// Если Env содержит дубликаты ключей среды, используется только последнее
// значение в срезе для каждого дубликата ключа.
// В качестве особого случая в Windows SYSTEMROOT всегда добавляется, если
// отсутствует и явно не установлен в пустую строку.
//
// См. также поле Dir, которое может устанавливать PWD в среде.
Env []string
// Dir указывает рабочий каталог команды.
// Если Dir является пустой строкой, Run запускает команду в
// текущем каталоге вызывающего процесса.
//
// В системах Unix значение Dir также определяет
// переменную окружения PWD дочернего процесса, если не указано иное
//. Процесс Unix представляет свой рабочий каталог
// не по имени, а как неявную ссылку на узел в
// дереве файлов. Таким образом, если дочерний процесс получает свой рабочий
// каталог путем вызова функции, такой как getcwd в C, которая
// вычисляет каноническое имя, проходя по дереву файлов, он
// не восстановит исходное значение Dir, если это значение
// было псевдонимом, включающим символьные ссылки. Однако, если
// дочерний процесс вызывает [os.Getwd] в Go или
// get_current_dir_name из GNU C, и значение PWD является псевдонимом для
// текущего каталога, эти функции вернут
// значение PWD, которое совпадает со значением Dir.
Dir string
// Stdin указывает стандартный ввод процесса.
//
// Если Stdin равно nil, процесс читает из нулевого устройства (os.DevNull).
//
// Если Stdin является *os.File, стандартный ввод процесса подключается
// непосредственно к этому файлу.
//
// В противном случае во время выполнения команды отдельная
// goroutine считывает данные из Stdin и передает их команде
// через канал. В этом случае Wait не завершается, пока goroutine
// не прекратит копирование, либо потому что достиг конца Stdin
// (EOF или ошибка чтения), либо потому, что запись в канал вернула ошибку,
// либо потому, что было установлено ненулевое значение WaitDelay, которое истекло.
Stdin io.Reader
// Stdout и Stderr указывают стандартный вывод и ошибки процесса.
//
// Если любой из них равен nil, Run подключает соответствующий файловый дескриптор
// к нулевому устройству (os.DevNull).
//
// Если любой из них является *os.File, соответствующий вывод из процесса
// подключается непосредственно к этому файлу.
//
// В противном случае во время выполнения команды отдельная goroutine
// считывает данные из процесса через канал и доставляет их в
// соответствующий Writer. В этом случае Wait не завершается, пока
// goroutine не достигнет EOF, не столкнется с ошибкой или не истечет ненулевое значение WaitDelay
//.
//
// Если Stdout и Stderr являются одним и тем же writer и имеют тип, который можно
// сравнить с помощью ==, то одновременно Write будет вызывать не более одного goroutine.
Stdout io.Writer
Stderr io.Writer
// ExtraFiles указывает дополнительные открытые файлы, которые будут унаследованы
// новый процесс. Он не включает стандартный ввод, стандартный вывод или
// стандартную ошибку. Если не равен nil, запись i становится файловым дескриптором 3+i.
//
// ExtraFiles не поддерживается в Windows.
ExtraFiles []*os.File
// SysProcAttr содержит дополнительные атрибуты, специфичные для операционной системы.
// Run передает его os.StartProcess как поле Sys os.ProcAttr.
SysProcAttr *syscall.SysProcAttr
// Process — это базовый процесс после запуска.
Process *os.Process
// ProcessState содержит информацию о завершенном процессе.
// Если процесс был запущен успешно, Wait или Run
// заполнят его ProcessState по завершении команды.
ProcessState *os.ProcessState
Err error // Ошибка LookPath, если есть.
// Если Cancel не равен nil, команда должна быть создана с помощью
// CommandContext, и Cancel будет вызван, когда
// Context команды будет выполнен. По умолчанию CommandContext устанавливает Cancel в
// вызывает метод Kill в процессе команды.
//
// Обычно пользовательский Cancel посылает сигнал процессу команды
//, но вместо этого он может предпринять другие действия для инициирования отмены,
// такие как закрытие канала stdin или stdout или отправка запроса на завершение работы
// сетевого сокета.
//
// Если команда завершается с успешным статусом после вызова Cancel
//, а Cancel не возвращает ошибку, эквивалентную
// os.ErrProcessDone, то Wait и подобные методы будут возвращать не нулевую
// ошибку: либо ошибку, оборачивающую ошибку, возвращенную Cancel,
// либо ошибку из Context.
// (Если команда завершается с неуспешным статусом или Cancel
// возвращает ошибку, которая оборачивает os.ErrProcessDone, Wait и подобные методы
// продолжают возвращать обычный статус завершения команды.)
//
// Если Cancel установлен в nil, ничего не произойдет сразу после завершения
// Context команды, но WaitDelay, отличное от нуля, все равно будет действовать. Это может
// быть полезно, например, для обхода тупиковых ситуаций в командах, которые не
// поддерживают сигналы завершения, но должны всегда завершаться быстро.
//
// Cancel не будет вызван, если Start возвращает ошибку, отличную от nil.
Cancel func() error
// Если WaitDelay отлично от нуля, оно ограничивает время ожидания двух источников
// непредвиденной задержки в Wait: дочернего процесса, который не завершается после
// отмены связанного Context, и дочернего процесса, который завершается, но оставляет
// свои каналы ввода-вывода незакрытыми.
//
// Таймер WaitDelay запускается, когда связанный контекст завершается или
// вызов Wait обнаруживает, что дочерний процесс завершился, в зависимости от того, что произойдет
// первым. По истечении задержки команда завершает дочерний процесс
// и/или его каналы ввода-вывода.
//
// Если дочерний процесс не завершился — возможно, потому что он проигнорировал или
// не получил сигнал о завершении от функции Cancel, или потому что не была
// установлена функция Cancel — то он будет завершен с помощью os.Process.Kill.
//
// Затем, если каналы ввода-вывода, связывающиеся с дочерним процессом, все еще открыты,
// эти каналы закрываются, чтобы разблокировать все goroutines, которые в данный момент заблокированы
// вызовами Read или Write.
//
// Если трубы закрыты из-за WaitDelay, вызов Cancel не произошел,
// и команда завершилась с успешным статусом, Wait и
// подобные методы вернут ErrWaitDelay вместо nil.
//
// Если WaitDelay равен нулю (по умолчанию), трубы ввода-вывода будут читаться до EOF,
// что может не произойти, пока осиротевшие подпроцессы команды
// также не закроют свои дескрипторы для каналов.
WaitDelay time.Duration
// содержит отфильтрованные или неэкспортированные поля
}
Cmd представляет внешнюю команду, которая готовится или запускается.
Cmd не может быть повторно использован после вызова методов Cmd.Run, Cmd.Output или Cmd.CombinedOutput.
func Command
func Command(name string, arg ...string) *Cmd
Command возвращает структуру Cmd для выполнения именованной программы с заданными аргументами.
В возвращаемой структуре задаются только Path и Args.
Если имя не содержит разделителей путей, Command использует LookPath для преобразования имени в полный путь, если это возможно. В противном случае она использует имя непосредственно в качестве Path.
Возвращаемое поле Args Cmd строится из имени команды, за которым следуют элементы arg, поэтому arg не должно включать само имя команды. Например, Command(“echo”, “hello”). Args[0] - это всегда имя, а не возможно разрешенный Path.
В Windows процессы получают всю командную строку как единую строку и выполняют собственный разбор. Command объединяет и заключает Args в кавычки в строку командной строки с помощью алгоритма, совместимого с приложениями, использующими CommandLineToArgvW (это наиболее распространенный способ). Заметными исключениями являются msiexec.exe и cmd.exe (и, соответственно, все пакетные файлы), которые имеют другой алгоритм снятия кавычек. В этих и других подобных случаях вы можете выполнить котирование самостоятельно и указать полную командную строку в SysProcAttr.CmdLine, оставив Args пустым.
Пример
package main
import (
"fmt"
"log"
"os/exec"
"strings"
)
func main() {
cmd := exec.Command("tr", "a-z", "A-Z")
cmd.Stdin = strings.NewReader("some input")
var out strings.Builder
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
fmt.Printf("in all caps: %q\n", out.String())
}
Пример с окружением среды
package main
import (
"log"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("prog")
cmd.Env = append(os.Environ(),
"FOO=duplicate_value", // ignored
"FOO=actual_value", // this value is used
)
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
func CommandContext
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd
CommandContext похож на Command, но включает в себя контекст.
Предоставленный контекст используется для прерывания процесса (путем вызова cmd.Cancel или os.Process.Kill), если контекст завершается до того, как команда завершит свою работу.
CommandContext устанавливает функцию Cancel команды для вызова метода Kill в ее Process и оставляет WaitDelay не установленным. Вызывающая сторона может изменить поведение отмены, изменив эти поля перед запуском команды.
Пример
package main
import (
"context"
"os/exec"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil {
// This will fail after 100 milliseconds. The 5 second sleep
// will be interrupted.
}
}
func (*Cmd) CombinedOutput
func (c *Cmd) CombinedOutput() ([]byte, error)
CombinedOutput запускает команду и возвращает ее объединенный стандартный вывод и стандартную ошибку.
Пример
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", stdoutStderr)
}
func (*Cmd) Environ
func (c *Cmd) Environ() []string
Environ возвращает копию среды, в которой будет запущена команда, в том виде, в котором она настроена в данный момент.
Пример
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("pwd")
// Set Dir before calling cmd.Environ so that it will include an
// updated PWD variable (on platforms where that is used).
cmd.Dir = ".."
cmd.Env = append(cmd.Environ(), "POSIXLY_CORRECT=1")
out, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", out)
}
func (*Cmd) Output
func (c *Cmd) Output() ([]byte, error)
Output запускает команду и возвращает ее стандартный вывод. Любая возвращаемая ошибка обычно будет иметь тип *ExitError. Если c.Stderr было nil, а возвращаемая ошибка имеет тип *ExitError, Output заполняет поле Stderr возвращаемой ошибки.
Пример
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
out, err := exec.Command("date").Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("The date is %s\n", out)
}
func (*Cmd) Run
func (c *Cmd) Run() error
Run запускает указанную команду и ждет ее завершения.
Возвращаемая ошибка равна nil, если команда запущена, не возникло проблем с копированием stdin, stdout и stderr, и она завершилась с нулевым статусом выхода.
Если команда запущена, но не завершилась успешно, ошибка будет типа *ExitError. В других ситуациях могут возвращаться ошибки других типов.
Если вызывающая goroutine заблокировала поток операционной системы с помощью runtime.LockOSThread и изменила любое наследуемое состояние потока на уровне ОС (например, пространства имен Linux или Plan 9), новый процесс унаследует состояние потока вызывающего.
Пример
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sleep", "1")
log.Printf("Running command and waiting for it to finish...")
err := cmd.Run()
log.Printf("Command finished with error: %v", err)
}
func (*Cmd) Start
func (c *Cmd) Start() error
Start запускает указанную команду, но не ждет ее завершения.
Если Start возвращается успешно, поле c.Process будет установлено.
После успешного вызова Start необходимо вызвать метод Cmd.Wait, чтобы освободить связанные системные ресурсы.
Пример
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sleep", "5")
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
}
func (*Cmd) StderrPipe
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
StderrPipe возвращает канал, который будет подключен к стандартной ошибке команды при ее запуске.
Cmd.Wait закроет канал после завершения команды, поэтому большинству вызывающих не нужно закрывать канал самостоятельно. Таким образом, неправильно вызывать Wait до завершения всех операций чтения из канала. По той же причине неправильно использовать Cmd.Run при использовании StderrPipe. Идиоматическое использование см. в примере StdoutPipe.
Пример
package main
import (
"fmt"
"io"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal(err)
}
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
slurp, _ := io.ReadAll(stderr)
fmt.Printf("%s\n", slurp)
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
}
func (*Cmd) StdinPipe
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
StdinPipe возвращает канал, который будет подключен к стандартному входу команды при ее запуске. Канал будет автоматически закрыт после того, как Cmd.Wait увидит выход команды. Вызывающему достаточно вызвать Close, чтобы заставить канал закрыться раньше. Например, если запускаемая команда не завершится до закрытия стандартного ввода, вызывающий должен закрыть канал.
Пример
package main
import (
"fmt"
"io"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("cat")
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
go func() {
defer stdin.Close()
io.WriteString(stdin, "values written to stdin are passed to cmd's standard input")
}()
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", out)
}
func (*Cmd) StdoutPipe
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
StdoutPipe возвращает канал, который будет подключен к стандартному выводу команды при ее запуске.
Cmd.Wait закроет канал после завершения команды, поэтому большинству вызывающих не нужно закрывать канал самостоятельно. Таким образом, неправильно вызывать Wait до завершения всех операций чтения из канала. По той же причине неправильно вызывать Cmd.Run при использовании StdoutPipe. См. пример для идиоматического использования.
Пример
package main
import (
"encoding/json"
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`)
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
var person struct {
Name string
Age int
}
if err := json.NewDecoder(stdout).Decode(&person); err != nil {
log.Fatal(err)
}
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
fmt.Printf("%s is %d years old\n", person.Name, person.Age)
func (*Cmd) String
func (c *Cmd) String() string
String возвращает удобочитаемое описание c. Предназначено только для отладки. В частности, не подходит для использования в качестве ввода в оболочку. Вывод String может различаться в разных версиях Go.
func (*Cmd) Wait
func (c *Cmd) Wait() error
Wait ожидает завершения команды и завершения копирования в stdin или копирования из stdout или stderr.
Команда должна быть запущена с помощью Cmd.Start.
Возвращаемая ошибка равна nil, если команда запущена, не имеет проблем с копированием stdin, stdout и stderr и завершается с нулевым статусом выхода.
Если команда не запускается или не завершается успешно, ошибка имеет тип *ExitError. Другие типы ошибок могут возвращаться при проблемах с вводом-выводом.
Если c.Stdin, c.Stdout или c.Stderr не являются *os.File, Wait также ожидает завершения соответствующего цикла ввода-вывода в процесс или из процесса.
Wait освобождает все ресурсы, связанные с Cmd.
type Error
type Error struct {
// Name — имя файла, для которого произошла ошибка.
Name string
// Err — базовая ошибка.
Err error
}
Error возвращается LookPath, когда он не может классифицировать файл как исполняемый.
func (*Error) Error
func (e *Error) Error() string
func (*Error) Unwrap
func (e *Error) Unwrap() error
type ExitError
type ExitError struct {
*os.ProcessState
// Stderr содержит подмножество стандартного вывода ошибок из
// метода Cmd.Output, если стандартные ошибки не были
// собраны иным способом.
//
// Если вывод ошибок длинный, Stderr может содержать только префикс
// и суффикс вывода, а середина будет заменена
// текстом о количестве пропущенных байтов.
//
// Stderr предоставляется для отладки, для включения в сообщения об ошибках.
// Пользователи с другими потребностями должны перенаправить Cmd.Stderr по мере необходимости.
Stderr []byte
}
ExitError сообщает о неудачном завершении команды.
func (*ExitError) Error
func (e *ExitError) Error() string
4.11.4 - Пакет os/user языка программирования Go
Пакет user позволяет искать учетные записи пользователей по имени или идентификатору.
Для большинства систем Unix этот пакет имеет две внутренние реализации преобразования идентификаторов пользователей и групп в имена, а также вывода списка дополнительных идентификаторов групп. Одна из них написана на чистом Go и анализирует файлы /etc/passwd и /etc/group. Другая основана на cgo и использует стандартные процедуры библиотеки C (libc), такие как getpwuid_r, getgrnam_r и getgrouplist.
Если cgo доступен, а необходимые процедуры реализованы в libc для конкретной платформы, используется код на основе cgo (с поддержкой libc). Это можно переопределить с помощью тега сборки osusergo, который принудительно использует реализацию на чистом Go.
type Group
type Group struct {
Gid string // идентификатор группы
Name string // название группы
}
Group представляет группу пользователей.
В системах POSIX Gid содержит десятичное число, представляющее идентификатор группы.
func LookupGroup
func LookupGroup(name string) (*Group, error)
LookupGroup ищет группу по имени. Если группа не найдена, возвращается ошибка типа UnknownGroupError.
func LookupGroupId
func LookupGroupId(gid string) (*Group, error)
LookupGroupId ищет группу по groupid. Если группа не найдена, возвращается ошибка типа UnknownGroupIdError.
type UnknownGroupError
type UnknownGroupError string
UnknownGroupError возвращается LookupGroup, когда группа не может быть найдена.
func (UnknownGroupError) Error
func (e UnknownGroupError) Error() string
type UnknownGroupIdError
type UnknownGroupIdError string
UnknownGroupIdError возвращается LookupGroupId, когда группа не может быть найдена.
func (UnknownGroupIdError) Error
func (e UnknownGroupIdError) Error() string
type UnknownUserError
type UnknownUserError string
UnknownUserError возвращается Lookup, когда пользователь не может быть найден.
func (UnknownUserError) Error
func (e UnknownUserError) Error() string
type UnknownUserIdError
type UnknownUserIdError int
UnknownUserIdError возвращается LookupId, когда пользователь не может быть найден.
func (UnknownUserIdError) Error
func (e UnknownUserIdError) Error() string
type User ¶
type User struct {
// Uid — это идентификатор пользователя.
// В системах POSIX это десятичное число, представляющее uid.
// В Windows это идентификатор безопасности (SID) в формате строки.
// В Plan 9 это содержимое /dev/user.
Uid string
// Gid — это идентификатор основной группы.
// В системах POSIX это десятичное число, представляющее gid.
// В Windows это SID в формате строки.
// В Plan 9 это содержимое /dev/user.
Gid string
// Username — это имя для входа в систему.
Username string
// Name — это настоящее или отображаемое имя пользователя.
// Оно может быть пустым.
// В системах POSIX это первая (или единственная) запись в поле GECOS
// списка GECOS.
// В Windows это отображаемое имя пользователя.
// В Plan 9 это содержимое /dev/user.
Строка имени
// HomeDir — это путь к домашнему каталогу пользователя (если он есть).
Строка HomeDir
}
User представляет учетную запись пользователя.
func Current
func Current() (*User, error)
Current возвращает текущего пользователя.
При первом вызове информация о текущем пользователе будет сохранена в кэше. Последующие вызовы будут возвращать значение из кэша и не будут отражать изменения текущего пользователя.
func Lookup
func Lookup(username string) (*User, error)
Lookup ищет пользователя по имени. Если пользователь не найден, возвращается ошибка типа UnknownUserError.
func LookupId
func LookupId(uid string) (*User, error)
LookupId ищет пользователя по идентификатору. Если пользователь не найден, возвращается ошибка типа UnknownUserIdError.
func (*User) GroupIds
func (u *User) GroupIds() ([]string, error)
GroupIds возвращает список идентификаторов групп, членом которых является пользователь.
4.11.5 - Пакет os/signal языка программирования Go
Пакет signal реализует доступ к входящим сигналам.
Сигналы в основном используются в Unix-подобных системах. Для использования этого пакета в Windows и Plan 9 см. ниже.
Типы сигналов
Сигналы SIGKILL и SIGSTOP не могут быть перехвачены программой и, следовательно, не могут быть затронуты этим пакетом.
Синхронные сигналы — это сигналы, вызываемые ошибками при выполнении программы: SIGBUS, SIGFPE и SIGSEGV. Они считаются синхронными только в том случае, если вызваны выполнением программы, а не отправлены с помощью os.Process.Kill, программы kill или аналогичного механизма. В общем случае, за исключением описанного ниже, программы Go преобразуют синхронный сигнал в панику во время выполнения.
Остальные сигналы являются асинхронными. Они не вызываются ошибками программы, а отправляются из ядра или из другой программы.
Из асинхронных сигналов сигнал SIGHUP отправляется, когда программа теряет свой управляющий терминал. Сигнал SIGINT отправляется, когда пользователь на управляющем терминале нажимает символ прерывания, который по умолчанию является ^C (Control-C). Сигнал SIGQUIT отправляется, когда пользователь на управляющем терминале нажимает символ выхода, который по умолчанию является ^\ (Control-Backslash). Как правило, вы можете просто завершить программу, нажав ^C, а также завершить ее с дампом стека, нажав ^.
Поведение сигналов по умолчанию в программах Go
По умолчанию синхронный сигнал преобразуется в панику во время выполнения. Сигнал SIGHUP, SIGINT или SIGTERM приводит к завершению программы. Сигнал SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT или SIGSYS приводит к завершению программы с дампом стека. Сигнал SIGTSTP, SIGTTIN или SIGTTOU получает поведение по умолчанию системы (эти сигналы используются оболочкой для управления задачами). Сигнал SIGPROF обрабатывается непосредственно средой выполнения Go для реализации runtime.CPUProfile. Другие сигналы будут перехвачены, но никаких действий не будет предпринято.
Если программа Go запущена с игнорированием сигналов SIGHUP или SIGINT (обработчик сигналов установлен в SIG_IGN), они будут по-прежнему игнорироваться.
Если программа Go запущена с непустой маской сигналов, она, как правило, будет соблюдаться. Однако некоторые сигналы явно разблокированы: синхронные сигналы, SIGILL, SIGTRAP, SIGSTKFLT, SIGCHLD, SIGPROF, а в Linux — сигналы 32 (SIGCANCEL) и 33 (SIGSETXID) (SIGCANCEL и SIGSETXID используются внутренне glibc). Подпроцессы, запущенные с помощью os.Exec или os/exec, унаследуют измененную маску сигналов.
Изменение поведения сигналов в программах Go
Функции в этом пакете позволяют программе изменять способ обработки сигналов программами Go.
Notify отключает поведение по умолчанию для заданного набора асинхронных сигналов и вместо этого доставляет их по одному или нескольким зарегистрированным каналам. В частности, это относится к сигналам SIGHUP, SIGINT, SIGQUIT, SIGABRT и SIGTERM. Это также относится к сигналам управления задачами SIGTSTP, SIGTTIN и SIGTTOU, в этом случае поведение по умолчанию системы не происходит. Она также применяется к некоторым сигналам, которые в противном случае не вызывают никаких действий: SIGUSR1, SIGUSR2, SIGPIPE, SIGALRM, SIGCHLD, SIGCONT, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGWINCH, SIGIO, SIGPWR, SIGINFO, SIGTHR, SIGWAITING, SIGLWP, SIGFREEZE, SIGTHAW, SIGLOST, SIGXRES, SIGJVM1, SIGJVM2 и любые сигналы реального времени, используемые в системе. Обратите внимание, что не все эти сигналы доступны во всех системах.
Если программа была запущена с игнорированием SIGHUP или SIGINT, и для любого из этих сигналов вызван Notify, для этого сигнала будет установлен обработчик сигналов, и он больше не будет игнорироваться. Если позже для этого сигнала вызван Reset или Ignore, или вызван Stop для всех каналов, переданных Notify для этого сигнала, сигнал снова будет игнорироваться. Reset восстановит поведение системы по умолчанию для сигнала, а Ignore заставит систему полностью игнорировать сигнал.
Если программа запущена с непустой маской сигналов, некоторые сигналы будут явно разблокированы, как описано выше. Если Notify вызывается для заблокированного сигнала, он будет разблокирован. Если позже для этого сигнала вызывается Reset или Stop для всех каналов, переданных Notify для этого сигнала, сигнал снова будет заблокирован.
SIGPIPE
Когда программа на Go записывает в разорванный канал, ядро генерирует сигнал SIGPIPE.
Если программа не вызвала Notify для получения сигналов SIGPIPE, то поведение зависит от номера файлового дескриптора. Запись в разорванный канал на файловых дескрипторах 1 или 2 (стандартный вывод или стандартная ошибка) приведет к завершению программы с сигналом SIGPIPE. Запись в разорванный канал на каком-либо другом файловом дескрипторе не вызовет никаких действий по сигналу SIGPIPE, и запись завершится с ошибкой EPIPE.
Если программа вызвала Notify для получения сигналов SIGPIPE, номер файлового дескриптора не имеет значения. Сигнал SIGPIPE будет доставлен в канал Notify, и запись завершится с ошибкой EPIPE.
Это означает, что по умолчанию программы командной строки будут вести себя как типичные программы командной строки Unix, в то время как другие программы не будут завершаться с SIGPIPE при записи в закрытое сетевое соединение.
Программы Go, использующие cgo или SWIG
В программе Go, которая включает код, не относящийся к Go, обычно код C/C++, доступ к которому осуществляется с помощью cgo или SWIG, сначала запускается код запуска Go. Он настраивает обработчики сигналов в соответствии с ожиданиями среды выполнения Go, прежде чем запускается код запуска, не относящийся к Go. Если код запуска, не относящийся к Go, желает установить свои собственные обработчики сигналов, он должен предпринять определенные шаги, чтобы Go продолжал работать нормально. В этом разделе описаны эти шаги и общие изменения, которые могут быть внесены в настройки обработчиков сигналов кодом, не относящимся к Go, в программах Go. В редких случаях код, не относящийся к Go, может запускаться перед кодом Go, и в этом случае также применяется следующий раздел.
Если код, не относящийся к Go, вызываемый программой Go, не изменяет обработчики сигналов или маски, то поведение будет таким же, как и для чистой программы Go.
Если код, не относящийся к Go, устанавливает какие-либо обработчики сигналов, он должен использовать флаг SA_ONSTACK с sigaction. Невыполнение этого требования может привести к сбою программы при получении сигнала. Программы Go обычно работают с ограниченным стеком, поэтому настраивают альтернативный стек сигналов.
Если код, не относящийся к Go, устанавливает обработчик сигналов для любого из синхронных сигналов (SIGBUS, SIGFPE, SIGSEGV), то он должен записывать существующий обработчик сигналов Go. Если эти сигналы возникают во время выполнения кода Go, он должен вызывать обработчик сигналов Go (возникновение сигнала во время выполнения кода Go можно определить, посмотрев на PC, передаваемый обработчику сигналов). В противном случае некоторые паники времени выполнения Go не будут происходить, как ожидается.
Если код, не относящийся к Go, устанавливает обработчик сигналов для любого из асинхронных сигналов, он может вызывать обработчик сигналов Go или нет, по своему усмотрению. Естественно, если он не вызывает обработчик сигналов Go, описанное выше поведение Go не будет происходить. Это может быть проблемой, в частности, с сигналом SIGPROF.
Код, не относящийся к Go, не должен изменять маску сигналов в любых потоках, созданных средой выполнения Go. Если код, не относящийся к Go, сам запускает новые потоки, эти потоки могут устанавливать маску сигналов по своему усмотрению.
Если код, не относящийся к Go, запускает новый поток, изменяет маску сигналов, а затем вызывает функцию Go в этом потоке, среда выполнения Go автоматически разблокирует определенные сигналы: синхронные сигналы, SIGILL, SIGTRAP, SIGSTKFLT, SIGCHLD, SIGPROF, SIGCANCEL и SIGSETXID. Когда функция Go возвращается, маска сигналов, не относящаяся к Go, будет восстановлена.
Если обработчик сигналов Go вызывается в потоке, не относящемся к Go и не выполняющем код Go, обработчик обычно пересылает сигнал в код, не относящийся к Go, следующим образом. Если сигнал является SIGPROF, обработчик Go ничего не делает. В противном случае обработчик Go удаляет себя, разблокирует сигнал и снова его генерирует, чтобы вызвать любой обработчик, не относящийся к Go, или системный обработчик по умолчанию. Если программа не завершается, обработчик Go переустанавливает себя и продолжает выполнение программы.
Если получен сигнал SIGPIPE, программа Go вызовет специальную обработку, описанную выше, если SIGPIPE получен в потоке Go. Если SIGPIPE получен в потоке, не относящемся к Go, сигнал будет перенаправлен в обработчик, не относящийся к Go, если таковой имеется; если его нет, обработчик по умолчанию системы приведет к завершению программы.
Программы, не написанные на Go, которые вызывают код Go
Когда код Go компилируется с такими опциями, как -buildmode=c-shared, он будет запускаться как часть существующей программы, не написанной на Go. Код, не написанный на Go, может уже иметь установленные обработчики сигналов, когда запускается код Go (это также может произойти в необычных случаях при использовании cgo или SWIG; в этом случае применимо обсуждение, приведенное здесь). Для -buildmode=c-archive среда выполнения Go инициализирует сигналы во время глобального конструктора. Для -buildmode=c-shared среда выполнения Go инициализирует сигналы при загрузке разделяемой библиотеки.
Если среда выполнения Go обнаруживает существующий обработчик сигналов для сигналов SIGCANCEL или SIGSETXID (которые используются только в Linux), она включает флаг SA_ONSTACK и в остальном сохраняет обработчик сигналов.
Для синхронных сигналов и SIGPIPE среда выполнения Go установит обработчик сигналов. Она сохранит любой существующий обработчик сигналов. Если синхронный сигнал поступает во время выполнения кода, не относящегося к Go, среда выполнения Go вызовет существующий обработчик сигналов вместо обработчика сигналов Go.
Код Go, скомпилированный с -buildmode=c-archive или -buildmode=c-shared, по умолчанию не будет устанавливать никаких других обработчиков сигналов. Если существующий обработчик сигналов, среда выполнения Go включит флаг SA_ONSTACK и сохранит обработчик сигналов. Если Notify вызывается для асинхронного сигнала, для этого сигнала будет установлен обработчик сигналов Go. Если позже для этого сигнала вызывается Reset, исходная обработка этого сигнала будет переустановлена, восстанавливая обработчик сигналов, не относящийся к Go, если таковой имеется.
Код Go, скомпилированный без -buildmode=c-archive или -buildmode=c-shared, установит обработчик сигналов для асинхронных сигналов, перечисленных выше, и сохранит любой существующий обработчик сигналов. Если сигнал доставляется в поток, не относящийся к Go, он будет действовать, как описано выше, за исключением того, что если существует обработчик сигналов, не относящийся к Go, этот обработчик будет установлен перед генерацией сигнала.
Windows ¶
В Windows ^C (Control-C) или ^BREAK (Control-Break) обычно приводят к завершению программы. Если Notify вызывается для os.Interrupt, ^C или ^BREAK приводят к отправке os.Interrupt по каналу, и программа не завершается. Если вызывается Reset или Stop на всех каналах, переданных Notify, то поведение по умолчанию будет восстановлено.
Кроме того, если вызывается Notify, и Windows отправляет CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT или CTRL_SHUTDOWN_EVENT процессу, Notify вернет syscall.SIGTERM. В отличие от Control-C и Control-Break, Notify не изменяет поведение процесса при получении CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT или CTRL_SHUTDOWN_EVENT — процесс все равно будет завершен, если он не завершится самостоятельно. Но получение syscall.SIGTERM даст процессу возможность очиститься перед завершением.
Plan 9
В Plan 9 сигналы имеют тип syscall.Note, который представляет собой строку. Вызов Notify с syscall.Note приведет к отправке этого значения по каналу, когда эта строка будет отправлена в качестве заметки.
Функции
func Ignore
func Ignore(sig ...os.Signal)
Ignore заставляет игнорировать указанные сигналы. Если они будут получены программой, ничего не произойдет. Ignore отменяет действие всех предыдущих вызовов Notify для указанных сигналов. Если сигналы не указаны, все входящие сигналы будут игнорироваться.
func Ignored
func Ignored(sig os.Signal) bool
Ignored сообщает, игнорируется ли sig в данный момент.
func Notify
func Notify(c chan<- os.Signal, sig ...os.Signal)
Notify заставляет пакет signal ретранслировать входящие сигналы в c. Если сигналы не предоставлены, все входящие сигналы будут ретранслированы в c. В противном случае будут ретранслированы только предоставленные сигналы.
Пакет signal не будет блокировать отправку в c: вызывающий должен убедиться, что c имеет достаточное буферное пространство, чтобы справиться с ожидаемой скоростью сигналов. Для канала, используемого для уведомления только об одном значении сигнала, достаточно буфера размером 1.
Разрешается вызывать Notify несколько раз с одним и тем же каналом: каждый вызов расширяет набор сигналов, отправляемых на этот канал. Единственный способ удалить сигналы из набора — вызвать Stop.
Разрешается вызывать Notify несколько раз с разными каналами и одними и теми же сигналами: каждый канал получает копии входящих сигналов независимо.
Пример
package main
import (
"fmt"
"os"
"os/signal"
)
func main() {
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
// Block until a signal is received.
s := <-c
fmt.Println("Got signal:", s)
}
Пример AllSignals
package main
import (
"fmt"
"os"
"os/signal"
)
func main() {
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
c := make(chan os.Signal, 1)
// Passing no signals to Notify means that
// all signals will be sent to the channel.
signal.Notify(c)
// Block until any signal is received.
s := <-c
fmt.Println("Got signal:", s)
}
func NotifyContext
func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
NotifyContext возвращает копию родительского контекста, который помечается как выполненный (его канал Done закрывается), когда поступает один из перечисленных сигналов, когда вызывается возвращаемая функция stop или когда канал Done родительского контекста закрывается, в зависимости от того, что произойдет раньше.
Функция stop отменяет регистрацию поведения сигнала, что, как и signal.Reset, может восстановить поведение по умолчанию для данного сигнала. Например, поведением по умолчанию для программы Go, получающей os.Interrupt, является выход. Вызов NotifyContext(parent, os.Interrupt) изменит поведение на отмену возвращенного контекста. Будущие прерывания не будут вызывать поведение по умолчанию (выход) до тех пор, пока не будет вызвана возвращенная функция stop.
Функция stop освобождает связанные с ней ресурсы, поэтому код должен вызывать stop, как только операции, выполняемые в этом контексте, завершатся и сигналы больше не нужно будет перенаправлять в контекст.
Пример
//go:build unix
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
)
var neverReady = make(chan struct{}) // never closed
// This example passes a context with a signal to tell a blocking function that
// it should abandon its work after a signal is received.
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
p, err := os.FindProcess(os.Getpid())
if err != nil {
log.Fatal(err)
}
// On a Unix-like system, pressing Ctrl+C on a keyboard sends a
// SIGINT signal to the process of the program in execution.
//
// This example simulates that by sending a SIGINT signal to itself.
if err := p.Signal(os.Interrupt); err != nil {
log.Fatal(err)
}
select {
case <-neverReady:
fmt.Println("ready")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context canceled"
stop() // stop receiving signal notifications as soon as possible.
}
}
func Reset
func Reset(sig ...os.Signal)
Reset отменяет эффект всех предыдущих вызовов Notify для предоставленных сигналов. Если сигналы не указаны, все обработчики сигналов будут сброшены.
func Stop
func Stop(c chan<- os.Signal)
Stop заставляет пакет signal прекратить ретрансляцию входящих сигналов в c. Он отменяет эффект всех предыдущих вызовов Notify с использованием c. Когда Stop возвращается, гарантируется, что c больше не будет получать сигналы.
4.12 - Описание пакета strconv
Пакет strconv реализует преобразования в строковые представления основных типов данных и обратно.
Числовые преобразования
Наиболее распространенными числовыми преобразованиями являются Atoi (строка в int) и Itoa (int в String).
i, err := strconv.Atoi(«-42»)
s := strconv.Itoa(-42)
Они предполагают десятичную систему счисления и тип Go int.
ParseBool, ParseFloat, ParseInt и ParseUint преобразуют строки в значения:
b, err := strconv.ParseBool(«true»)
f, err := strconv.ParseFloat(«3.1415», 64)
i, err := strconv.ParseInt(«-42», 10, 64)
u, err := strconv.ParseUint(«42», 10, 64)
Функции преобразования возвращают самый широкий тип (float64, int64 и uint64), но если аргумент size указывает более узкую ширину, результат может быть преобразован в этот более узкий тип без потери данных:
s := «2147483647» // самый большой int32
i64, err := strconv.ParseInt(s, 10, 32)
...
i := int32(i64)
FormatBool, FormatFloat, FormatInt и FormatUint преобразуют значения в строки:
s := strconv.FormatBool(true)
s := strconv.FormatFloat(3.1415, „E“, -1, 64)
s := strconv.FormatInt(-42, 16)
s := strconv.FormatUint(42, 16)
AppendBool, AppendFloat, AppendInt и AppendUint аналогичны, но добавляют отформатированное значение в целевой срез.
Преобразование строк
Quote и QuoteToASCII преобразуют строки в строчные литералы Go в кавычках. Последний гарантирует, что результатом будет строка ASCII, экранируя любой не-ASCII Unicode с помощью \u:
q := strconv.Quote(«Hello, 世界»)
q := strconv.QuoteToASCII(«Hello, 世界»)
QuoteRune и QuoteRuneToASCII похожи, но принимают руны и возвращают зацикленные литералы Go.
Unquote и UnquoteChar разъединяют литералы строк и рун Go.
Константы
IntSize - это размер в битах значения int или uint.
Переменные
var ErrRange = errors.New("значение вне диапазона")
ErrRange указывает, что значение выходит за пределы диапазона для целевого типа.
var ErrSyntax = errors.New("invalid syntax")
ErrSyntax указывает, что значение не имеет правильного синтаксиса для целевого типа.
4.12.1 - Описание функций пакета strconv
Описание функция из пакета strconv
func AppendBool
func AppendBool(dst []byte, b bool) []byte
AppendBool добавляет «true» или «false» в зависимости от значения b к dst и возвращает расширенный буфер.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
b := []byte("bool:")
b = strconv.AppendBool(b, true)
fmt.Println(string(b))
}
func AppendFloat
func AppendFloat(dst []byte, f float64, fmt byte, prec, bitSize int) []byte
AppendFloat добавляет строковую форму числа с плавающей запятой f, сгенерированную FormatFloat, к dst и возвращает расширенный буфер.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
b32 := []byte("float32:")
b32 = strconv.AppendFloat(b32, 3.1415926535, 'E', -1, 32)
fmt.Println(string(b32))
b64 := []byte("float64:")
b64 = strconv.AppendFloat(b64, 3.1415926535, 'E', -1, 64)
fmt.Println(string(b64))
}
func AppendInt
func AppendInt(dst []byte, i int64, base int) []byte
AppendInt добавляет строковую форму целого числа i, сгенерированную FormatInt, к dst и возвращает расширенный буфер.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
b10 := []byte("int (base 10):")
b10 = strconv.AppendInt(b10, -42, 10)
fmt.Println(string(b10))
b16 := []byte("int (base 16):")
b16 = strconv.AppendInt(b16, -42, 16)
fmt.Println(string(b16))
}
func AppendQuote
func AppendQuote(dst []byte, s string) []byte
AppendQuote добавляет к dst строковый литерал Go в двойных кавычках, представляющий s, сгенерированный Quote, и возвращает расширенный буфер.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
b := []byte("quote:")
b = strconv.AppendQuote(b, `"Fran & Freddie's Diner"`)
fmt.Println(string(b))
}
Output:
quote:"\"Fran & Freddie's Diner\""
func AppendQuoteRune
func AppendQuoteRune(dst []byte, r rune) []byte
AppendQuoteRune добавляет к dst однострочный литерал Go, представляющий руну, сгенерированный QuoteRune, и возвращает расширенный буфер.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
b := []byte("rune:")
b = strconv.AppendQuoteRune(b, '☺')
fmt.Println(string(b))
}
func AppendQuoteRuneToASCII
func AppendQuoteRuneToASCII(dst []byte, r rune) []byte
AppendQuoteRuneToASCII добавляет в dst однострочный литерал символа Go, представляющий руну, сгенерированный QuoteRuneToASCII, и возвращает расширенный буфер.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
b := []byte("rune (ascii):")
b = strconv.AppendQuoteRuneToASCII(b, '☺')
fmt.Println(string(b))
}
Output:
rune (ascii):'\u263a'
func AppendQuoteRuneToGraphic
func AppendQuoteRuneToGraphic(dst []byte, r rune) []byte
AppendQuoteRuneToGraphic добавляет к dst однострочный литерал символа Go, представляющий руну, сгенерированный QuoteRuneToGraphic, и возвращает расширенный буфер.
func AppendQuoteToASCII
func AppendQuoteToASCII(dst []byte, s string) []byte
AppendQuoteToASCII добавляет к dst строковый литерал Go в двойных кавычках, представляющий s, сгенерированный QuoteToASCII, и возвращает расширенный буфер.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
b := []byte("quote (ascii):")
b = strconv.AppendQuoteToASCII(b, `"Fran & Freddie's Diner"`)
fmt.Println(string(b))
}
Output:
quote (ascii):"\"Fran & Freddie's Diner\""
func AppendQuoteToGraphic
func AppendQuoteToGraphic(dst []byte, s string) []byte
AppendQuoteToGraphic добавляет к dst строковый литерал Go в двойных кавычках, представляющий s, сгенерированный QuoteToGraphic, и возвращает расширенный буфер.
func AppendUint
func AppendUint(dst []byte, i uint64, base int) []byte
AppendUint добавляет строковую форму целого числа без знака i, сгенерированную FormatUint, к dst и возвращает расширенный буфер.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
b10 := []byte("uint (base 10):")
b10 = strconv.AppendUint(b10, 42, 10)
fmt.Println(string(b10))
b16 := []byte("uint (base 16):")
b16 = strconv.AppendUint(b16, 42, 16)
fmt.Println(string(b16))
}
Output:
uint (base 10):42
uint (base 16):2a
## func Atoi
```go
func Atoi(s string) (int, error)
```
Atoi эквивалентно ParseInt(s, 10, 0), преобразованному в тип int.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v := "10"
if s, err := strconv.Atoi(v); err == nil {
fmt.Printf("%T, %v", s, s)
}
}
func CanBackquote
func CanBackquote(s string) bool
CanBackquote сообщает, может ли строка s быть представлена без изменений в виде однострочной строки с обратными кавычками, не содержащей контрольных символов, кроме табуляции.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
fmt.Println(strconv.CanBackquote("Fran & Freddie's Diner ☺"))
fmt.Println(strconv.CanBackquote("`can't backquote this`"))
}
func FormatBool(b bool) string
FormatBool возвращает «true» или «false» в зависимости от значения b.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v := true
s := strconv.FormatBool(v)
fmt.Printf("%T, %v\n", s, s)
}
func FormatComplex(c complex128, fmt byte, prec, bitSize int) string
FormatComplex преобразует комплексное число c в строку вида (a+bi), где a и b — действительная и мнимая части, отформатированные в соответствии с форматом fmt и точностью prec.
Формат fmt и точность prec имеют то же значение, что и в FormatFloat. Он округляет результат, предполагая, что исходное значение было получено из комплексного значения bitSize бит, которое должно быть 64 для complex64 и 128 для complex128.
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
FormatFloat преобразует число с плавающей запятой f в строку в соответствии с форматом fmt и точностью prec. Оно округляет результат, предполагая, что исходное значение было получено из значения с плавающей запятой bitSize бит (32 для float32, 64 для float64).
Формат fmt может быть одним из следующих
- “b” (-ddddp±ddd, двоичный экспонента),
- “e” (-d.dddde±dd, десятичный экспонент),
- “E” (-d.ddddE±dd, десятичный экспонент),
- “f” (-ddd.dddd, без экспонента),
- “g” („e“ для больших экспонентов, „f“ в остальных случаях),
- “G” („E“ для больших экспонентов, „f“ в остальных случаях),
- “x” (-0xd.ddddp±ddd, шестнадцатеричная дробь и двоичный показатель), или
- “X” (-0Xd.ddddP±ddd, шестнадцатеричная дробь и двоичный показатель).
Точность prec контролирует количество цифр (исключая экспоненту), выводимых форматами „e“, „E“, „f“, „g“, „G“, „x“ и „X“. Для „e“, „E“, „f“, „x“ и „X“ это количество цифр после десятичной запятой. Для „g“ и „G“ это максимальное количество значимых цифр (конечные нули удаляются). Специальная точность -1 использует минимальное количество цифр, необходимое для того, чтобы ParseFloat вернул точное значение f. Экспонента записывается в виде десятичного целого числа; для всех форматов, кроме „b“, она будет состоять как минимум из двух цифр.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v := 3.1415926535
s32 := strconv.FormatFloat(v, 'E', -1, 32)
fmt.Printf("%T, %v\n", s32, s32)
s64 := strconv.FormatFloat(v, 'E', -1, 64)
fmt.Printf("%T, %v\n", s64, s64)
// fmt.Println uses these arguments to print floats
fmt64 := strconv.FormatFloat(v, 'g', -1, 64)
fmt.Printf("%T, %v\n", fmt64, fmt64)
}
Output:
string, 3.1415927E+00
string, 3.1415926535E+00
string, 3.1415926535
func FormatInt(i int64, base int) string
FormatInt возвращает строковое представление i в заданной базе, для 2 <= base <= 36. Результат использует строчные буквы от «a» до «z» для значений цифр >= 10.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v := int64(-42)
s10 := strconv.FormatInt(v, 10)
fmt.Printf("%T, %v\n", s10, s10)
s16 := strconv.FormatInt(v, 16)
fmt.Printf("%T, %v\n", s16, s16)
}
Output:
string, -42
string, -2a
func FormatUint(i uint64, base int) string
FormatUint возвращает строковое представление i в заданной базе, для 2 <= base <= 36. В результате для значений цифр >= 10 используются строчные буквы от „a“ до „z“.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v := uint64(42)
s10 := strconv.FormatUint(v, 10)
fmt.Printf("%T, %v\n", s10, s10)
s16 := strconv.FormatUint(v, 16)
fmt.Printf("%T, %v\n", s16, s16)
}
Output:
string, 42
string, 2a
func IsGraphic
func IsGraphic(r rune) bool
IsGraphic сообщает, определена ли руна как графический символ в Unicode. К таким символам относятся буквы, знаки, цифры, знаки препинания, символы и пробелы из категорий L, M, N, P, S и Zs.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
shamrock := strconv.IsGraphic('☘')
fmt.Println(shamrock)
a := strconv.IsGraphic('a')
fmt.Println(a)
bel := strconv.IsGraphic('\007')
fmt.Println(bel)
}
func IsPrint
func IsPrint(r rune) bool
IsPrint сообщает, определена ли руна как печатная в Go, с тем же определением, что и unicode.IsPrint: буквы, цифры, знаки препинания, символы и пробел ASCII.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
c := strconv.IsPrint('\u263a')
fmt.Println(c)
bel := strconv.IsPrint('\007')
fmt.Println(bel)
}
func Itoa
Itoa эквивалентна FormatInt(int64(i), 10).
package main
import (
"fmt"
"strconv"
)
func main() {
i := 10
s := strconv.Itoa(i)
fmt.Printf("%T, %v\n", s, s)
}
func ParseBool
func ParseBool(str string) (bool, error)
ParseBool возвращает булево значение, представленное строкой. Принимает 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False. Любое другое значение возвращает ошибку.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v := "true"
if s, err := strconv.ParseBool(v); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
}
func ParseComplex
func ParseComplex(s string, bitSize int) (complex128, error)
ParseComplex преобразует строку s в комплексное число с точностью, указанной в bitSize: 64 для complex64 или 128 для complex128. Когда bitSize=64, результат по-прежнему имеет тип complex128, но его можно преобразовать в complex64 без изменения его значения.
Число, представленное s, должно иметь вид N, Ni или N±Ni, где N обозначает число с плавающей запятой, распознаваемое ParseFloat, а i — мнимую составляющую. Если второе N не имеет знака, между двумя составляющими требуется знак +, как указано ±. Если второе N равно NaN, допускается только знак +. Форма может быть заключена в скобки и не может содержать пробелов. Результирующее комплексное число состоит из двух компонентов, преобразованных ParseFloat.
Ошибки, возвращаемые ParseComplex, имеют конкретный тип *NumError и включают err.Num = s.
Если s не является синтаксически правильным, ParseComplex возвращает err.Err = ErrSyntax.
Если s является синтаксически правильным, но любой из компонентов находится на расстоянии более 1/2 ULP от наибольшего числа с плавающей запятой заданного размера компонента, ParseComplex возвращает err.Err = ErrRange и c = ±Inf для соответствующего компонента.
func ParseFloat
func ParseFloat(s string, bitSize int) (float64, error)
ParseFloat преобразует строку s в число с плавающей запятой с точностью, указанной в bitSize: 32 для float32 или 64 для float64. Когда bitSize=32, результат по-прежнему имеет тип float64, но его можно преобразовать в float32 без изменения его значения.
ParseFloat принимает десятичные и шестнадцатеричные числа с плавающей запятой, как определено синтаксисом Go для литералов с плавающей запятой. Если s имеет правильную форму и близка к действительному числу с плавающей запятой, ParseFloat возвращает ближайшее число с плавающей запятой, округленное с использованием беспристрастного округления IEEE754. (При разборе шестнадцатеричного числа с плавающей запятой округление происходит только в том случае, если в шестнадцатеричном представлении больше битов, чем может поместиться в мантиссе).
Ошибки, которые возвращает ParseFloat, имеют конкретный тип *NumError и включают err.Num = s.
Если s не является синтаксически правильно сформированным, ParseFloat возвращает err.Err = ErrSyntax.
Если s является синтаксически правильным, но находится на расстоянии более 1/2 ULP от наибольшего числа с плавающей запятой заданного размера, ParseFloat возвращает f = ±Inf, err.Err = ErrRange.
ParseFloat распознает строку «NaN» и строки «Inf» и «Infinity» (возможно со знаком) как соответствующие специальные значения с плавающей запятой. При сопоставлении он игнорирует регистр.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v := "3.1415926535"
if s, err := strconv.ParseFloat(v, 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseFloat(v, 64); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseFloat("NaN", 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
// ParseFloat is case insensitive
if s, err := strconv.ParseFloat("nan", 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseFloat("inf", 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseFloat("+Inf", 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseFloat("-Inf", 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseFloat("-0", 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseFloat("+0", 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
}
Output:
float64, 3.1415927410125732
float64, 3.1415926535
float64, NaN
float64, NaN
float64, +Inf
float64, +Inf
float64, -Inf
float64, -0
float64, 0
func ParseInt
func ParseInt(s string, base int, bitSize int) (i int64, err error)
ParseInt интерпретирует строку s в заданной базе (от 0 до 36) и размере бита (от 0 до 64) и возвращает соответствующее значение i.
Строка может начинаться с ведущего знака: «+» или «-».
Если аргумент base равен 0, истинная база подразумевается префиксом строки, следующим за знаком (если он есть): 2 для «0b», 8 для «0» или «0o», 16 для «0x» и 10 в остальных случаях. Кроме того, только для аргумента base 0 допускаются символы подчеркивания, как определено синтаксисом Go для целочисленных литералов.
Аргумент bitSize указывает тип целого числа, в который должен помещаться результат. Размеры битов 0, 8, 16, 32 и 64 соответствуют int, int8, int16, int32 и int64. Если bitSize меньше 0 или больше 64, возвращается ошибка.
Ошибки, которые возвращает ParseInt, имеют конкретный тип *NumError и включают err.Num = s. Если s пустое или содержит недопустимые цифры, err.Err = ErrSyntax, и возвращаемое значение равно 0; если значение, соответствующее s, не может быть представлено целым числом с знаком заданного размера, err.Err = ErrRange, и возвращаемое значение является целым числом максимальной величины с соответствующим bitSize и знаком.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v32 := "-354634382"
if s, err := strconv.ParseInt(v32, 10, 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseInt(v32, 16, 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
v64 := "-3546343826724305832"
if s, err := strconv.ParseInt(v64, 10, 64); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseInt(v64, 16, 64); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
}
Output:
int64, -354634382
int64, -3546343826724305832
func ParseUint
func ParseUint(s string, base int, bitSize int) (uint64, error)
ParseUint похож на ParseInt, но для чисел без знака.
Префикс знака не допускается.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
v := "42"
if s, err := strconv.ParseUint(v, 10, 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
if s, err := strconv.ParseUint(v, 10, 64); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
}
Output:
uint64, 42
uint64, 42
func Quote
func Quote(s string) string
Quote возвращает строковый литерал Go в двойных кавычках, представляющий s. Возвращаемая строка использует экранирующие последовательности Go (\t, \n, \xFF, \u0100) для управляющих символов и непечатаемых символов, как определено в IsPrint.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
// This string literal contains a tab character.
s := strconv.Quote(`"Fran & Freddie's Diner ☺"`)
fmt.Println(s)
}
Output:
"\"Fran & Freddie's Diner\t☺\""
func QuoteRune
func QuoteRune(r rune) string
QuoteRune возвращает строковый литерал Go в одинарных кавычках, представляющий руну. Возвращаемая строка использует экранирующие последовательности Go (\t, \n, \xFF, \u0100) для управляющих символов и непечатаемых символов, как определено в IsPrint. Если r не является действительным кодом Unicode, он интерпретируется как символ замены Unicode U+FFFD.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
s := strconv.QuoteRune('☺')
fmt.Println(s)
}
func QuoteRuneToASCII
func QuoteRuneToASCII(r rune) string
QuoteRuneToASCII возвращает однострочный литерал символа Go, представляющий руну. Возвращаемая строка использует экранирующие последовательности Go (\t, \n, \xFF, \u0100) для не-ASCII символов и непечатаемых символов, как определено в IsPrint. Если r не является действительным кодом Unicode, он интерпретируется как заменяющий символ Unicode U+FFFD.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
s := strconv.QuoteRuneToASCII('☺')
fmt.Println(s)
}
func QuoteRuneToGraphic
func QuoteRuneToGraphic(r rune) string
QuoteRuneToGraphic возвращает однострочный литерал Go, представляющий руну. Если руна не является графическим символом Unicode, как определено IsGraphic, возвращаемая строка будет использовать экранирующие последовательности Go (\t, \n, \xFF, \u0100). Если r не является допустимым кодовым пунктом Unicode, он интерпретируется как символ замены Unicode U+FFFD.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
s := strconv.QuoteRuneToGraphic('☺')
fmt.Println(s)
s = strconv.QuoteRuneToGraphic('\u263a')
fmt.Println(s)
s = strconv.QuoteRuneToGraphic('\u000a')
fmt.Println(s)
s = strconv.QuoteRuneToGraphic(' ') // tab character
fmt.Println(s)
}
Output:
'☺'
'☺'
'\n'
'\t'
func QuoteToASCII
func QuoteToASCII(s string) string
QuoteToASCII возвращает строковый литерал Go в двойных кавычках, представляющий s. Возвращаемая строка использует экранирующие последовательности Go (\t, \n, \xFF, \u0100) для не-ASCII символов и непечатаемых символов, как определено IsPrint.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
// This string literal contains a tab character.
s := strconv.QuoteToASCII(`"Fran & Freddie's Diner ☺"`)
fmt.Println(s)
}
Output:
"\"Fran & Freddie's Diner\t\u263a\""
func QuoteToGraphic
func QuoteToGraphic(s string) string
QuoteToGraphic возвращает строковый литерал Go в двойных кавычках, представляющий s. Возвращаемая строка оставляет графические символы Unicode, как определено IsGraphic, без изменений и использует экранирующие последовательности Go (\t, \n, \xFF, \u0100) для неграфических символов.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
s := strconv.QuoteToGraphic("☺")
fmt.Println(s)
// This string literal contains a tab character.
s = strconv.QuoteToGraphic("This is a \u263a \u000a")
fmt.Println(s)
s = strconv.QuoteToGraphic(`" This is a ☺ \n "`)
fmt.Println(s)
}
Output:
"☺"
"This is a ☺\t\n"
"\" This is a ☺ \\n \""
func QuotedPrefix
func QuotedPrefix(s string) (string, error)
QuotedPrefix возвращает строку в кавычках (как понимает Unquote) в префиксе s. Если s не начинается с действительной строки в кавычках, QuotedPrefix возвращает ошибку.
package main
import (
"fmt"
"strconv"
)
func main() {
s, err := strconv.QuotedPrefix("not a quoted string")
fmt.Printf("%q, %v\n", s, err)
s, err = strconv.QuotedPrefix("\"double-quoted string\" with trailing text")
fmt.Printf("%q, %v\n", s, err)
s, err = strconv.QuotedPrefix("`or backquoted` with more trailing text")
fmt.Printf("%q, %v\n", s, err)
s, err = strconv.QuotedPrefix("'\u263a' is also okay")
fmt.Printf("%q, %v\n", s, err)
}
Output:
"", invalid syntax
"\"double-quoted string\"", <nil>
"`or backquoted`", <nil>
"'☺'", <nil>
func Unquote
func Unquote(s string) (string, error)
Unquote интерпретирует s как строковый литерал Go в одинарных, двойных или обратных кавычках, возвращая значение строки, которое s заключает в кавычки. (Если s заключено в одинарные кавычки, это будет символьный литерал Go; Unquote возвращает соответствующую односимвольную строку. Для пустого символьного литерала Unquote возвращает пустую строку.)
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
s, err := strconv.Unquote("You can't unquote a string without quotes")
fmt.Printf("%q, %v\n", s, err)
s, err = strconv.Unquote("\"The string must be either double-quoted\"")
fmt.Printf("%q, %v\n", s, err)
s, err = strconv.Unquote("`or backquoted.`")
fmt.Printf("%q, %v\n", s, err)
s, err = strconv.Unquote("'\u263a'") // single character only allowed in single quotes
fmt.Printf("%q, %v\n", s, err)
s, err = strconv.Unquote("'\u2639\u2639'")
fmt.Printf("%q, %v\n", s, err)
}
Output:
"", invalid syntax
"The string must be either double-quoted", <nil>
"or backquoted.", <nil>
"☺", <nil>
"", invalid syntax
func UnquoteChar
func UnquoteChar(s string, quote byte) (value rune, multibyte bool, tail string, err error)
UnquoteChar декодирует первый символ или байт в экранированной строке или символьном литерале, представленном строкой s. Он возвращает четыре значения:
value, декодированный код Unicode или значение байта;
multibyte, булево значение, указывающее, требует ли декодированный символ многобайтового представления UTF-8;
tail, остаток строки после символа; и
ошибка, которая будет nil, если символ синтаксически валиден.
Второй аргумент, quote, указывает тип анализируемого литерала и, следовательно, какой экранированный символ кавычки разрешен. Если установлен в одиночную кавычку, он разрешает последовательность \„ и запрещает неэкранированный “. Если установлен в двойную кавычку, он разрешает \« и запрещает неэкранированный ». Если установлен в ноль, он не разрешает ни один из экранированных символов и позволяет обоим символам кавычки появляться неэкранированными.
Пример
package main
import (
"fmt"
"log"
"strconv"
)
func main() {
v, mb, t, err := strconv.UnquoteChar(`\"Fran & Freddie's Diner\"`, '"')
if err != nil {
log.Fatal(err)
}
fmt.Println("value:", string(v))
fmt.Println("multibyte:", mb)
fmt.Println("tail:", t)
}
Output:
value: "
multibyte: false
tail: Fran & Freddie's Diner\"
4.12.2 - Тип NumError пакета strconv
Ошибка NumError фиксирует неудачное преобразование.
type NumError
type NumError struct {
Func string // сбойная функция (ParseBool, ParseInt, ParseUint, ParseFloat, ParseComplex)
Num string // входные данные
Err error // причина сбоя преобразования (например, ErrRange, ErrSyntax и т. д.)
}
Ошибка NumError фиксирует неудачное преобразование.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
str := "Not a number"
if _, err := strconv.ParseFloat(str, 64); err != nil {
e := err.(*strconv.NumError)
fmt.Println("Func:", e.Func)
fmt.Println("Num:", e.Num)
fmt.Println("Err:", e.Err)
fmt.Println(err)
}
}
Output:
Func: ParseFloat
Num: Not a number
Err: invalid syntax
strconv.ParseFloat: parsing "Not a number": invalid syntax
func (*NumError) Error
func (e *NumError) Error() string
func (*NumError) Unwrap
func (e *NumError) Unwrap() error
4.13 - Описание пакета error языка программирования Go
Пакет errors реализует функции для манипуляции ошибками
Функция New создает ошибки, содержащие только текстовое сообщение.
Ошибка e оборачивает другую ошибку, если тип e имеет один из методов:
Unwrap() error
Unwrap() []error
Если e.Unwrap() возвращает не nil ошибку w или срез, содержащий w, то говорится, что e оборачивает w. Возвращение nil ошибки из e.Unwrap() указывает на то, что e не оборачивает никакую ошибку. Недопустимо, чтобы метод Unwrap() возвращал срез, содержащий nil значение ошибки.
Легкий способ создать обернутые ошибки — вызвать fmt.Errorf и применить шаблон %w к аргументу ошибки:
wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)
Последовательное разворачивание ошибки создает дерево. Функции Is и As исследуют дерево ошибки, проверяя сначала саму ошибку, а затем дерево каждого из ее потомков по очереди (префиксный, глубинный обход).
См. https://go.dev/blog/go1.13-errors для более глубокого обсуждения философии оборачивания и когда следует оборачивать.
Функция Is исследует дерево своего первого аргумента в поисках ошибки, совпадающей со вторым. Она сообщает, найдено ли совпадение. Ее следует использовать вместо простых проверок на равенство:
if errors.Is(err, fs.ErrExist)
лучше, чем
потому что первая будет успешной, если err оборачивает io/fs.ErrExist.
Функция As исследует дерево своего первого аргумента в поисках ошибки, которую можно присвоить второму аргументу, который должен быть указателем. Если она успешна, она выполняет присваивание и возвращает true. В противном случае она возвращает false. Форма
var perr *fs.PathError
if errors.As(err, &perr) {
fmt.Println(perr.Path)
}
лучше, чем
if perr, ok := err.(*fs.PathError); ok {
fmt.Println(perr.Path)
}
потому что первая будет успешной, если err оборачивает *io/fs.PathError.
Пример
package main
import (
"fmt"
"time"
)
// MyError is an error implementation that includes a time and message.
type MyError struct {
When time.Time
What string
}
func (e MyError) Error() string {
return fmt.Sprintf("%v: %v", e.When, e.What)
}
func oops() error {
return MyError{
time.Date(1989, 3, 15, 22, 30, 0, 0, time.UTC),
"the file system has gone away",
}
}
func main() {
if err := oops(); err != nil {
fmt.Println(err)
}
}
Output:
1989-03-15 22:30:00 +0000 UTC: the file system has gone away
Переменные
var ErrUnsupported = New("unsupported operation")
ErrUnsupported указывает на то, что запрашиваемая операция не может быть выполнена, потому что она не поддерживается. Например, вызов os.Link при использовании файловой системы, которая не поддерживает жесткие ссылки.
Функции и методы не должны возвращать эту ошибку, а вместо этого должны возвращать ошибку, включающую соответствующий контекст, который удовлетворяет
errors.Is(err, errors.ErrUnsupported)
либо путем прямого оборачивания ErrUnsupported, либо путем реализации метода Is.
Функции и методы должны документировать случаи, в которых будет возвращена ошибка, оборачивающая эту.
Функции
func As
Объяснение As
📌 Пример 1: Простая кастомная ошибка
Допустим, у нас есть своя ошибка с дополнительными полями:
type NetworkError struct {
Code int
Message string
}
// Реализуем метод Error(), чтобы NetworkError соответствовала интерфейсу error
func (e *NetworkError) Error() string {
return fmt.Sprintf("Network error %d: %s", e.Code, e.Message)
}
Теперь создадим ошибку и проверим её тип с помощью As:
func main() {
err := &NetworkError{Code: 404, Message: "Not Found"} // создаём ошибку
var netErr *NetworkError
if errors.As(err, &netErr) { // проверяем тип и извлекаем
fmt.Printf("Ошибка сети! Код: %d, Текст: %s\n", netErr.Code, netErr.Message)
} else {
fmt.Println("Это не ошибка сети")
}
}
Вывод:
Ошибка сети! Код: 404, Текст: Not Found
👉 Что произошло?
errors.As проверила, что err имеет тип *NetworkError.
- Если да — сохранила её в
netErr, и мы можем использовать её поля (Code, Message).
📌 Пример 2: Ошибка обёрнута в другую ошибку
Часто ошибки “заворачивают” в другие ошибки с помощью fmt.Errorf и %w. As умеет “пробираться” через такие обёртки.
func fetchData() error {
return &NetworkError{Code: 500, Message: "Server Error"}
}
func main() {
err := fetchData()
wrappedErr := fmt.Errorf("не удалось получить данные: %w", err) // оборачиваем
var netErr *NetworkError
if errors.As(wrappedErr, &netErr) { // всё равно находит NetworkError внутри!
fmt.Printf("Ошибка сети: %d - %s\n", netErr.Code, netErr.Message)
}
}
Вывод:
Ошибка сети: 500 - Server Error
👉 Почему это полезно?
Даже если ошибка была обёрнута (%w), As “достанет” её исходный тип.
📌 Пример 3: Проверка нескольких типов ошибок
Иногда ошибка может быть одного из нескольких типов. As позволяет это проверить.
type TimeoutError struct {
TimeoutSec int
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("Timeout after %d sec", e.TimeoutSec)
}
func main() {
err := &TimeoutError{TimeoutSec: 30} // допустим, это наша ошибка
// Пробуем разные типы:
var timeoutErr *TimeoutError
var networkErr *NetworkError
if errors.As(err, &timeoutErr) {
fmt.Println("Это TimeoutError:", timeoutErr.TimeoutSec)
}
if errors.As(err, &networkErr) {
fmt.Println("Это NetworkError:", networkErr.Code)
} else {
fmt.Println("Это не NetworkError") // сработает это
}
}
Вывод:
Это TimeoutError: 30
Это не NetworkError
👉 Вывод:
As проверяет по очереди каждый тип. Если ошибка не соответствует типу — просто возвращает false.
📌 Пример 4: Работа со стандартными ошибками
As можно использовать и со встроенными типами ошибок, например, с *os.PathError.
func main() {
_, err := os.Open("non-existent-file.txt") // вернёт PathError
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Ошибка файла: %s (операция: %s)\n", pathErr.Path, pathErr.Op)
}
}
Вывод (примерно):
Ошибка файла: non-existent-file.txt (операция: open)
🎯 Итог: Когда использовать As?
- Если нужно проверить тип ошибки (например,
MyCustomError).
- Если нужно достать поля ошибки (например,
err.Code, err.TimeoutSec).
- Если ошибка может быть обёрнута (
%w), но тебе нужна исходная.
❌ As vs Is
Is → проверяет конкретное значение ошибки (err == io.EOF).
As → проверяет тип ошибки (err — это *MyError?).
func As(err error, target any) bool
As находит первую ошибку в дереве err, которая совпадает с target, и если такая найдена, устанавливает target в значение этой ошибки и возвращает true. В противном случае возвращает false.
Дерево состоит из err самой по себе, за которой следуют ошибки, полученные путем многократного вызова ее метода Unwrap() error или Unwrap() []error. Когда err оборачивает несколько ошибок, As проверяет err, за которой следует глубинный обход ее потомков.
Ошибка совпадает с target, если конкретное значение ошибки может быть присвоено значению, на которое указывает target, или если у ошибки есть метод As(any) bool, такой что As(target) возвращает true. В последнем случае метод As отвечает за установку target.
Тип ошибки может предоставить метод As, чтобы его можно было рассматривать как если бы он был другим типом ошибки.
As вызывает панику, если target не является не nil указателем на тип, реализующий error, или на любой интерфейсный тип.
Пример
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
Output:
Failed at path: non-existing
func Is
Объяснение Is и As
В Go пакет errors предоставляет функции Is и As для работы с ошибками. Они помогают сравнивать ошибки и проверять их тип. Давай разберём их простым языком.
errors.Is – проверка на конкретную ошибку
Что делает?
Проверяет, содержит ли цепочка ошибок (error) конкретную ошибку.
Аналог из жизни:
Представь, что у тебя есть коробка с вложенными коробками (ошибка, которая обёрнута в другие ошибки). Ты ищешь конкретную вещь (например, ключ). errors.Is рекурсивно открывает все коробки и проверяет, есть ли там нужный ключ.
Пример:
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("ошибка: %w", ErrNotFound) // оборачиваем ошибку
if errors.Is(err, ErrNotFound) {
fmt.Println("Ошибка 'not found' найдена!")
}
Вывод:
Ошибка 'not found' найдена!
Здесь errors.Is “распаковала” err и обнаружила внутри ErrNotFound.
errors.As – проверка типа ошибки
Что делает?
Проверяет, можно ли привести ошибку к определённому типу (например, к твоей кастомной структуре-ошибке). Если да – сохраняет её в переменную.
Аналог из жизни:
У тебя есть коробка, в которой может лежать либо книга, либо ручка. Ты говоришь: “Если там книга, положи её на стол”. errors.As проверяет тип и “кладёт” ошибку в переменную, если тип подходит.
Пример:
type MyError struct {
Code int
Msg string
}
err := &MyError{Code: 404, Msg: "Not Found"} // создаём ошибку типа *MyError
var myErr *MyError
if errors.As(err, &myErr) { // передаём указатель на переменную
fmt.Printf("Код: %d, Сообщение: %s\n", myErr.Code, myErr.Msg)
}
Вывод:
Код: 404, Сообщение: Not Found
Здесь errors.As проверила, что err имеет тип *MyError, и сохранила её в myErr.
Разница между Is и As
| Функция |
Для чего? |
Работает с |
Is |
Проверить, что ошибка именно X |
Конкретными ошибками (==) |
As |
Проверить, что ошибка типа X |
Кастомными типами ошибок (структуры) |
Когда использовать?
Is – если нужно проверить, что ошибка равна конкретному значению (например, io.EOF).
As – если нужно проверить, что ошибка имеет определённый тип, и достать её поля.
Обе функции поддерживают обёрнутые ошибки (с %w в fmt.Errorf), поэтому они умеют “заглядывать” внутрь цепочек ошибок. 🚀
func Is(err, target error) bool
Is сообщает, совпадает ли какая-либо ошибка в дереве err с target.
Дерево состоит из err самой по себе, за которой следуют ошибки, полученные путем многократного вызова ее метода Unwrap() error или Unwrap() []error. Когда err оборачивает несколько ошибок, Is проверяет err, за которой следует глубинный обход ее потомков.
Ошибка считается совпадающей с целевой, если она равна этой целевой или если она реализует метод Is(error) bool, такой что Is(target) возвращает true.
Тип ошибки может предоставить метод Is, чтобы его можно было рассматривать как эквивалентный существующей ошибке. Например, если MyError определяет
func (m MyError) Is(target error) bool { return target == fs.ErrExist }
то Is(MyError{}, fs.ErrExist) возвращает true. См. syscall.Errno.Is для примера в стандартной библиотеке. Метод Is должен только поверхностно сравнивать err и цель и не вызывать Unwrap для обоих.
Пример
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")
} else {
fmt.Println(err)
}
}
}
Output:
file does not exist
func Join
func Join(errs ...error) error
Join возвращает ошибку, которая оборачивает переданные ошибки. Любые значения nil ошибки отбрасываются. Join возвращает nil, если все значения в errs равны nil. Ошибка форматируется как конкатенация строк, полученных путем вызова метода Error каждого элемента errs, с новой строкой между каждой строкой.
Не nil ошибка, возвращенная Join, реализует метод Unwrap() []error.
Пример:
package main
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("first error")
err2 := errors.New("second error")
err3 := errors.New("third error")
combinedErr := errors.Join(err1, err2, err3)
fmt.Println(combinedErr)
}
func New
func New(text string) error
New возвращает ошибку, которая форматируется как заданный текст. Каждый вызов New возвращает уникальное значение ошибки, даже если текст идентичен.
Пример:
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("something went wrong")
fmt.Println(err)
}
Пример
package main
import (
"fmt"
)
func main() {
const name, id = "bimmler", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)
if err != nil {
fmt.Print(err)
}
}
Output:
user "bimmler" (id 17) not found
func Unwrap
func Unwrap(err error) error
Unwrap возвращает основную ошибку, обернутую в err, если она существует. Если err не оборачивает никакую ошибку, Unwrap возвращает nil.
Пример
package main
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("error1")
err2 := fmt.Errorf("error2: [%w]", err1)
fmt.Println(err2)
fmt.Println(errors.Unwrap(err2))
}
Output:
error2: [error1]
error1
4.14 - Описание пакета iter языка программирования Go
Пакет iter предоставляет базовые определения и операции, связанные с итераторами по последовательностям.
Итераторы
Итераторы
Итератор — это функция, которая передает последовательные элементы последовательности в функцию обратного вызова, традиционно называемую yield. Функция останавливается либо когда последовательность завершена, либо когда yield возвращает false, указывая на необходимость прекратить итерацию раньше. Этот пакет определяет Seq и Seq2 (произносится как seek — первая слог последовательности) как сокращения для итераторов, которые передают 1 или 2 значения на элемент последовательности в yield:
type (
Seq[V any] func(yield func(V) bool)
Seq2[K, V any] func(yield func(K, V) bool)
)
Seq2 представляет последовательность пар значений, традиционно ключ-значение или индекс-значение.
Yield возвращает true, если итератор должен продолжить с следующим элементом последовательности, и false, если он должен остановиться.
Например, maps.Keys возвращает итератор, который производит последовательность ключей карты m, реализованную следующим образом:
func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K] {
return func(yield func(K) bool) {
for k := range m {
if !yield(k) {
return
}
}
}
}
Дополнительные примеры можно найти в блоге Go: Range Over Function Types.
Функции итераторов чаще всего вызываются в цикле range, как в следующем примере:
func PrintAll[V any](seq iter.Seq[V]) {
for v := range seq {
fmt.Println(v)
}
}
Конвенции именования
Функции и методы итераторов называются в зависимости от последовательности, по которой они проходят:
// All возвращает итератор по всем элементам в s.
func (s *Set[V]) All() iter.Seq[V]
Метод итератора на типе коллекции традиционно называется All, потому что он итерирует последовательность всех значений в коллекции.
Для типа, содержащего несколько возможных последовательностей, имя итератора может указывать, какая последовательность предоставляется:
// Cities возвращает итератор по основным городам страны.
func (c *Country) Cities() iter.Seq[*City]
// Languages возвращает итератор по официальным языкам страны.
func (c *Country) Languages() iter.Seq[string]
Если итератор требует дополнительной конфигурации, конструкторная функция может принимать дополнительные аргументы конфигурации:
// Scan возвращает итератор по парам ключ-значение с min ≤ key ≤ max.
func (m *Map[K, V]) Scan(min, max K) iter.Seq2[K, V]
// Split возвращает итератор по (возможно, пустым) подстрокам s,
// разделенным sep.
func Split(s, sep string) iter.Seq[string]
Когда существует несколько возможных порядков итерации, имя метода может указывать на этот порядок:
// All возвращает итератор по списку от начала к концу.
func (l *List[V]) All() iter.Seq[V]
// Backward возвращает итератор по списку от конца к началу.
func (l *List[V]) Backward() iter.Seq[V]
// Preorder возвращает итератор по всем узлам синтаксического дерева
// под (и включая) указанный корень, в глубину, в порядке preorder,
// посещая родительский узел перед его дочерними узлами.
func Preorder(root Node) iter.Seq[Node]
Одноразовые итераторы
Большинство итераторов предоставляют возможность пройти по всей последовательности: при вызове итератор выполняет любую необходимую настройку для начала последовательности, затем вызывает yield для последовательных элементов последовательности и затем очищает перед возвратом. Повторный вызов итератора снова проходит по последовательности.
Некоторые итераторы нарушают это правило, предоставляя возможность пройти по последовательности только один раз. Эти “одноразовые итераторы” обычно сообщают значения из потока данных, который нельзя перемотать назад для начала. Повторный вызов итератора после ранней остановки может продолжить поток, но повторный вызов после завершения последовательности не вернет никаких значений. Документационные комментарии для функций или методов, возвращающих одноразовые итераторы, должны документировать этот факт:
// Lines возвращает итератор по строкам, прочитанным из r.
// Он возвращает одноразовый итератор.
func (r *Reader) Lines() iter.Seq[string]
Извлечение значений
Функции и методы, которые принимают или возвращают итераторы, должны использовать стандартные типы Seq или Seq2, чтобы обеспечить совместимость с циклами range и другими адаптерами итераторов. Стандартные итераторы можно рассматривать как “push-итераторы”, которые передают значения в функцию yield.
Иногда цикл range не является наиболее естественным способом потребления значений последовательности. В этом случае Pull преобразует стандартный push-итератор в “pull-итератор”, который можно вызвать для извлечения одного значения за раз из последовательности. Pull запускает итератор и возвращает пару функций — next и stop, которые возвращают следующее значение из итератора и останавливают его соответственно.
Например:
// Pairs возвращает итератор по последовательным парам значений из seq.
func Pairs[V any](seq iter.Seq[V]) iter.Seq2[V, V] {
return func(yield func(V, V) bool) {
next, stop := iter.Pull(seq)
defer stop()
for {
v1, ok1 := next()
if !ok1 {
return
}
v2, ok2 := next()
// Если ok2 равно false, v2 должно быть
// нулевым значением; yield одна последняя пара.
if !yield(v1, v2) {
return
}
if !ok2 {
return
}
}
}
}
Если клиенты не потребляют последовательность до конца, они должны вызвать stop, что позволяет функции итератора завершить выполнение и вернуться. Как показано в примере, традиционный способ обеспечить это — использовать defer.
Использование стандартной библиотеки
Некоторые пакеты в стандартной библиотеке предоставляют API на основе итераторов, наиболее заметными из которых являются пакеты maps и slices. Например, maps.Keys возвращает итератор по ключам карты, в то время как slices.Sorted собирает значения итератора в срез, сортирует их и возвращает срез. Чтобы итерировать по отсортированным ключам карты, можно использовать следующий код:
for _, key := range slices.Sorted(maps.Keys(m)) {
...
}
Мутация
Итераторы предоставляют только значения последовательности, но не прямой способ их изменения. Если итератор хочет предоставить механизм для изменения последовательности во время итерации, обычное решение — определить тип позиции с дополнительными операциями и затем предоставить итератор по позициям.
Например, реализация дерева может предоставить:
// Positions возвращает итератор по позициям в последовательности.
func (t *Tree[V]) Positions() iter.Seq[*Pos]
// Pos представляет позицию в последовательности.
// Он действителен только во время вызова yield, в который он передается.
type Pos[V any] struct { ... }
// Value возвращает значение в курсоре.
func (p *Pos[V]) Value() V
// Delete удаляет значение в этой точке итерации.
func (p *Pos[V]) Delete()
// Set изменяет значение v в курсоре.
func (p *Pos[V]) Set(v V)
И затем клиент может удалить boring значения из дерева, используя:
for p := range t.Positions() {
if boring(p.Value()) {
p.Delete()
}
}
Этот подход позволяет итератору предоставлять механизм для изменения последовательности во время итерации, сохраняя при этом чистоту итератора как такового.
Функции
func Pull
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())
Pull преобразует последовательность итератора в стиле “push” seq в итератор в стиле “pull”, доступный через две функции next и stop.
Next возвращает следующее значение в последовательности и булево значение, указывающее, является ли значение действительным. Когда последовательность завершена, next возвращает нулевое значение V и false. Вызов next после достижения конца последовательности или после вызова stop является допустимым. Эти вызовы будут продолжать возвращать нулевое значение V и false.
Stop завершает итерацию. Его нужно вызвать, когда вызывающий не заинтересован в следующих значениях, и next еще не сигнализировал о завершении последовательности (с возвратом false). Вызов stop несколько раз и после того, как next уже вернул false, является допустимым. Обычно вызывающие должны использовать “defer stop()”.
Ошибкой является вызов next или stop из нескольких горутин одновременно.
Если итератор вызывает панику во время вызова next (или stop), то next (или stop) сам вызывает панику с тем же значением.
func Pull2
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())
Pull2 преобразует последовательность итератора в стиле “push” seq в итератор в стиле “pull”, доступный через две функции next и stop.
Next возвращает следующую пару в последовательности и булево значение, указывающее, является ли пара действительной. Когда последовательность завершена, next возвращает пару нулевых значений и false. Вызов next после достижения конца последовательности или после вызова stop является допустимым. Эти вызовы будут продолжать возвращать пару нулевых значений и false.
Stop завершает итерацию. Его нужно вызвать, когда вызывающий не заинтересован в следующих значениях, и next еще не сигнализировал о завершении последовательности (с возвратом false). Вызов stop несколько раз и после того, как next уже вернул false, является допустимым. Обычно вызывающие должны использовать “defer stop()”.
Ошибкой является вызов next или stop из нескольких горутин одновременно.
Если итератор вызывает панику во время вызова next (или stop), то next (или stop) сам вызывает панику с тем же значением.
Типы
type Seq
type Seq[V any] func(yield func(V) bool)
Seq — это итератор по последовательностям отдельных значений. Когда вызывается как seq(yield), seq вызывает yield(v) для каждого значения v в последовательности, останавливаясь раньше, если yield возвращает false. Подробнее см. в документации пакета iter.
type Seq2
type Seq2[K, V any] func(yield func(K, V) bool)
Seq2 — это итератор по последовательностям пар значений, наиболее часто ключ-значение. Когда вызывается как seq(yield), seq вызывает yield(k, v) для каждой пары (k, v) в последовательности, останавливаясь раньше, если yield возвращает false. Подробнее см. в документации пакета iter.
4.15 - Пакет net встроенных функций и типов языка Go
Пакет net предоставляет переносимый интерфейс для сетевого ввода-вывода, включая TCP/IP, UDP, разрешение доменных имен и сокеты доменов Unix.
Пакет обеспечивает доступ к примитивам низкоуровневых сетей, большинству клиентов понадобится только базовый интерфейс, предоставляемый функциями Dial, Listen и Accept и связанными интерфейсами Conn и Listener. Пакет crypto/tls использует те же интерфейсы и похожие функции Dial и Listen.
Функция Dial подключается к серверу:
conn, err := net.Dial("tcp", "golang.org:80")
if err != nil {
// handle error
}
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
// ...
Функция Listen создает серверы:
ln, err := net.Listen("tcp", ":8080")
if err != nil {
// handle error
}
for {
conn, err := ln.Accept()
if err != nil {
// handle error
}
go handleConnection(conn)
}
Разрешение имен (Name Resolution)
Метод разрешения доменных имен (DNS) в пакете net зависит от операционной системы. Функции, такие как Dial, используют разрешение имен косвенно, а функции вроде LookupHost и LookupAddr делают это напрямую.
На Unix-системах
Ресолвер имеет два варианта работы:
- Чистый Go-ресолвер – отправляет DNS-запросы напрямую к серверам, указанным в
/etc/resolv.conf.
- CGO-ресолвер – использует системные вызовы C (
getaddrinfo, getnameinfo).
Чистый Go-ресолвер предпочтительнее, потому что:
- Заблокированный DNS-запрос потребляет только горутину (легковесный поток Go).
- Заблокированный вызов C занимает системный поток ОС (более затратно).
Когда используется CGO-ресолвер?
- На системах, где запрещены прямые DNS-запросы (например, macOS).
- Если заданы переменные окружения:
LOCALDOMAIN (даже пустая),
RES_OPTIONS,
HOSTALIASES (не пустые),
ASR_CONFIG (только OpenBSD).
- Если
/etc/resolv.conf или /etc/nsswitch.conf требуют функциональности, не реализованной в Go.
Лимит на CGO-запросы
Во всех системах (кроме Plan 9) действует ограничение в 500 одновременных CGO-запросов, чтобы избежать исчерпания системных потоков.
Управление выбором ресолвера
Можно переопределить выбор ресолвера через переменную окружения GODEBUG:
export GODEBUG=netdns=go # принудительно использовать чистый Go-ресолвер
export GODEBUG=netdns=cgo # принудительно использовать нативный (CGO) ресолвер
Также можно задать поведение при сборке через теги:
netgo – только Go-ресолвер (CGO полностью отключен).
netcgo – оба ресолвера, но CGO имеет приоритет (можно переопределить через GODEBUG=netdns=go).
Отладочная информация
Если установить GODEBUG=netdns=1, ресолвер будет выводить отладочную информацию. Можно комбинировать:
export GODEBUG=netdns=go+1 # принудительно Go + отладка
EDNS0 и проблемы с роутерами
Go-ресолвер по умолчанию отправляет EDNS0-заголовок, чтобы запрашивать большие DNS-пакеты. Это может вызывать ошибки на некоторых модемах/роутерах. Отключить:
export GODEBUG=netedns0=0
Особенности на разных ОС
- macOS:
- Если Go-код собран с
-buildmode=c-archive, для линковки в C-программу требуется флаг -lresolv.
- Plan 9:
- Ресолвер всегда использует
/net/cs и /net/dns.
- Windows (Go 1.18.x и старше):
- Всегда используются C-функции (
GetAddrInfo, DnsQuery).
Ключевые термины
- Ресолвер (Resolver) – механизм преобразования доменных имен в IP-адреса.
- CGO – вызовы C-кода из Go.
- EDNS0 – расширение DNS, позволяющее увеличить размер пакета.
- GODEBUG – переменная окружения для настройки поведения Go-рантайма.
4.15.1 - Функции пакета net языка Go
Функции пакета net
func JoinHostPort
func JoinHostPort(host, port string) string
JoinHostPort объединяет хост и порт в сетевой адрес вида «хост:порт». Если хост содержит двоеточие, как в буквенных IPv6-адресах, то JoinHostPort возвращает «[хост]:порт».
Описание параметров хоста и порта см. в func Dial.
func LookupAddr
func LookupAddr(addr string) (names []string, err error)
LookupAddr выполняет обратный поиск по заданному адресу, возвращая список имен, сопоставленных с этим адресом.
Возвращаемые имена проверяются на правильность форматирования доменных имен в формате представления. Если в ответе содержатся недопустимые имена, эти записи отфильтровываются, и вместе с оставшимися результатами, если таковые имеются, возвращается ошибка.
При использовании хост-резольвера библиотеки C будет возвращено не более одного результата. Чтобы обойти хост-резольвер, используйте собственный резольвер.
LookupAddr внутренне использует context.Background; чтобы указать контекст, используйте Resolver.LookupAddr.
func LookupCNAME
func LookupCNAME(host string) (cname string, err error)
LookupCNAME возвращает каноническое имя для заданного хоста. Те, кому не важно каноническое имя, могут вызвать LookupHost или LookupIP напрямую; оба они позаботятся о разрешении канонического имени в процессе поиска.
Каноническое имя - это конечное имя после следования нуля или более записей CNAME. LookupCNAME не возвращает ошибку, если хост не содержит записей DNS «CNAME», при условии, что хост разрешается в адресные записи.
Возвращаемое каноническое имя проверяется на то, что оно является правильно отформатированным доменным именем формата представления.
LookupCNAME внутренне использует context.Background; чтобы указать контекст, используйте Resolver.LookupCNAME.
func LookupHost
func LookupHost(host string) (addrs []string, err error)
LookupHost ищет заданный хост с помощью локального резольвера. Он возвращает фрагмент адресов этого хоста.
Внутри LookupHost используется context.Background; чтобы указать контекст, используйте Resolver.LookupHost.
func LookupPort
func LookupPort(network, service string) (port int, err error)
LookupPort ищет порт для заданной сети и сервиса.
Внутри LookupPort используется context.Background; чтобы указать контекст, используйте Resolver.LookupPort.
func LookupTXT
func LookupTXT(name string) ([]string, error)
LookupTXT возвращает записи DNS TXT для заданного доменного имени.
Если DNS TXT-запись содержит несколько строк, они объединяются в одну строку.
Внутри LookupTXT используется context.Background; чтобы указать контекст, используйте Resolver.LookupTXT.
func ParseCIDR
func ParseCIDR(s string) (IP, *IPNet, error)
ParseCIDR анализирует s как IP-адрес в нотации CIDR и длину префикса, например «192.0.2.0/24» или «2001:db8::/32», как определено в RFC 4632 и RFC 4291.
Он возвращает IP-адрес и сеть, подразумеваемую IP-адресом и длиной префикса. Например, ParseCIDR(«192.0.2.1/24») возвращает IP-адрес 192.0.2.1 и сеть 192.0.2.0/24.
Пример
package main
import (
"fmt"
"log"
"net"
)
func main() {
ipv4Addr, ipv4Net, err := net.ParseCIDR("192.0.2.1/24")
if err != nil {
log.Fatal(err)
}
fmt.Println(ipv4Addr)
fmt.Println(ipv4Net)
ipv6Addr, ipv6Net, err := net.ParseCIDR("2001:db8:a0b:12f0::1/32")
if err != nil {
log.Fatal(err)
}
fmt.Println(ipv6Addr)
fmt.Println(ipv6Net)
}
Output:
192.0.2.1
192.0.2.0/24
2001:db8:a0b:12f0::1
2001:db8::/32
func Pipe ¶
func Pipe() (Conn, Conn)
Pipe создает синхронное, в памяти, полнодуплексное сетевое соединение; оба конца реализуют интерфейс Conn. Чтение на одном конце совпадает с записью на другом, копируя данные непосредственно между ними; внутренней буферизации нет.
Объяснение Pipe
Функция Pipe из пакета net в языке Go позволяет создать синхронный in-memory канал между двумя сетевыми соединениями. Она возвращает два Conn (интерфейсы соединения), где запись в одно соединение сразу же становится доступной для чтения из другого, и наоборот.
Практическое значение net.Pipe()
Эта функция полезна в тестировании и имитации сетевых взаимодействий без реального использования сети (TCP/IP, Unix-сокетов и т. д.).
Примеры использования
1. Тестирование сетевого клиента и сервера без реального соединения
Допустим, у вас есть клиент и сервер, которые обмениваются данными по сети. Вместо запуска реального сервера в тестах можно использовать net.Pipe().
package main
import (
"io"
"net"
"testing"
)
// Серверная логика (обрабатывает соединение)
func handleConn(conn net.Conn) {
defer conn.Close()
_, _ = conn.Write([]byte("Hello, client!"))
}
// Клиентская логика (читает данные)
func readFromServer(conn net.Conn) string {
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
return string(buf[:n])
}
func TestClientServer(t *testing.T) {
// Создаём "виртуальное" соединение
clientConn, serverConn := net.Pipe()
// Запускаем сервер в горутине
go handleConn(serverConn)
// Клиент читает ответ
response := readFromServer(clientConn)
if response != "Hello, client!" {
t.Errorf("Expected 'Hello, client!', got '%s'", response)
} else {
t.Log("Test passed!")
}
}
Что происходит:
net.Pipe() создаёт два конца соединения.
- Сервер пишет в
serverConn, клиент читает из clientConn.
- Всё происходит в памяти, без реального сетевого взаимодействия.
2. Мокирование сетевых вызовов в unit-тестах
Если ваш код зависит от внешнего API или базы данных, можно подменить реальное соединение моком через net.Pipe().
func TestDatabaseClient(t *testing.T) {
clientConn, mockServerConn := net.Pipe()
// Эмуляция сервера БД
go func() {
defer mockServerConn.Close()
request := make([]byte, 1024)
n, _ := mockServerConn.Read(request)
if string(request[:n]) == "GET user:123" {
_, _ = mockServerConn.Write([]byte(`{"id": "123", "name": "Alice"}`))
}
}()
// Клиентский код
_, _ = clientConn.Write([]byte("GET user:123"))
response := make([]byte, 1024)
n, _ := clientConn.Read(response)
expected := `{"id": "123", "name": "Alice"}`
if string(response[:n]) != expected {
t.Errorf("Expected '%s', got '%s'", expected, string(response[:n]))
}
}
Что происходит:
- Клиент отправляет запрос в
clientConn.
- “Сервер” (горутина) читает запрос и возвращает моковый ответ.
- Тест проверяет, что клиент корректно обрабатывает ответ.
Преимущества net.Pipe()
✅ Не требует реального сетевого соединения – работает в памяти.
✅ Синхронная работа – запись блокируется, пока кто-то не прочитает данные.
✅ Удобно для тестирования – не нужно поднимать TCP-сервер или использовать mock-библиотеки.
Ограничения
❌ Только для локального использования – нельзя заменить реальный сетевой обмен в продакшене.
❌ Блокирующие операции – если одна сторона не читает данные, вторая заблокируется при записи.
Вывод
net.Pipe() – это мощный инструмент для тестирования сетевого кода в Go без реальных соединений. Он особенно полезен в unit-тестах, где важна изоляция и скорость выполнения.
func SplitHostPort
func SplitHostPort(hostport string) (host, port string, err error)
SplitHostPort разделяет сетевой адрес вида «host:port», «host%zone:port», «[host]:port» или «[host%zone]:port» на host или host%zone и port.
Буквальный IPv6-адрес в hostport должен быть заключен в квадратные скобки, как в «[::1]:80», «[::1%lo0]:80».
Описание параметра hostport, а также результатов хоста и порта см. в разделе func Dial.
4.15.2 - Типы и методы пакета net языка Go
Типы и методы пакета net
type Addr
type Addr интерфейс {
Network() string // имя сети (например, "tcp", "udp")
String() string // строковая форма адреса (например, "192.0.2.1:25", "[2001:db8::1]:80")
}
Addr представляет адрес конечной точки сети.
Два метода [Addr.Network] и [Addr.String] условно возвращают строки, которые могут быть переданы в качестве аргументов в Dial, но точная форма и значение строк зависят от реализации.
func InterfaceAddrs
func InterfaceAddrs() ([]Addr, error)
InterfaceAddrs возвращает список адресов одноадресных интерфейсов системы.
Возвращаемый список не идентифицирует связанный с ним интерфейс; для получения более подробной информации используйте Interfaces и Interface.Addrs.
type AddrError
type AddrError struct {
строка Err
Addr string
}
func (*AddrError) Error
func (e *AddrError) Error() string
func (*AddrError) Temporary
func (e *AddrError) Temporary() bool
func (*AddrError) Timeout
func (e *AddrError) Timeout() bool
type Buffers
Buffers содержит ноль или более байтов для записи.
На некоторых машинах для определенных типов соединений это оптимизируется в специфическую для ОС операцию пакетной записи (например, “writev”).
func (*Buffers) Read
func (v *Буферы) Read(p []byte) (n int, err error)
Чтение из буферов.
Read реализует io.Reader для буферов.
Read изменяет фрагмент v, а также v[i] для 0 <= i < len(v), но не изменяет v[i][j] для любых i, j.
func (*Buffers) WriteTo
func (v *Буферы) WriteTo(w io.Writer) (n int64, err error)
WriteTo записывает содержимое буферов в w.
WriteTo реализует io.WriterTo для буферов.
WriteTo изменяет фрагмент v, а также v[i] для 0 <= i < len(v), но не изменяет v[i][j] для любых i, j.
type Conn ¶
type Conn interface {
// Read считывает данные из соединения.
// Чтение можно сделать тайм-аутом и вернуть ошибку по истечении фиксированного
// времени; см. SetDeadline и SetReadDeadline.
Read(b []byte) (n int, err error)
// Write записывает данные в соединение.
// Запись может быть сделана так, чтобы тайм-аут и возврат ошибки происходили через фиксированное
// времени; см. SetDeadline и SetWriteDeadline.
Write(b []byte) (n int, err error)
// Close закрывает соединение.
// Любые заблокированные операции чтения или записи будут разблокированы и вернут ошибку.
Close() error
// LocalAddr возвращает адрес локальной сети, если он известен.
LocalAddr() Addr
// RemoteAddr возвращает адрес удаленной сети, если он известен.
RemoteAddr() Addr
// SetDeadline устанавливает крайние сроки чтения и записи, связанные
// с данным соединением. Это эквивалентно вызову обеих функций.
// SetReadDeadline и SetWriteDeadline.
//
// Крайний срок - это абсолютное время, по истечении которого операции ввода-вывода
// прекращаются, а не блокируются. Крайний срок применяется ко всем будущим
// и ожидающим операциям ввода-вывода, а не только к непосредственно следующему вызову
// чтения или записи. После превышения крайнего срока
// соединение можно обновить, установив крайний срок в будущем.
//
// Если крайний срок превышен, вызов Read или Write или других
// методы ввода/вывода вернут ошибку, обернутую в os.ErrDeadlineExceeded.
// Это можно проверить с помощью errors.Is(err, os.ErrDeadlineExceeded).
// Метод Timeout ошибки вернет true, но обратите внимание, что существуют
// есть и другие возможные ошибки, для которых метод Timeout
// вернет true, даже если срок не был превышен.
//
// Таймаут простоя может быть реализован путем многократного продления
// крайнего срока после успешных вызовов чтения или записи.
//
// Нулевое значение t означает, что операции ввода-вывода не будут выполняться по тайм-ауту.
SetDeadline(t time.Time) error
// SetReadDeadline устанавливает крайний срок для будущих вызовов Read
// и любого заблокированного в данный момент вызова Read.
// Нулевое значение t означает, что операции Read не будут завершаться.
SetReadDeadline(t time.Time) error
// SetWriteDeadline устанавливает крайний срок для будущих вызовов записи
// и любого заблокированного в данный момент вызова Write.
// Даже если запись завершится, она может вернуть n > 0, указывая на то, что
// часть данных была успешно записана.
// Нулевое значение t означает, что Write не будет завершаться.
SetWriteDeadline(t time.Time) error
}
Conn - это общее потоково-ориентированное сетевое соединение.
Несколько горутинов могут вызывать методы на Conn одновременно.
func Dial
func Dial(network, address string) (Conn, error)
Dial подключается к адресу в указанной сети.
Известные сети: “tcp”, “tcp4” (только для IPv4), “tcp6” (только для IPv6), “udp”, “udp4” (только для IPv4), “udp6” (только для IPv6), “ip”, “ip4” (только для IPv4), “ip6” (только для IPv6), “unix”, “unixgram” и “unixpacket”.
Для сетей TCP и UDP адрес имеет вид “хост:порт”. Хост должен быть буквальным IP-адресом или именем хоста, которое может быть преобразовано в IP-адрес. Порт должен быть буквенным номером порта или именем службы. Если хост является литеральным IPv6-адресом, он должен быть заключен в квадратные скобки, как в “[2001:db8::1]:80” или “[fe80::1%zone]:80”. Зона определяет область действия буквального IPv6-адреса, как определено в RFC 4007. Функции JoinHostPort и SplitHostPort манипулируют парой хост-порт в таком виде. При использовании TCP, когда хост разрешается в несколько IP-адресов, Dial будет пробовать каждый IP-адрес по порядку, пока один не будет успешным.
Примеры:
Dial("tcp", "golang.org:http")
Dial("tcp", "192.0.2.1:http")
Dial("tcp", "198.51.100.1:80")
Dial("udp", "[2001:db8::1]:domain")
Dial("udp", "[fe80::1%lo0]:53")
Dial("tcp", ":80")
Для IP-сетей сеть должна быть “ip”, “ip4” или “ip6”, за которой следует двоеточие и литеральный номер протокола или имя протокола, а адрес имеет вид “host”. Хост должен быть буквальным IP-адресом или буквальным IPv6-адресом с зоной. От каждой операционной системы зависит, как она поведет себя с номером протокола, не являющимся общеизвестным, например “0” или “255”.
Примеры:
Dial("ip4:1", "192.0.2.1")
Dial("ip6:ipv6-icmp", "2001:db8::1")
Dial("ip6:58", "fe80::1%lo0")
Для сетей TCP, UDP и IP, если хост пуст или является буквальным неопределенным IP-адресом, как в “:80”, “0.0.0.0:80” или “[::]:80” для TCP и UDP, “”, “0.0.0.0” или “::” для IP, предполагается локальная система.
Для сетей Unix адрес должен быть путем к файловой системе.
func DialTimeout
func DialTimeout(network, address string, timeout time.Duration) (Conn, error)
DialTimeout действует как Dial, но берет таймаут.
Таймаут включает разрешение имен, если это необходимо. Если используется TCP и хост в параметре адреса разрешается в несколько IP-адресов, таймаут распределяется на каждый последовательный набор, так что каждому дается соответствующая доля времени для соединения.
Описание параметров сети и адреса см. в func Dial.
func FileConn
func FileConn(f *os.File) (c Conn, err error)
FileConn возвращает копию сетевого соединения, соответствующего открытому файлу f. Ответственность за закрытие f по завершении работы лежит на вызывающей стороне. Закрытие c не влияет на f, а закрытие f не влияет на c.
type DNSConfigError
type DNSConfigError struct {
Err error
}
DNSConfigError представляет ошибку чтения конфигурации DNS машины. (Больше не используется; сохранено для совместимости).
func (*DNSConfigError) Error
func (e *DNSConfigError) Error() string
func (*DNSConfigError) Temporary
func (e *DNSConfigError) Temporary() bool
func (*DNSConfigError) Timeout
func (e *DNSConfigError) Timeout() bool
func (*DNSConfigError) Unwrap
func (e *DNSConfigError) Unwrap() error
type DNSError
type DNSError struct {
UnwrapErr error // ошибка, возвращаемая методом [DNSError.Unwrap], может быть nil
Err string // описание ошибки
Name string // искомое имя
Server string // используемый сервер
IsTimeout bool // если true, то таймаут истек; не все таймауты устанавливают это значение
IsTemporary bool // если true, то ошибка временная; не все ошибки устанавливают это
// IsNotFound устанавливается в true, если запрашиваемое имя не
// не содержит записей запрашиваемого типа (данные не найдены),
// или само имя не найдено (NXDOMAIN).
IsNotFound bool
}
DNSError представляет ошибку поиска DNS.
func (*DNSError) Error
func (e *DNSError) Error() string
func (*DNSError) Temporary
func (e *DNSError) Temporary() bool
Temporary сообщает, является ли ошибка DNS временной. Это не всегда известно; поиск DNS может завершиться неудачей из-за временной ошибки и вернуть DNSError, для которого Temporary возвращает false.
func (*DNSError) Timeout
func (e *DNSError) Timeout() bool
Таймаут сообщает о том, что поиск DNS завершился по таймеру. Это не всегда известно; поиск DNS может завершиться неудачей из-за таймаута и вернуть DNSError, для которого Timeout возвращает false.
func (*DNSError) Unwrap
func (e *DNSError) Unwrap() error
Unwrap возвращает e.UnwrapErr.
type Dialer
type Dialer struct {
// Timeout - это максимальное количество времени, в течение которого набор будет ждать
// завершения соединения. Если также задано значение Deadline, то соединение может завершиться
// раньше.
//
// По умолчанию таймаут отсутствует.
//
// При использовании TCP и наборе имени хоста с несколькими IP
// адресов, тайм-аут может быть разделен между ними.
//
// С таймаутом или без него операционная система может установить
// свой собственный более ранний тайм-аут. Например, тайм-ауты TCP
// часто составляет около 3 минут.
Timeout time.Duration
// Крайний срок - это абсолютный момент времени, после которого набор номера
// завершится неудачей. Если установлен таймаут, то отказ может произойти раньше.
// Ноль означает отсутствие крайнего срока, или зависит от операционной системы.
// как в случае с параметром Timeout.
Deadline time.Time
// LocalAddr - локальный адрес, который следует использовать при наборе
// адреса. Адрес должен быть совместимого типа для
// набираемой сети.
// Если nil, локальный адрес выбирается автоматически.
LocalAddr Addr
// В DualStack ранее была включена поддержка RFC 6555 Fast Fallback
// поддержка, также известная как "Happy Eyeballs", при которой IPv4
// пробует вскоре, если IPv6 оказывается неправильно сконфигурированным и
// зависает.
//
// Утративший силу: Fast Fallback включен по умолчанию. Чтобы
// отключить, установите FallbackDelay в отрицательное значение.
DualStack bool
// FallbackDelay задает время ожидания перед тем, как
// порождения соединения RFC 6555 Fast Fallback. То есть это
// это время ожидания успеха IPv6, прежде чем
// предположить, что IPv6 неправильно сконфигурирован, и вернуться к
// IPv4.
//
// Если значение равно нулю, то по умолчанию используется задержка 300 мс.
// Отрицательное значение отключает поддержку быстрого отката.
FallbackDelay time.Duration
// KeepAlive задает интервал между попытками сохранить активное сетевое соединение.
// зондами активного сетевого соединения.
//
// KeepAlive игнорируется, если KeepAliveConfig.Enable равен true.
//
// Если значение равно нулю, то зонды keep-alive отправляются со значением по умолчанию.
// (в настоящее время 15 секунд), если это поддерживается протоколом и операционной // системой.
// системой. Сетевые протоколы или операционные системы, которые.
// не поддерживают keep-alive, игнорируют это поле.
// Если значение отрицательное, то запросы keep-alive отключаются.
KeepAlive time.Duration
// KeepAliveConfig определяет конфигурацию зонда keep-alive
// для активного сетевого соединения, если оно поддерживается
// протоколом и операционной системой.
//
// Если KeepAliveConfig.Enable равен true, то зонды keep-alive включены.
// Если KeepAliveConfig.Enable ложно и KeepAlive отрицательно,
// зонды keep-alive отключены.
KeepAliveConfig KeepAliveConfig
// Resolver опционально указывает альтернативный резолвер для использования.
Resolver *Resolver
// Cancel - необязательный канал, закрытие которого указывает, что
// набор должен быть отменен. Не все типы циферблатов поддерживают
// отмену.
//
// Утратил актуальность: Вместо этого используйте DialContext.
Cancel <-chan struct{}
// Если Control не nil, то вызывается после создания сетевого
// соединения, но до фактического набора номера.
//
// Параметры сети и адреса, передаваемые в функцию Control, не являются
// обязательно те, которые передаются в Dial. Вызов Dial с сетями TCP
// приведет к вызову функции Control с параметрами "tcp4" или "tcp6",
// UDP-сети станут "udp4" или "udp6", IP-сети - "ip4" или "ip6",
// а другие известные сети передаются как есть.
//
// Контроль игнорируется, если ControlContext не равен nil.
Control func(network, address string, c syscall.RawConn) error
// Если ControlContext не равен nil, то эта функция вызывается после создания сетевого
// соединения, но до фактического набора номера.
//
// Параметры сети и адреса, передаваемые в функцию ControlContext, не являются
// обязательно те, что переданы в Dial. Вызов Dial с сетями TCP
// приведет к вызову функции ControlContext с параметрами "tcp4" или "tcp6",
// UDP-сети станут "udp4" или "udp6", IP-сети - "ip4" или "ip6",
// а другие известные сети передаются как есть.
//
// Если ControlContext не равен nil, Control игнорируется.
ControlContext func(ctx context.Context, network, address string, c syscall.RawConn) error
// содержит отфильтрованные или неэкспонированные поля
}
Dialer содержит опции для соединения с адресом.
Нулевое значение для каждого поля эквивалентно дозвону без этой опции. Поэтому набор номера с нулевым значением Dialer эквивалентен простому вызову функции Dial.
Безопасно вызывать методы Dialer одновременно.
Пример
package main
import (
"context"
"log"
"net"
"time"
)
func main() {
var d net.Dialer
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
conn, err := d.DialContext(ctx, "tcp", "localhost:12345")
if err != nil {
log.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
if _, err := conn.Write([]byte("Hello, World!")); err != nil {
log.Fatal(err)
}
}
Пример Unix
package main
import (
"context"
"log"
"net"
"time"
)
func main() {
// DialUnix does not take a context.Context parameter. This example shows
// how to dial a Unix socket with a Context. Note that the Context only
// applies to the dial operation; it does not apply to the connection once
// it has been established.
var d net.Dialer
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
d.LocalAddr = nil // if you have a local addr, add it here
raddr := net.UnixAddr{Name: "/path/to/unix.sock", Net: "unix"}
conn, err := d.DialContext(ctx, "unix", raddr.String())
if err != nil {
log.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
if _, err := conn.Write([]byte("Hello, socket!")); err != nil {
log.Fatal(err)
}
}
func (*Dialer) Dial
func (d *Dialer) Dial(network, address string) (Conn, error)
Dial соединяется с адресом в указанной сети.
Описание параметров сети и адреса см. в func Dial.
Dial внутренне использует context.Background; чтобы указать контекст, используйте Dialer.DialContext.
func (*Dialer) DialContext
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
DialContext соединяется с адресом в названной сети, используя предоставленный контекст.
Предоставленный контекст должен быть ненулевым. Если срок действия контекста истекает до завершения соединения, возвращается ошибка. После успешного соединения любое истечение срока действия контекста не повлияет на соединение.
При использовании TCP, когда хост в параметре адреса разрешается в несколько сетевых адресов, таймаут набора номера (из d.Timeout или ctx) распределяется на каждый последовательный набор, так что каждому дается соответствующая доля времени для соединения. Например, если у хоста есть 4 IP-адреса и таймаут составляет 1 минуту, то на соединение с каждым отдельным адресом будет дано 15 секунд, прежде чем будет выполнена попытка соединения со следующим адресом.
Описание параметров сети и адреса см. в func Dial.
func (*Dialer) MultipathTCP
func (d *Dialer) MultipathTCP() bool
MultipathTCP сообщает, будет ли использоваться MPTCP.
Этот метод не проверяет, поддерживается ли MPTCP операционной системой или нет.
func (*Dialer) SetMultipathTCP
func (d *Dialer) SetMultipathTCP(use bool)
SetMultipathTCP направляет методы Dial на использование или неиспользование MPTCP, если это поддерживается операционной системой. Этот метод отменяет системное значение по умолчанию и настройку GODEBUG=multipathtcp=…, если таковая имеется.
Если MPTCP недоступен на хосте или не поддерживается сервером, методы набора будут возвращаться к TCP.
type Error
type Error interface {
error
Timeout() bool // Является ли ошибка тайм-аутом?
// Исправлено: Временные ошибки не имеют четкого определения.
// Большинство "временных" ошибок - это таймауты, а редкие исключения вызывают удивление.
// Не используйте этот метод.
Temporary() bool
}
Error представляет сетевую ошибку.
type Flags
const (
FlagUp Flags = 1 << iota // интерфейс административно поднят
FlagBroadcast // интерфейс поддерживает возможность широковещательного доступа
FlagLoopback // интерфейс является интерфейсом обратной связи
FlagPointToPoint // интерфейс принадлежит к каналу "точка-точка
FlagMulticast // интерфейс поддерживает возможность многоадресного доступа
FlagRunning // интерфейс находится в рабочем состоянии
)
func (Flags) String
func (f Flags) String() string
type HardwareAddr
HardwareAddr представляет физический аппаратный адрес.
func ParseMAC
func ParseMAC(s string) (hw HardwareAddr, err error)
ParseMAC анализирует s как IEEE 802 MAC-48, EUI-48, EUI-64 или 20-октетный адрес канального уровня IP поверх InfiniBand, используя один из следующих форматов:
00:00:5e:00:53:01
02:00:5e:10:00:00:00:01
00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01
00-00-5e-00-53-01
02-00-5e-10-00-00-00-01
00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01
0000.5e00.5301
0200.5e10.0000.0001
0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001
func (HardwareAddr) String
func (a HardwareAddr) String() string
type IP
IP - это один IP-адрес, представляющий собой фрагмент байтов. Функции этого пакета принимают на вход 4-байтовые (IPv4) или 16-байтовые (IPv6) фрагменты.
Обратите внимание, что в этой документации обращение к IP-адресу как к IPv4-адресу или IPv6-адресу является семантическим свойством адреса, а не просто длиной байтового фрагмента: 16-байтовый фрагмент все еще может быть IPv4-адресом.
func IPv4
func IPv4(a, b, c, d byte) IP
IPv4 возвращает IP-адрес (в 16-байтовой форме) IPv4-адреса a.b.c.d.
Пример
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println(net.IPv4(8, 8, 8, 8))
}
func LookupIP
func LookupIP(host string) ([]IP, error)
LookupIP ищет хост с помощью локального резольвера. Он возвращает фрагмент IPv4 и IPv6 адресов этого хоста.
func ParseIP
func ParseIP(s string) IP
ParseIP разбирает s как IP-адрес, возвращая результат. Строка s может быть в десятичном точечном формате IPv4 (“192.0.2.1”), IPv6 (“2001:db8::68”) или IPv4-mapped IPv6 ("::ffff:192.0.2.1"). Если s не является корректным текстовым представлением IP-адреса, ParseIP возвращает nil. Возвращаемый адрес всегда имеет размер 16 байт, IPv4-адреса возвращаются в IPv4-сопоставленной IPv6-форме.
Пример
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println(net.ParseIP("192.0.2.1"))
fmt.Println(net.ParseIP("2001:db8::68"))
fmt.Println(net.ParseIP("192.0.2"))
}
Output:
192.0.2.1
2001:db8::68
<nil>
func (IP) AppendText
func (ip IP) AppendText(b []byte) ([]byte, error)
AppendText реализует интерфейс encoding.TextAppender. Кодировка такая же, как и у IP.String, за одним исключением: Если len(ip) равно нулю, то ничего не добавляется.
func (IP) DefaultMask
func (ip IP) DefaultMask() IPMask
DefaultMask возвращает IP-маску по умолчанию для IP-адреса ip. Маски по умолчанию есть только у IPv4-адресов; DefaultMask возвращает nil, если ip не является действительным IPv4-адресом.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ip := net.ParseIP("192.0.2.1")
fmt.Println(ip.DefaultMask())
}
func (IP) Equal
func (ip IP) Equal(x IP) bool
Equal сообщает, являются ли ip и x одним и тем же IP-адресом. IPv4-адрес и тот же адрес в форме IPv6 считаются равными.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv4DNS := net.ParseIP("8.8.8.8")
ipv4Lo := net.ParseIP("127.0.0.1")
ipv6DNS := net.ParseIP("0:0:0:0:0:FFFF:0808:0808")
fmt.Println(ipv4DNS.Equal(ipv4DNS))
fmt.Println(ipv4DNS.Equal(ipv4Lo))
fmt.Println(ipv4DNS.Equal(ipv6DNS))
}
func (IP) IsGlobalUnicast
func (ip IP) IsGlobalUnicast() bool
IsGlobalUnicast сообщает, является ли ip глобальным одноадресным адресом.
Для идентификации глобальных одноадресных адресов используется идентификация типа адреса, как определено в RFC 1122, RFC 4632 и RFC 4291, за исключением адресов направленной широковещательной рассылки IPv4. Возвращает true, даже если ip находится в частном адресном пространстве IPv4 или локальном адресном пространстве IPv6 одноадресной рассылки.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6Global := net.ParseIP("2000::")
ipv6UniqLocal := net.ParseIP("2000::")
ipv6Multi := net.ParseIP("FF00::")
ipv4Private := net.ParseIP("10.255.0.0")
ipv4Public := net.ParseIP("8.8.8.8")
ipv4Broadcast := net.ParseIP("255.255.255.255")
fmt.Println(ipv6Global.IsGlobalUnicast())
fmt.Println(ipv6UniqLocal.IsGlobalUnicast())
fmt.Println(ipv6Multi.IsGlobalUnicast())
fmt.Println(ipv4Private.IsGlobalUnicast())
fmt.Println(ipv4Public.IsGlobalUnicast())
fmt.Println(ipv4Broadcast.IsGlobalUnicast())
}
Output:
true
true
false
true
true
false
func (IP) IsInterfaceLocalMulticast
func (ip IP) IsInterfaceLocalMulticast() bool
IsInterfaceLocalMulticast сообщает, является ли ip интерфейсно-локальным многоадресным адресом.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6InterfaceLocalMulti := net.ParseIP("ff01::1")
ipv6Global := net.ParseIP("2000::")
ipv4 := net.ParseIP("255.0.0.0")
fmt.Println(ipv6InterfaceLocalMulti.IsInterfaceLocalMulticast())
fmt.Println(ipv6Global.IsInterfaceLocalMulticast())
fmt.Println(ipv4.IsInterfaceLocalMulticast())
}
func (IP) IsLinkLocalMulticast
func (ip IP) IsLinkLocalMulticast() bool
IsLinkLocalMulticast сообщает, является ли ip адресом link-local multicast.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6LinkLocalMulti := net.ParseIP("ff02::2")
ipv6LinkLocalUni := net.ParseIP("fe80::")
ipv4LinkLocalMulti := net.ParseIP("224.0.0.0")
ipv4LinkLocalUni := net.ParseIP("169.254.0.0")
fmt.Println(ipv6LinkLocalMulti.IsLinkLocalMulticast())
fmt.Println(ipv6LinkLocalUni.IsLinkLocalMulticast())
fmt.Println(ipv4LinkLocalMulti.IsLinkLocalMulticast())
fmt.Println(ipv4LinkLocalUni.IsLinkLocalMulticast())
}
Output:
true
false
true
false
func (IP) IsLinkLocalUnicast
func (ip IP) IsLinkLocalUnicast() bool
IsLinkLocalUnicast сообщает, является ли ip адресом link-local unicast.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6LinkLocalUni := net.ParseIP("fe80::")
ipv6Global := net.ParseIP("2000::")
ipv4LinkLocalUni := net.ParseIP("169.254.0.0")
ipv4LinkLocalMulti := net.ParseIP("224.0.0.0")
fmt.Println(ipv6LinkLocalUni.IsLinkLocalUnicast())
fmt.Println(ipv6Global.IsLinkLocalUnicast())
fmt.Println(ipv4LinkLocalUni.IsLinkLocalUnicast())
fmt.Println(ipv4LinkLocalMulti.IsLinkLocalUnicast())
}
Output:
true
false
true
false
func (IP) IsLoopback
func (ip IP) IsLoopback() bool
IsLoopback сообщает, является ли ip адресом loopback.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6Lo := net.ParseIP("::1")
ipv6 := net.ParseIP("ff02::1")
ipv4Lo := net.ParseIP("127.0.0.0")
ipv4 := net.ParseIP("128.0.0.0")
fmt.Println(ipv6Lo.IsLoopback())
fmt.Println(ipv6.IsLoopback())
fmt.Println(ipv4Lo.IsLoopback())
fmt.Println(ipv4.IsLoopback())
}
Output:
true
false
true
false
func (IP) IsMulticast
func (ip IP) IsMulticast() bool
IsMulticast сообщает, является ли ip адресом многоадресной рассылки.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6Multi := net.ParseIP("FF00::")
ipv6LinkLocalMulti := net.ParseIP("ff02::1")
ipv6Lo := net.ParseIP("::1")
ipv4Multi := net.ParseIP("239.0.0.0")
ipv4LinkLocalMulti := net.ParseIP("224.0.0.0")
ipv4Lo := net.ParseIP("127.0.0.0")
fmt.Println(ipv6Multi.IsMulticast())
fmt.Println(ipv6LinkLocalMulti.IsMulticast())
fmt.Println(ipv6Lo.IsMulticast())
fmt.Println(ipv4Multi.IsMulticast())
fmt.Println(ipv4LinkLocalMulti.IsMulticast())
fmt.Println(ipv4Lo.IsMulticast())
}
Output:
true
true
false
true
true
false
func (IP) IsPrivate
func (ip IP) IsPrivate() bool
IsPrivate сообщает, является ли ip частным адресом, в соответствии с RFC 1918 (IPv4-адреса) и RFC 4193 (IPv6-адреса).
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6Private := net.ParseIP("fc00::")
ipv6Public := net.ParseIP("fe00::")
ipv4Private := net.ParseIP("10.255.0.0")
ipv4Public := net.ParseIP("11.0.0.0")
fmt.Println(ipv6Private.IsPrivate())
fmt.Println(ipv6Public.IsPrivate())
fmt.Println(ipv4Private.IsPrivate())
fmt.Println(ipv4Public.IsPrivate())
}
Output:
true
false
true
false
func (IP) IsUnspecified
func (ip IP) IsUnspecified() bool
IsUnspecified сообщает, является ли ip неопределенным адресом, либо IPv4-адресом “0.0.0.0”, либо IPv6-адресом “::”.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6Unspecified := net.ParseIP("::")
ipv6Specified := net.ParseIP("fe00::")
ipv4Unspecified := net.ParseIP("0.0.0.0")
ipv4Specified := net.ParseIP("8.8.8.8")
fmt.Println(ipv6Unspecified.IsUnspecified())
fmt.Println(ipv6Specified.IsUnspecified())
fmt.Println(ipv4Unspecified.IsUnspecified())
fmt.Println(ipv4Specified.IsUnspecified())
}
Output:
true
false
true
false
func (IP) MarshalText
func (ip IP) MarshalText() ([]byte, error)
MarshalText реализует интерфейс encoding.TextMarshaler. Кодировка та же, что возвращается IP.String, за одним исключением: Если len(ip) равен нулю, возвращается пустой фрагмент.
func (IP) Mask
func (ip IP) Mask(mask IPMask) IP
Mask возвращает результат маскирования IP-адреса ip с помощью маски.
func (IP) String
func (ip IP) String() string
String возвращает строковую форму IP-адреса ip. Возвращается одна из 4 форм:
- “”, если ip имеет длину 0
- десятичная точка (“192.0.2.1”), если ip - это IPv4 или IP4-сопоставленный IPv6-адрес.
- IPv6 в соответствии с RFC 5952 (“2001:db8::1”), если ip - действительный IPv6-адрес
- шестнадцатеричная форма ip без знаков препинания, если другие случаи не применимы.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6 := net.IP{0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
ipv4 := net.IPv4(10, 255, 0, 0)
fmt.Println(ipv6.String())
fmt.Println(ipv4.String())
}
Output:
fc00::
10.255.0.0
func (IP) To16
To16 преобразует IP-адрес ip в 16-байтовое представление. Если ip не является IP-адресом (имеет неправильную длину), To16 возвращает nil.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6 := net.IP{0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
ipv4 := net.IPv4(10, 255, 0, 0)
fmt.Println(ipv6.To16())
fmt.Println(ipv4.To16())
}
Output:
fc00::
10.255.0.0
func (IP) To4
To4 преобразует IPv4-адрес ip в 4-байтовое представление. Если ip не является IPv4-адресом, To4 возвращает nil.
Пример
package main
import (
"fmt"
"net"
)
func main() {
ipv6 := net.IP{0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
ipv4 := net.IPv4(10, 255, 0, 0)
fmt.Println(ipv6.To4())
fmt.Println(ipv4.To4())
}
func (*IP) UnmarshalText
func (ip *IP) UnmarshalText(text []byte) error
UnmarshalText реализует интерфейс encoding.TextUnmarshaler. IP-адрес ожидается в форме, принимаемой ParseIP.
type IPAddr
type IPAddr struct {
IP-АДРЕС
Zone string // Зона масштабируемой адресации IPv6
}
IPAddr представляет адрес конечной точки IP.
func ResolveIPAddr
func ResolveIPAddr(network, address string) (*IPAddr, error)
ResolveIPAddr возвращает адрес конечной IP-точки.
Сеть должна быть именем IP-сети.
Если хост в параметре address не является литеральным IP-адресом, ResolveIPAddr преобразует адрес в адрес конечной точки IP. В противном случае он анализирует адрес как литеральный IP-адрес. В качестве параметра адреса может использоваться имя хоста, но это не рекомендуется, так как будет возвращен не более одного из IP-адресов имени хоста.
Описание параметров сети и адреса см. в разделе func Dial.
func (*IPAddr) Сеть
func (a *IPAddr) Network() string
Network возвращает сетевое имя адреса, “ip”.
func (*IPAddr) String
func (a *IPAddr) String() string
type IPConn
type IPConn struct {
// содержит отфильтрованные или неотправленные поля
}
IPConn - это реализация интерфейсов Conn и PacketConn для сетевых IP-соединений.
func DialIP
func DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error)
DialIP действует как Dial для IP-сетей.
Сеть должна быть именем IP-сети; подробности см. в func Dial.
Если laddr равен nil, автоматически выбирается локальный адрес. Если поле IP в raddr равно nil или является неопределенным IP-адресом, предполагается локальная система.
func ListenIP
func ListenIP(network string, laddr *IPAddr) (*IPConn, error)
ListenIP действует как ListenPacket для IP-сетей.
Сеть должна быть именем IP-сети; подробности см. в func Dial.
Если поле IP в laddr равно nil или неопределенному IP-адресу, ListenIP прослушивает все доступные IP-адреса локальной системы, кроме IP-адресов многоадресной рассылки.
func (*IPConn) Закрыть
func (c *IPConn) Close() error
Close закрывает соединение.
func (*IPConn) File
func (c *IPConn) File() (f *os.File, err error)
File возвращает копию базового файла os.File. Ответственность за закрытие f по завершении работы лежит на вызывающей стороне. Закрытие c не влияет на f, а закрытие f не влияет на c.
Дескриптор файла возвращаемого os.File отличается от дескриптора соединения. Попытка изменить свойства оригинала с помощью этого дубликата может привести к желаемому результату, а может и не привести.
func (*IPConn) LocalAddr
func (c *IPConn) LocalAddr() Addr
LocalAddr возвращает адрес локальной сети. Возвращаемый Addr является общим для всех вызовов LocalAddr, поэтому не следует его изменять.
func (*IPConn) Read
func (c *IPConn) Read(b []byte) (int, error)
Read реализует метод Conn Read.
func (*IPConn) ReadFrom
func (c *IPConn) ReadFrom(b []byte) (int, Addr, error)
ReadFrom реализует метод PacketConn ReadFrom.
func (*IPConn) ReadFromIP
func (c *IPConn) ReadFromIP(b []byte) (int, *IPAddr, error)
ReadFromIP действует как ReadFrom, но возвращает IPAddr.
func (*IPConn) ReadMsgIP
func (c *IPConn) ReadMsgIP(b, oob []byte) (n, oobn, flags int, addr *IPAddr, err error)
ReadMsgIP считывает сообщение из c, копируя полезную нагрузку в b и связанные с ней внеполосные данные в oob. Возвращается количество байт, скопированных в b, количество байт, скопированных в oob, флаги, которые были установлены для сообщения, и адрес источника сообщения.
Пакеты golang.org/x/net/ipv4 и golang.org/x/net/ipv6 могут быть использованы для манипулирования опциями сокетов IP-уровня в oob.
func (*IPConn) RemoteAddr
func (c *IPConn) RemoteAddr() Addr
RemoteAddr возвращает удаленный сетевой адрес. Возвращаемый Addr является общим для всех вызовов RemoteAddr, поэтому не изменяйте его.
func (*IPConn) SetDeadline
func (c *IPConn) SetDeadline(t time.Time) error
SetDeadline реализует метод Conn SetDeadline.
func (*IPConn) SetReadBuffer
func (c *IPConn) SetReadBuffer(bytes int) error
SetReadBuffer устанавливает размер буфера приема операционной системы, связанного с соединением.
func (*IPConn) SetReadDeadline
func (c *IPConn) SetReadDeadline(t time.Time) error
SetReadDeadline реализует метод Conn SetReadDeadline.
func (*IPConn) SetWriteBuffer
func (c *IPConn) SetWriteBuffer(bytes int) error
SetWriteBuffer устанавливает размер буфера передачи операционной системы, связанного с соединением.
func (*IPConn) SetWriteDeadline
func (c *IPConn) SetWriteDeadline(t time.Time) error
SetWriteDeadline реализует метод Conn SetWriteDeadline.
func (*IPConn) SyscallConn
func (c *IPConn) SyscallConn() (syscall.RawConn, error)
SyscallConn возвращает необработанное сетевое соединение. Оно реализует интерфейс syscall.Conn.
func (*IPConn) Write
func (c *IPConn) Write(b []byte) (int, error)
Write реализует метод Conn Write.
func (*IPConn) WriteMsgIP
func (c *IPConn) WriteMsgIP(b, oob []byte, addr *IPAddr) (n, oobn int, err error)
WriteMsgIP записывает сообщение на addr через c, копируя полезную нагрузку из b и связанные с ней внеполосные данные из oob. Возвращается количество записанных байт полезной нагрузки и внеполосных данных.
Пакеты golang.org/x/net/ipv4 и golang.org/x/net/ipv6 могут быть использованы для манипулирования опциями сокетов IP-уровня в oob.
func (*IPConn) WriteTo
func (c *IPConn) WriteTo(b []byte, addr Addr) (int, error)
WriteTo реализует метод PacketConn WriteTo.
func (*IPConn) WriteToIP
func (c *IPConn) WriteToIP(b []байт, addr *IPAddr) (int, error)
WriteToIP действует подобно IPConn.WriteTo, но принимает IPAddr.
type IPMask
IPMask - это битовая маска, которая может быть использована для манипулирования IP-адресами для IP-адресации и маршрутизации.
Подробнее см. в разделе type IPNet и func ParseCIDR.
func CIDRMask
func CIDRMask(ones, bits int) IPMask
CIDRMask возвращает IPMask, состоящую из битов ‘ones’ 1, за которыми следуют 0 до общей длины ‘bits’ бит. Для маски такой формы CIDRMask является обратной величиной к IPMask.Size.
Пример
package main
import (
"fmt"
"net"
)
func main() {
// This mask corresponds to a /31 subnet for IPv4.
fmt.Println(net.CIDRMask(31, 32))
// This mask corresponds to a /64 subnet for IPv6.
fmt.Println(net.CIDRMask(64, 128))
}
Output:
fffffffe
ffffffffffffffff0000000000000000
func IPv4Mask
func IPv4Mask(a, b, c, d byte) IPMask
IPv4Mask возвращает IP-маску (в 4-байтовой форме) IPv4-маски a.b.c.d.
Пример
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println(net.IPv4Mask(255, 255, 255, 0))
}
func (IPMask) Size
func (m IPMask) Size() (ones, bits int)
Size возвращает количество ведущих единиц и общее количество битов в маске. Если маска не имеет канонической формы - за единицами следуют нули - то Size возвращает 0, 0.
func (IPMask) String
func (m IPMask) String() string
String возвращает шестнадцатеричную форму m, без знаков препинания.
type IPNet
type IPNet struct {
IP IP // номер сети
Mask IPMask // маска сети
}
IPNet представляет IP-сеть.
func (*IPNet) Contains
func (n *IPNet) Contains(ip IP) bool
Contains сообщает, включает ли сеть ip.
func (*IPNet) Network
func (n *IPNet) Network() string
Network возвращает сетевое имя адреса, “ip+net”.
func (*IPNet) String
func (n *IPNet) String() string
String возвращает CIDR-нотацию n, например “192.0.2.0/24” или “2001:db8::/48”, как определено в RFC 4632 и RFC 4291. Если маска не имеет канонической формы, возвращается строка, состоящая из IP-адреса, за которым следует символ косой черты и маска, выраженная в шестнадцатеричной форме без знаков препинания, например “198.51.100.0/c000ff00”.
type Interface
type Interface struct {
Index int // положительное целое число, начинающееся с единицы, ноль никогда не используется
MTU int // максимальная единица передачи
Name string // например, "en0", "lo0", "eth0.100".
HardwareAddr HardwareAddr // форма IEEE MAC-48, EUI-48 и EUI-64
Flags Флаги // например, FlagUp, FlagLoopback, FlagMulticast
}
Интерфейс представляет собой сопоставление между именем и индексом сетевого интерфейса. Он также представляет информацию об объекте сетевого интерфейса.
func InterfaceByIndex
func InterfaceByIndex(index int) (*Interface, error)
InterfaceByIndex возвращает интерфейс, указанный индексом.
В Solaris возвращается один из логических сетевых интерфейсов, разделяющих логический канал данных; для большей точности используйте InterfaceByName.
func InterfaceByName
func InterfaceByName(name string) (*Interface, error)
InterfaceByName возвращает интерфейс, указанный именем.
func Интерфейсы
func Interfaces() ([]Interface, error)
Interfaces возвращает список сетевых интерфейсов системы.
func (*Interface) Addrs
func (ifi *Интерфейс) Addrs() ([]Addr, error)
Addrs возвращает список адресов одноадресных интерфейсов для конкретного интерфейса.
func (*Interface) MulticastAddrs
func (ifi *Интерфейс) MulticastAddrs() ([]Addr, error)
MulticastAddrs возвращает список адресов многоадресной рассылки, объединенных групп для определенного интерфейса.
type InvalidAddrError
type InvalidAddrError string
func (InvalidAddrError) Error
func (e InvalidAddrError) Error() string
func (InvalidAddrError) Temporary
func (e InvalidAddrError) Temporary() bool
func (InvalidAddrError) Timeout
func (e InvalidAddrError) Timeout() bool
type KeepAliveConfig
type KeepAliveConfig struct {
// Если Enable равно true, то зонды keep-alive включены.
Enable bool
// Idle - время, в течение которого соединение должно простаивать, прежде чем
// отправки первого зонда keep-alive.
// Если значение равно нулю, то по умолчанию используется значение 15 секунд.
Idle time.Duration
// Интервал - время между зондами keep-alive.
// Если ноль, по умолчанию используется значение 15 секунд.
Interval time.Duration
// Count - максимальное количество зондов keep-alive, которые
// может остаться без ответа до разрыва соединения.
// Если значение равно нулю, то по умолчанию используется значение 9.
Count int
}
KeepAliveConfig содержит параметры TCP keep-alive.
Если поля Idle, Interval или Count равны нулю, выбирается значение по умолчанию. Если поле отрицательно, соответствующая опция уровня сокета будет оставлена без изменений.
Обратите внимание, что до версии Windows 10 1709 ни установка Idle и Interval по отдельности, ни изменение Count (которое обычно равно 10) не поддерживаются. Поэтому, если вы хотите настроить параметры TCP keep-alive, рекомендуется установить неотрицательные значения Idle и Interval в сочетании с -1 для Count в старых версиях Windows. Напротив, если только один из Idle и Interval установлен в неотрицательное значение, другой будет установлен в значение по умолчанию, и в конечном итоге установите оба Idle и Interval в отрицательные значения, если вы хотите оставить их без изменений.
Обратите внимание, что Solaris и ее производные не поддерживают установку Interval в неотрицательное значение и Count в отрицательное значение, или наоборот.
type ListenConfig
type ListenConfig struct {
// Если Control не nil, то вызывается после создания сетевого
// соединения, но до его привязки к операционной системе.
//
// Параметры сети и адреса, передаваемые в функцию Control, не являются
// обязательно те, которые передаются в функцию Listen. Вызов Listen с сетями TCP
// приведет к вызову функции Control с параметрами "tcp4" или "tcp6",
// UDP-сети станут "udp4" или "udp6", IP-сети - "ip4" или "ip6",
// и другие известные сети передаются как есть.
Control func(network, address string, c syscall.RawConn) error
// KeepAlive задает период ожидания для сетевых
// соединений, принимаемых этим слушателем.
//
// KeepAlive игнорируется, если KeepAliveConfig.Enable равен true.
//
// Если ноль, то keep-alive включается, если поддерживается протоколом
// и операционной системой. Сетевые протоколы или операционные системы.
// которые не поддерживают keep-alive, игнорируют это поле.
// Если значение отрицательно, keep-alive отключены.
KeepAlive time.Duration
// KeepAliveConfig определяет конфигурацию зонда keep-alive
// для активного сетевого соединения, если оно поддерживается
// протоколом и операционной системой.
//
// Если KeepAliveConfig.Enable равен true, то зонды keep-alive включены.
// Если KeepAliveConfig.Enable ложно и KeepAlive отрицательно,
// зонды keep-alive отключены.
KeepAliveConfig KeepAliveConfig
// содержит отфильтрованные или неэкспонированные поля
}
ListenConfig содержит опции для прослушивания адреса.
func (*ListenConfig) Listen
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error)
Listener сообщает об адресе локальной сети.
Описание параметров сети и адреса см. в разделе func Listen.
Аргумент ctx используется при разрешении адреса для прослушивания; он не влияет на возвращаемый error.
func (*ListenConfig) ListenPacket
func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (PacketConn, error)
ListenPacket сообщает об адресе локальной сети.
Описание параметров сети и адреса см. в func ListenPacket.
Аргумент ctx используется при разрешении адреса для прослушивания; он не влияет на возвращаемый слушатель.
func (*ListenConfig) MultipathTCP
func (lc *ListenConfig) MultipathTCP() bool
MultipathTCP сообщает, будет ли использоваться MPTCP.
Этот метод не проверяет, поддерживается ли MPTCP операционной системой или нет.
func (*ListenConfig) SetMultipathTCP
func (lc *ListenConfig) SetMultipathTCP(use bool)
SetMultipathTCP направляет метод Listen на использование или неиспользование MPTCP, если это поддерживается операционной системой. Этот метод отменяет системное значение по умолчанию и настройку GODEBUG=multipathtcp=…, если таковая имеется.
Если MPTCP недоступен на хосте или не поддерживается клиентом, метод Listen вернется к TCP.
type Listener ¶
type Listener interface {
// Accept ожидает и возвращает следующее соединение со слушателем.
Accept() (Conn, error)
// Close закрывает слушателя.
// Любые заблокированные операции Accept будут разблокированы и вернут ошибки.
Close() error
// Addr возвращает сетевой адрес слушателя.
Addr() Addr
}
Listener - это общий сетевой слушатель для потоково-ориентированных протоколов.
Несколько горутинов могут одновременно вызывать методы одного Listener.
Пример
package main
import (
"io"
"log"
"net"
)
func main() {
// Listen on TCP port 2000 on all available unicast and
// anycast IP addresses of the local system.
l, err := net.Listen("tcp", ":2000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
for {
// Wait for a connection.
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
// Handle the connection in a new goroutine.
// The loop then returns to accepting, so that
// multiple connections may be served concurrently.
go func(c net.Conn) {
// Echo all incoming data.
io.Copy(c, c)
// Shut down the connection.
c.Close()
}(conn)
}
}
func FileListener
func FileListener(f *os.File) (ln Listener, err error)
FileListener возвращает копию сетевого слушателя, соответствующего открытому файлу f. Ответственность за закрытие ln по завершении работы лежит на вызывающей стороне. Закрытие ln не влияет на f, а закрытие f не влияет на ln.
func Listen
func Listen(network, address string) (Listener, error)
Listener сообщает об адресе локальной сети.
Сеть должна быть “tcp”, “tcp4”, “tcp6”, “unix” или “unixpacket”.
Для TCP-сетей, если параметр host в параметре address пуст или является буквальным неуказанным IP-адресом, Listen прослушивает все доступные одноадресные и одноадресные IP-адреса локальной системы. Чтобы использовать только IPv4, используйте сеть “tcp4”. В качестве адреса может использоваться имя хоста, но это не рекомендуется, поскольку в этом случае будет создан слушатель только для одного из IP-адресов хоста. Если порт в параметре адреса пустой или “0”, как в “127.0.0.1:” или “[::1]:0”, номер порта выбирается автоматически. Для определения выбранного порта можно использовать метод Addr Слушателя.
Описание параметров сети и адреса см. в func Dial.
Listen внутренне использует context.Background; чтобы указать контекст, используйте ListenConfig.Listen.
type MX
type MX struct {
Host string
Pref uint16
}
MX представляет собой одну запись DNS MX.
func LookupMX
func LookupMX(name string) ([]*MX, error)
LookupMX возвращает DNS MX-записи для заданного доменного имени, отсортированные по предпочтениям.
Возвращаемые имена почтовых серверов проверяются на правильность форматирования доменных имен в формате представления. Если ответ содержит недопустимые имена, эти записи отфильтровываются, и вместе с оставшимися результатами, если таковые имеются, возвращается ошибка.
Внутри LookupMX используется context.Background; чтобы указать контекст, используйте Resolver.LookupMX.
type NS
type NS struct {
Host string
}
NS представляет собой одну запись DNS NS.
func LookupNS
func LookupNS(name string) ([]*NS, error)
LookupNS возвращает записи DNS NS для заданного доменного имени.
Возвращаемые имена серверов имен проверяются на правильность форматирования доменных имен в формате представления. Если в ответе содержатся недопустимые имена, эти записи отфильтровываются, и вместе с оставшимися результатами возвращается ошибка, если таковая имеется.
Внутри LookupNS используется context.Background; чтобы указать контекст, используйте Resolver.LookupNS.
type OpError
type OpError struct {
// Op - это операция, вызвавшая ошибку, например.
// "чтение" или "запись".
Op string
// Net - тип сети, в которой произошла ошибка,
// например, "tcp" или "udp6".
Net string
// Для операций, связанных с удаленным сетевым соединением, например.
// набор, чтение или запись, Source - это соответствующий локальный
// сетевой адрес.
Source Addr
// Addr - это сетевой адрес, для которого произошла ошибка.
// Для локальных операций, таких как Listen или SetDeadline, Addr - это
// адрес локальной конечной точки, с которой производится манипуляция.
// Для операций с удаленным сетевым соединением, таких как.
// Dial, Read или Write, Addr - это удаленный адрес этого
// соединения.
Addr Addr
// Err - это ошибка, произошедшая во время операции.
// Метод Error паникует, если ошибка равна nil.
Err error
}
OpError - это тип ошибки, обычно возвращаемый функциями пакета net. Он описывает операцию, тип сети и адрес ошибки.
func (*OpError) Error
func (e *OpError) Error() string
func (*OpError) Temporary
func (e *OpError) Temporary() bool
func (*OpError) Timeout
func (e *OpError) Timeout() bool
func (*OpError) Unwrap
func (e *OpError) Unwrap() error
type PacketConn
type PacketConn interface {
// ReadFrom читает пакет из соединения,
// копируя полезную нагрузку в p. Возвращается количество
// байт, скопированных в p, и адрес возврата, который
// был в пакете.
// Возвращается количество прочитанных байт (0 <= n <= len(p))
// и любую возникшую ошибку. Вызывающая сторона всегда должна обрабатывать
// возвращенные n > 0 байт, прежде чем рассматривать ошибку err.
// ReadFrom можно сделать так, чтобы тайм-аут и возврат ошибки происходили через
// фиксированного лимита времени; см. SetDeadline и SetReadDeadline.
ReadFrom(p []byte) (n int, addr Addr, err error)
// WriteTo записывает пакет с полезной нагрузкой p в addr.
// WriteTo можно сделать так, чтобы по истечении фиксированного времени она завершилась и вернула ошибку.
// фиксированного лимита времени; см. SetDeadline и SetWriteDeadline.
// На пакетно-ориентированных соединениях таймаут записи случается редко.
WriteTo(p []byte, addr Addr) (n int, err error)
// Close закрывает соединение.
// Любые заблокированные операции ReadFrom или WriteTo будут разблокированы и вернут ошибки.
Close() error
// LocalAddr возвращает адрес локальной сети, если он известен.
LocalAddr() Addr
// SetDeadline устанавливает крайние сроки чтения и записи, связанные
// с данным соединением. Это эквивалентно вызову обеих функций.
// SetReadDeadline и SetWriteDeadline.
//
// Крайний срок - это абсолютное время, по истечении которого операции ввода-вывода
// прекращаются, а не блокируются. Крайний срок применяется ко всем будущим
// и ожидающим операциям ввода-вывода, а не только к непосредственно следующему вызову
// чтения или записи. После превышения крайнего срока
// соединение можно обновить, установив крайний срок в будущем.
//
// Если крайний срок превышен, вызов Read или Write или других
// методы ввода/вывода вернут ошибку, обернутую в os.ErrDeadlineExceeded.
// Это можно проверить с помощью errors.Is(err, os.ErrDeadlineExceeded).
// Метод Timeout ошибки вернет true, но обратите внимание, что существуют
// есть и другие возможные ошибки, для которых метод Timeout
// вернет true, даже если срок не был превышен.
//
// Таймаут ожидания может быть реализован путем многократного продления
// крайнего срока после успешных вызовов ReadFrom или WriteTo.
//
// Нулевое значение t означает, что операции ввода-вывода не будут выполняться по тайм-ауту.
SetDeadline(t time.Time) error
// SetReadDeadline устанавливает крайний срок для будущих вызовов ReadFrom
// и любого заблокированного в данный момент вызова ReadFrom.
// Нулевое значение t означает, что ReadFrom не будет завершаться по времени.
SetReadDeadline(t time.Time) error
// SetWriteDeadline устанавливает крайний срок для будущих вызовов WriteTo
// и любого текущего заблокированного вызова WriteTo.
// Даже если запись завершилась, она может вернуть n > 0, указывая на то, что
// часть данных была успешно записана.
// Нулевое значение t означает, что WriteTo не прервется.
SetWriteDeadline(t time.Time) error
}
PacketConn - это общее сетевое соединение, ориентированное на пакеты.
Несколько горутинов могут вызывать методы на PacketConn одновременно.
func FilePacketConn
func FilePacketConn(f *os.File) (c PacketConn, err error)
FilePacketConn возвращает копию пакетного сетевого соединения, соответствующего открытому файлу f. Ответственность за закрытие f по завершении работы лежит на вызывающей стороне. Закрытие c не влияет на f, а закрытие f не влияет на c.
func ListenPacket
func ListenPacket(network, address string) (PacketConn, error)
ListenPacket сообщает адрес локальной сети.
Сеть должна быть “udp”, “udp4”, “udp6”, “unixgram” или IP-транспортом. IP-транспорт - это “ip”, “ip4” или “ip6”, за которым следует двоеточие и буквенный номер протокола или имя протокола, как в “ip:1” или “ip:icmp”.
Для UDP- и IP-сетей, если в параметре address значение host пустое или это буквальный неопределенный IP-адрес, ListenPacket прослушивает все доступные IP-адреса локальной системы, кроме IP-адресов многоадресной рассылки. Чтобы использовать только IPv4, используйте сеть “udp4” или “ip4:proto”. В качестве адреса можно использовать имя хоста, но это не рекомендуется, поскольку в этом случае будет создан слушатель только для одного из IP-адресов хоста. Если порт в параметре адреса пустой или “0”, как в “127.0.0.1:” или “[::1]:0”, номер порта выбирается автоматически. Для обнаружения выбранного порта можно использовать метод LocalAddr программы PacketConn.
Описание параметров сети и адреса см. в func Dial.
ListenPacket внутренне использует context.Background; чтобы указать контекст, используйте ListenConfig.ListenPacket.
type ParseError
type ParseError struct {
// Тип - это тип ожидаемой строки, например.
// "IP-адрес", "CIDR-адрес".
Type string
// Text - неправильно сформированная текстовая строка.
Text string
}
ParseError - это тип ошибки синтаксического анализатора сетевых адресов.
func (*ParseError) Error
func (e *ParseError) Error() string
func (*ParseError) Temporary
func (e *ParseError) Temporary() bool
func (*ParseError) Timeout
func (e *ParseError) Timeout() bool
type Resolver
type Resolver struct {
// PreferGo управляет тем, предпочтителен ли встроенный DNS-резольвер Go.
// на платформах, где он доступен. Это эквивалентно настройке
// GODEBUG=netdns=go, но относится только к этому резольверу.
PreferGo bool
// StrictErrors управляет поведением временных ошибок
// (включая таймаут, ошибки сокетов и SERVFAIL) при использовании
// встроенного в Go резолвера. Для запроса, состоящего из нескольких
// подзапросов (например, поиск адреса A+AAA или переход по
// список поиска DNS), эта опция заставляет такие ошибки прерывать
// весь запрос, а не возвращать частичный результат. Эта опция
// не включена по умолчанию, поскольку это может повлиять на совместимость
// с резолверами, которые некорректно обрабатывают запросы AAAA.
StrictErrors bool
// Dial опционально задает альтернативный дозвон для использования
// встроенным DNS-резольвером Go для создания TCP- и UDP-соединений
// к службам DNS. Хост в параметре address будет
// всегда будет буквальным IP-адресом, а не именем хоста, а
// порт в параметре адреса будет буквальным номером порта
// а не имя службы.
// Если возвращаемый Conn также является PacketConn, отправленные и полученные DNS
// сообщения должны соответствовать разделу 4.2.1 RFC 1035, "Использование UDP".
// В противном случае DNS-сообщения, передаваемые через Conn, должны соответствовать
// разделу 5 RFC 7766, "Выбор транспортного протокола".
// Если nil, используется дозвонщик по умолчанию.
Dial func(ctx context.Context, network, address string) (Conn, error)
// содержит отфильтрованные или неэкспонированные поля
}
Резольвер ищет имена и номера.
Нулевой *Resolver эквивалентен нулевому Resolver.
func (*Resolver) LookupAddr
func (r *Resolver) LookupAddr(ctx context.Context, addr string) ([]string, error)
LookupAddr выполняет обратный поиск для заданного адреса, возвращая список имен, сопоставленных с этим адресом.
Возвращаемые имена проверяются на правильность форматирования доменных имен в формате представления. Если ответ содержит недопустимые имена, эти записи отфильтровываются, и вместе с оставшимися результатами, если таковые имеются, возвращается ошибка.
func (*Resolver) LookupCNAME
func (r *Resolver) LookupCNAME(ctx context.Context, host string) (string, error)
LookupCNAME возвращает каноническое имя для данного хоста. Пользователи, которым не важно каноническое имя, могут вызвать LookupHost или LookupIP напрямую; оба они позаботятся о разрешении канонического имени как части поиска.
Каноническое имя - это окончательное имя после следования нуля или более записей CNAME. LookupCNAME не возвращает ошибку, если хост не содержит записей DNS “CNAME”, при условии, что хост разрешается в адресные записи.
Возвращаемое каноническое имя проверяется на то, что оно является правильно отформатированным доменным именем формата представления.
func (*Resolver) LookupHost
func (r *Resolver) LookupHost(ctx context.Context, host string) (addrs []string, err error)
LookupHost ищет заданный хост с помощью локального резольвера. Возвращается фрагмент адресов этого хоста.
func (*Resolver) LookupIP
func (r *Resolver) LookupIP(ctx context.Context, network, host string) ([]IP, error)
LookupIP ищет хост для заданной сети с помощью локального резольвера. Возвращается фрагмент IP-адресов этого хоста типа, указанного network. network должен быть одним из “ip”, “ip4” или “ip6”.
func (*Resolver) LookupIPAddr
func (r *Resolver) LookupIPAddr(ctx context.Context, host string) ([]IPAddr, error)
LookupIPAddr ищет хост с помощью локального резольвера. Возвращается фрагмент IPv4- и IPv6-адресов этого хоста.
func (*Resolver) LookupMX
func (r *Resolver) LookupMX(ctx context.Context, name string) ([]*MX, error)
LookupMX возвращает DNS MX-записи для заданного доменного имени, отсортированные по предпочтению.
Возвращаемые имена почтовых серверов проверяются на правильность форматирования доменных имен в формате представления. Если в ответе содержатся недопустимые имена, эти записи отфильтровываются, и вместе с оставшимися результатами, если таковые имеются, возвращается ошибка.
func (*Resolver) LookupNS
func (r *Resolver) LookupNS(ctx context.Context, name string) ([]*NS, error)
LookupNS возвращает NS-записи DNS для заданного доменного имени.
Возвращаемые имена серверов имен проверяются на правильность форматирования доменных имен в формате представления. Если ответ содержит недопустимые имена, эти записи отфильтровываются, и вместе с оставшимися результатами, если таковые имеются, возвращается ошибка.
func (*Resolver) LookupNetIP
func (r *Resolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
LookupNetIP ищет хост с помощью локального резольвера. Возвращается фрагмент IP-адресов этого хоста типа, указанного в network. Сеть должна быть одной из “ip”, “ip4” или “ip6”.
func (*Resolver) LookupPort
func (r *Resolver) LookupPort(ctx context.Context, network, service string) (port int, err error)
LookupPort ищет порт для заданной сети и сервиса.
Сеть должна быть одной из “tcp”, “tcp4”, “tcp6”, “udp”, “udp4”, “udp6” или “ip”.
func (*Resolver) LookupSRV
func (r *Resolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*SRV, error)
LookupSRV пытается разрешить SRV-запрос данного сервиса, протокола и доменного имени. Протоколом является “tcp” или “udp”. Возвращаемые записи сортируются по приоритету и рандомизируются по весу в пределах приоритета.
LookupSRV строит DNS-имя для поиска в соответствии с RFC 2782. То есть, он ищет _service._proto.name. Для удобства сервисов, публикующих SRV-записи под нестандартными именами, если service и proto являются пустыми строками, LookupSRV ищет имя напрямую.
Возвращаемые имена сервисов проверяются на правильность форматирования доменных имен в формате представления. Если ответ содержит недопустимые имена, эти записи отфильтровываются и возвращается ошибка вместе с оставшимися результатами, если таковые имеются.
func (*Resolver) LookupTXT
func (r *Resolver) LookupTXT(ctx context.Context, name string) ([]string, error)
LookupTXT возвращает записи DNS TXT для заданного доменного имени.
Если DNS TXT-запись содержит несколько строк, они объединяются в одну строку.
type SRV
type SRV struct {
Target string
Port uint16
Priority uint16
Weight uint16
}
SRV представляет собой одну запись DNS SRV.
func LookupSRV
func LookupSRV(service, proto, name string) (cname string, addrs []*SRV, err error)
LookupSRV пытается разрешить SRV-запрос для данного сервиса, протокола и доменного имени. Протоколом является “tcp” или “udp”. Возвращаемые записи сортируются по приоритету и рандомизируются по весу в пределах приоритета.
LookupSRV строит DNS-имя для поиска в соответствии с RFC 2782. То есть, он ищет _service._proto.name. Для удобства сервисов, публикующих SRV-записи под нестандартными именами, если service и proto являются пустыми строками, LookupSRV ищет имя напрямую.
Возвращаемые имена сервисов проверяются на правильность форматирования доменных имен в формате представления. Если ответ содержит недопустимые имена, эти записи отфильтровываются, и вместе с оставшимися результатами, если таковые имеются, возвращается ошибка.
type TCPAddr
type TCPAddr struct {
IP IP
Port int
Zone string // Зона масштабируемой адресации IPv6
}
TCPAddr представляет адрес конечной точки TCP.
func ResolveTCPAddr
func ResolveTCPAddr(network, address string) (*TCPAddr, error)
ResolveTCPAddr возвращает адрес конечной точки TCP.
Сеть должна быть именем TCP-сети.
Если хост в параметре address не является литеральным IP-адресом или порт не является литеральным номером порта, ResolveTCPAddr преобразует адрес в адрес конечной точки TCP. В противном случае он разбирает адрес как пару из буквального IP-адреса и номера порта. В качестве параметра address может использоваться имя хоста, но это не рекомендуется, так как вернется не более одного из IP-адресов имени хоста.
Описание параметров сети и адреса см. в разделе func Dial.
func TCPAddrFromAddrPort
func TCPAddrFromAddrPort(addr netip.AddrPort) *TCPAddr
TCPAddrFromAddrPort возвращает addr как TCPAddr. Если addr.IsValid() равен false, то возвращаемый TCPAddr будет содержать нулевое поле IP, указывающее на неопределенный адрес, не зависящий от семейства адресов.
func (*TCPAddr) AddrPort
func (a *TCPAddr) AddrPort() netip.AddrPort
AddrPort возвращает TCPAddr a в виде netip.AddrPort.
Если a.Port не помещается в uint16, он тихо усекается.
Если a равно nil, возвращается нулевое значение.
func (*TCPAddr) Network
func (a *TCPAddr) Network() string
Network возвращает сетевое имя адреса, “tcp”.
func (*TCPAddr) String
func (a *TCPAddr) String() string
type TCPConn
type TCPConn struct {
// содержит отфильтрованные или неэкспонированные поля
}
TCPConn - это реализация интерфейса Conn для сетевых соединений TCP.
func DialTCP
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
DialTCP действует как Dial для сетей TCP.
Сеть должна быть именем TCP-сети; подробности см. в func Dial.
Если laddr равен nil, автоматически выбирается локальный адрес. Если поле IP в raddr равно nil или является неопределенным IP-адресом, предполагается локальная система.
func (*TCPConn) Закрыть
func (c *TCPConn) Close() error
Close закрывает соединение.
func (*TCPConn) CloseRead
func (c *TCPConn) CloseRead() error
CloseRead закрывает TCP-соединение со стороны чтения. Большинство вызывающих должны просто использовать Close.
func (*TCPConn) CloseWrite
func (c *TCPConn) CloseWrite() error
CloseWrite закрывает пишущую сторону TCP-соединения. Большинству вызывающих следует просто использовать Close.
func (*TCPConn) File
func (c *TCPConn) File() (f *os.File, err error)
File возвращает копию базового файла os.File. Ответственность за закрытие f по завершении работы лежит на вызывающей стороне. Закрытие c не влияет на f, а закрытие f не влияет на c.
Дескриптор файла возвращаемого os.File отличается от дескриптора соединения. Попытка изменить свойства оригинала с помощью этого дубликата может привести к желаемому результату, а может и не привести.
func (*TCPConn) LocalAddr
func (c *TCPConn) LocalAddr() Addr
LocalAddr возвращает адрес локальной сети. Возвращаемый Addr является общим для всех вызовов LocalAddr, поэтому не изменяйте его.
func (*TCPConn) MultipathTCP
func (c *TCPConn) MultipathTCP() (bool, error)
MultipathTCP сообщает, использует ли текущее соединение MPTCP.
Если Multipath TCP не поддерживается хостом, другим пиром или намеренно/случайно отфильтровывается каким-либо устройством между ними, будет выполнен откат к TCP. Этот метод делает все возможное, чтобы проверить, используется ли MPTCP или нет.
В Linux больше условий проверяется на ядрах >= v5.16, что улучшает результаты.
func (*TCPConn) Read
func (c *TCPConn) Read(b []byte) (int, error)
Read реализует метод Conn Read.
func (*TCPConn) ReadFrom
func (c *TCPConn) ReadFrom(r io.Reader) (int64, error)
ReadFrom реализует метод io.ReaderFrom ReadFrom.
func (*TCPConn) RemoteAddr
func (c *TCPConn) RemoteAddr() Addr
RemoteAddr возвращает адрес удаленной сети. Возвращаемый Addr является общим для всех вызовов RemoteAddr, поэтому не изменяйте его.
func (*TCPConn) SetDeadline
func (c *TCPConn) SetDeadline(t time.Time) error
SetDeadline реализует метод Conn SetDeadline.
func (*TCPConn) SetKeepAlive
func (c *TCPConn) SetKeepAlive(keepalive bool) error
SetKeepAlive устанавливает, должна ли операционная система отправлять сообщения keep-alive на соединение.
func (*TCPConn) SetKeepAliveConfig
func (c *TCPConn) SetKeepAliveConfig(config KeepAliveConfig) error
SetKeepAliveConfig настраивает сообщения keep-alive, отправляемые операционной системой.
func (*TCPConn) SetKeepAlivePeriod
func (c *TCPConn) SetKeepAlivePeriod(d time.Duration) error
SetKeepAlivePeriod устанавливает время, в течение которого соединение должно простаивать, прежде чем TCP начнет посылать keepalive зонды.
Обратите внимание, что вызов этого метода на Windows до Windows 10 версии 1709 сбросит KeepAliveInterval на системное значение по умолчанию, которое обычно составляет 1 секунду.
func (*TCPConn) SetLinger
func (c *TCPConn) SetLinger(sec int) error
SetLinger задает поведение Close на соединении, которое все еще ожидает отправки или подтверждения данных.
Если sec < 0 (по умолчанию), операционная система завершает отправку данных в фоновом режиме.
Если sec == 0, операционная система отбрасывает все неотправленные или неподтвержденные данные.
Если sec > 0, данные отправляются в фоновом режиме, как и в случае sec < 0. В некоторых операционных системах, включая Linux, это может привести к блокировке Close до тех пор, пока все данные не будут отправлены или отброшены. В некоторых операционных системах по истечении sec все оставшиеся неотправленные данные могут быть отброшены.
func (*TCPConn) SetNoDelay
func (c *TCPConn) SetNoDelay(noDelay bool) error
SetNoDelay управляет тем, должна ли операционная система задерживать передачу пакетов в надежде отправить меньше пакетов (алгоритм Нагла). По умолчанию установлено значение true (без задержки), что означает, что данные отправляются как можно быстрее после записи.
func (*TCPConn) SetReadBuffer
func (c *TCPConn) SetReadBuffer(bytes int) error
SetReadBuffer устанавливает размер буфера приема операционной системы, связанного с соединением.
func (*TCPConn) SetReadDeadline
func (c *TCPConn) SetReadDeadline(t time.Time) error
SetReadDeadline реализует метод Conn SetReadDeadline.
func (*TCPConn) SetWriteBuffer
func (c *TCPConn) SetWriteBuffer(bytes int) error
SetWriteBuffer устанавливает размер буфера передачи операционной системы, связанного с соединением.
func (*TCPConn) SetWriteDeadline
func (c *TCPConn) SetWriteDeadline(t time.Time) error
SetWriteDeadline реализует метод Conn SetWriteDeadline.
func (*TCPConn) SyscallConn
func (c *TCPConn) SyscallConn() (syscall.RawConn, error)
SyscallConn возвращает необработанное сетевое соединение. Оно реализует интерфейс syscall.Conn.
func (*TCPConn) Write
func (c *TCPConn) Write(b []byte) (int, error)
Write реализует метод записи Conn.
func (*TCPConn) WriteTo
func (c *TCPConn) WriteTo(w io.Writer) (int64, error)
WriteTo реализует метод io.WriterTo WriteTo.
type TCPListener
type TCPListener struct {
// содержит отфильтрованные или неэкспонированные поля
}
TCPListener - это сетевой слушатель TCP. Клиенты обычно должны использовать переменные типа Listener, а не предполагать, что это TCP.
func ListenTCP
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
ListenTCP действует как Listen для сетей TCP.
Сеть должна быть именем TCP-сети; подробности см. в func Dial.
Если поле IP в laddr равно nil или неопределенному IP-адресу, ListenTCP прослушивает все доступные одноадресные и одноадресные IP-адреса локальной системы. Если поле Port в laddr равно 0, номер порта выбирается автоматически.
func (*TCPListener) Accept
func (l *TCPListener) Accept() (Conn, error)
Accept реализует метод Accept в интерфейсе Listener; он ожидает следующего вызова и возвращает общий Conn.
func (*TCPListener) AcceptTCP
func (l *TCPListener) AcceptTCP() (*TCPConn, error)
AcceptTCP принимает следующий входящий вызов и возвращает новое соединение.
func (*TCPListener) Addr
func (l *TCPListener) Addr() Addr
Addr возвращает сетевой адрес слушателя, *TCPAddr. Возвращаемый Addr является общим для всех вызовов Addr, поэтому не изменяйте его.
func (*TCPListener) Close
func (l *TCPListener) Close() error
Close прекращает прослушивание TCP-адреса. Уже принятые соединения не закрываются.
func (*TCPListener) File
func (l *TCPListener) File() (f *os.File, err error)
File возвращает копию базового файла os.File. Ответственность за закрытие f по завершении работы лежит на вызывающей стороне. Закрытие l не влияет на f, а закрытие f не влияет на l.
Файловый дескриптор возвращаемого os.File отличается от дескриптора соединения. Попытка изменить свойства оригинала с помощью этого дубликата может привести к желаемому результату, а может и не привести.
func (*TCPListener) SetDeadline
func (l *TCPListener) SetDeadline(t time.Time) error
SetDeadline устанавливает крайний срок, связанный со слушателем. Нулевое значение времени отключает дедлайн.
func (*TCPListener) SyscallConn
func (l *TCPListener) SyscallConn() (syscall.RawConn, error)
SyscallConn возвращает необработанное сетевое соединение. Оно реализует интерфейс syscall.Conn.
Возвращаемое RawConn поддерживает только вызов Control. Чтение и запись возвращают ошибку.
type UDPAddr
type UDPAddr struct {
IP IP
Порт int
Zone string // Зона масштабируемой адресации IPv6
}
UDPAddr представляет адрес конечной точки UDP.
func ResolveUDPAddr
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
ResolveUDPAddr возвращает адрес конечной точки UDP.
Сеть должна быть именем UDP-сети.
Если хост в параметре address не является литеральным IP-адресом или порт не является литеральным номером порта, ResolveUDPAddr преобразует адрес в адрес конечной точки UDP. В противном случае он разбирает адрес как пару из буквального IP-адреса и номера порта. В качестве параметра address может использоваться имя хоста, но это не рекомендуется, так как вернется не более одного из IP-адресов имени хоста.
Описание параметров сети и адреса см. в разделе func Dial.
func UDPAddrFromAddrPort
func UDPAddrFromAddrPort(addr netip.AddrPort) *UDPAddr
UDPAddrFromAddrPort возвращает addr как UDPAddr. Если addr.IsValid() равен false, то возвращаемый UDPAddr будет содержать нулевое поле IP, указывающее на неопределенный адрес, не зависящий от семейства адресов.
func (*UDPAddr) AddrPort
func (a *UDPAddr) AddrPort() netip.AddrPort
AddrPort возвращает UDPAddr a в виде netip.AddrPort.
Если a.Port не помещается в uint16, он тихо усекается.
Если a равно nil, возвращается нулевое значение.
func (*UDPAddr) Network
func (a *UDPAddr) Network() string
Network возвращает сетевое имя адреса, “udp”.
func (*UDPAddr) String
func (a *UDPAddr) String() string
type UDPConn
type UDPConn struct {
// содержит отфильтрованные или неэкспонированные поля
}
UDPConn - это реализация интерфейсов Conn и PacketConn для сетевых соединений UDP.
func DialUDP
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
DialUDP действует как Dial для сетей UDP.
Сеть должна быть именем UDP-сети; подробности см. в func Dial.
Если laddr равно nil, автоматически выбирается локальный адрес. Если поле IP в raddr равно nil или является неопределенным IP-адресом, предполагается локальная система.
func ListenMulticastUDP
func ListenMulticastUDP(network string, ifi *Interface, gaddr *UDPAddr) (*UDPConn, error)
ListenMulticastUDP действует подобно ListenPacket для UDP сетей, но принимает групповой адрес на определенном сетевом интерфейсе.
Сеть должна быть именем UDP-сети; подробности см. в func Dial.
ListenMulticastUDP прослушивает все доступные IP-адреса локальной системы, включая групповой, многоадресный IP-адрес. Если ifi равно nil, ListenMulticastUDP использует назначенный системой многоадресный интерфейс, хотя это не рекомендуется, поскольку назначение зависит от платформ и иногда может потребовать настройки маршрутизации. Если поле Port в gaddr равно 0, номер порта выбирается автоматически.
ListenMulticastUDP предназначен только для удобства простых, небольших приложений. Для общего назначения существуют пакеты golang.org/x/net/ipv4 и golang.org/x/net/ipv6.
Обратите внимание, что ListenMulticastUDP установит опцию сокета IP_MULTICAST_LOOP в 0 в IPPROTO_IP, чтобы отключить обратную петлю для многоадресных пакетов.
func ListenUDP
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
ListenUDP действует как ListenPacket для сетей UDP.
Сеть должна быть именем UDP-сети; подробности см. в func Dial.
Если поле IP в laddr равно nil или неопределенному IP-адресу, ListenUDP прослушивает все доступные IP-адреса локальной системы, кроме IP-адресов многоадресной рассылки. Если поле Port в laddr равно 0, номер порта выбирается автоматически.
func (*UDPConn) Close
func (c *UDPConn) Close() error
Close закрывает соединение.
func (*UDPConn) File
func (c *UDPConn) File() (f *os.File, err error)
File возвращает копию базового файла os.File. Ответственность за закрытие f по завершении работы лежит на вызывающей стороне. Закрытие c не влияет на f, а закрытие f не влияет на c.
Дескриптор файла возвращаемого os.File отличается от дескриптора соединения. Попытка изменить свойства оригинала с помощью этого дубликата может привести к желаемому результату, а может и не привести.
func (*UDPConn) LocalAddr
func (c *UDPConn) LocalAddr() Addr
LocalAddr возвращает адрес локальной сети. Возвращаемый Addr является общим для всех вызовов LocalAddr, поэтому не изменяйте его.
func (*UDPConn) Read
func (c *UDPConn) Read(b []byte) (int, error)
Read реализует метод Conn Read.
func (*UDPConn) ReadFrom
func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error)
ReadFrom реализует метод PacketConn ReadFrom.
func (*UDPConn) ReadFromUDP
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
ReadFromUDP действует как UDPConn.ReadFrom, но возвращает UDPAddr.
func (*UDPConn) ReadFromUDPAddrPort
func (c *UDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error)
ReadFromUDPAddrPort действует аналогично ReadFrom, но возвращает netip.AddrPort.
Если c привязан к неопределенному адресу, возвращаемый адрес netip.AddrPort может быть IPv4-маппированным IPv6-адресом. Используйте netip.Addr.Unmap, чтобы получить адрес без префикса IPv6.
func (*UDPConn) ReadMsgUDP
func (c *UDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *UDPAddr, err error)
ReadMsgUDP считывает сообщение из c, копируя полезную нагрузку в b и связанные с ней внеполосные данные в oob. Возвращается количество байт, скопированных в b, количество байт, скопированных в oob, флаги, которые были установлены для сообщения, и адрес источника сообщения.
Пакеты golang.org/x/net/ipv4 и golang.org/x/net/ipv6 могут быть использованы для манипулирования опциями сокетов IP-уровня в oob.
func (*UDPConn) ReadMsgUDPAddrPort
func (c *UDPConn) ReadMsgUDPAddrPort(b, oob []byte) (n, oobn, flags int, addr netip.AddrPort, err error)
ReadMsgUDPAddrPort подобен UDPConn.ReadMsgUDP, но возвращает netip.AddrPort вместо UDPAddr.
func (*UDPConn) RemoteAddr
func (c *UDPConn) RemoteAddr() Addr
RemoteAddr возвращает адрес удаленной сети. Возвращаемый Addr является общим для всех вызовов RemoteAddr, поэтому не изменяйте его.
func (*UDPConn) SetDeadline
func (c *UDPConn) SetDeadline(t time.Time) error
SetDeadline реализует метод Conn SetDeadline.
func (*UDPConn) SetReadBuffer
func (c *UDPConn) SetReadBuffer(bytes int) error
SetReadBuffer устанавливает размер буфера приема операционной системы, связанного с соединением.
func (*UDPConn) SetReadDeadline
func (c *UDPConn) SetReadDeadline(t time.Time) error
SetReadDeadline реализует метод Conn SetReadDeadline.
func (*UDPConn) SetWriteBuffer
func (c *UDPConn) SetWriteBuffer(bytes int) error
SetWriteBuffer устанавливает размер буфера передачи операционной системы, связанного с соединением.
func (*UDPConn) SetWriteDeadline
func (c *UDPConn) SetWriteDeadline(t time.Time) error
SetWriteDeadline реализует метод Conn SetWriteDeadline.
func (*UDPConn) SyscallConn
func (c *UDPConn) SyscallConn() (syscall.RawConn, error)
SyscallConn возвращает необработанное сетевое соединение. Оно реализует интерфейс syscall.Conn.
func (*UDPConn) Write
func (c *UDPConn) Write(b []byte) (int, error)
Write реализует метод записи Conn.
func (*UDPConn) WriteMsgUDP
func (c *UDPConn) WriteMsgUDP(b, oob []byte, addr *UDPAddr) (n, oobn int, err error)
WriteMsgUDP записывает сообщение на addr через c, если c не подключен, или на удаленный адрес c, если c подключен (в этом случае addr должен быть nil). Полезная нагрузка копируется из b, а связанные с ней внеполосные данные - из oob. Возвращает количество записанных байтов полезной нагрузки и внеполосных данных.
Пакеты golang.org/x/net/ipv4 и golang.org/x/net/ipv6 могут быть использованы для манипулирования опциями сокетов IP-уровня в oob.
func (*UDPConn) WriteMsgUDPAddrPort
func (c *UDPConn) WriteMsgUDPAddrPort(b, oob []byte, addr netip.AddrPort) (n, oobn int, err error)
WriteMsgUDPAddrPort подобен UDPConn.WriteMsgUDP, но принимает netip.AddrPort вместо UDPAddr.
func (*UDPConn) WriteTo
func (c *UDPConn) WriteTo(b []byte, addr Addr) (int, error)
WriteTo реализует метод PacketConn WriteTo.
Пример
package main
import (
"log"
"net"
)
func main() {
// Unlike Dial, ListenPacket creates a connection without any
// association with peers.
conn, err := net.ListenPacket("udp", ":0")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
dst, err := net.ResolveUDPAddr("udp", "192.0.2.1:2000")
if err != nil {
log.Fatal(err)
}
// The connection can write data to the desired address.
_, err = conn.WriteTo([]byte("data"), dst)
if err != nil {
log.Fatal(err)
}
}
func (*UDPConn) WriteToUDP
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
WriteToUDP действует как UDPConn.WriteTo, но принимает UDPAddr.
func (*UDPConn) WriteToUDPAddrPort
func (c *UDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error)
WriteToUDPAddrPort действует как UDPConn.WriteTo, но принимает netip.AddrPort.
type UnixAddr
type UnixAddr struct {
строка имени
Net string
}
UnixAddr представляет адрес конечной точки сокета домена Unix.
func ResolveUnixAddr
func ResolveUnixAddr(network, address string) (*UnixAddr, error)
ResolveUnixAddr возвращает адрес конечной точки сокета домена Unix.
Сеть должна быть именем сети Unix.
Описание параметров сети и адреса см. в разделе func Dial.
func (*UnixAddr) Network
func (a *UnixAddr) Network() string
Network возвращает сетевое имя адреса, “unix”, “unixgram” или “unixpacket”.
func (*UnixAddr) String
func (a *UnixAddr) String() string
type UnixConn
type UnixConn struct {
// содержит отфильтрованные или неэкспонированные поля
}
UnixConn - это реализация интерфейса Conn для соединений с сокетами домена Unix.
func DialUnix
func DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error)
DialUnix действует как Dial для сетей Unix.
Сеть должна быть именем сети Unix; подробности см. в func Dial.
Если laddr не равен nil, он используется в качестве локального адреса для соединения.
func ListenUnixgram
func ListenUnixgram(network string, laddr *UnixAddr) (*UnixConn, error)
ListenUnixgram действует как ListenPacket для сетей Unix.
Сеть должна быть “unixgram”.
func (*UnixConn) Закрыть
func (c *UnixConn) Close() error
Close закрывает соединение.
func (*UnixConn) CloseRead
func (c *UnixConn) CloseRead() error
CloseRead закрывает сторону чтения соединения с доменом Unix. Большинство вызывающих должны просто использовать Close.
func (*UnixConn) CloseWrite
func (c *UnixConn) CloseWrite() error
CloseWrite закрывает пишущую сторону соединения с доменом Unix. Большинство вызывающих должны просто использовать Close.
func (*UnixConn) File
func (c *UnixConn) File() (f *os.File, err error)
File возвращает копию базового файла os.File. Ответственность за закрытие f по завершении работы лежит на вызывающей стороне. Закрытие c не влияет на f, а закрытие f не влияет на c.
Дескриптор файла возвращаемого os.File отличается от дескриптора соединения. Попытка изменить свойства оригинала с помощью этого дубликата может привести к желаемому результату, а может и не привести.
func (*UnixConn) LocalAddr
func (c *UnixConn) LocalAddr() Addr
LocalAddr возвращает адрес локальной сети. Возвращаемый Addr является общим для всех вызовов LocalAddr, поэтому не изменяйте его.
func (*UnixConn) Read
func (c *UnixConn) Read(b []byte) (int, error)
Read реализует метод Conn Read.
func (*UnixConn) ReadFrom
func (c *UnixConn) ReadFrom(b []byte) (int, Addr, error)
ReadFrom реализует метод PacketConn ReadFrom.
func (*UnixConn) ReadFromUnix
func (c *UnixConn) ReadFromUnix(b []byte) (int, *UnixAddr, error)
ReadFromUnix действует как UnixConn.ReadFrom, но возвращает UnixAddr.
func (*UnixConn) ReadMsgUnix
func (c *UnixConn) ReadMsgUnix(b, oob []byte) (n, oobn, flags int, addr *UnixAddr, err error)
ReadMsgUnix считывает сообщение из c, копируя полезную нагрузку в b и связанные с ней внеполосные данные в oob. Возвращается количество байт, скопированных в b, количество байт, скопированных в oob, флаги, которые были установлены для сообщения, и адрес источника сообщения.
Обратите внимание, что если len(b) == 0 и len(oob) > 0, эта функция все равно прочитает (и отбросит) 1 байт из соединения.
func (*UnixConn) RemoteAddr
func (c *UnixConn) RemoteAddr() Addr
RemoteAddr возвращает адрес удаленной сети. Возвращаемый Addr является общим для всех вызовов RemoteAddr, поэтому не изменяйте его.
func (*UnixConn) SetDeadline
func (c *UnixConn) SetDeadline(t time.Time) error
SetDeadline реализует метод Conn SetDeadline.
func (*UnixConn) SetReadBuffer
func (c *UnixConn) SetReadBuffer(bytes int) error
SetReadBuffer устанавливает размер буфера приема операционной системы, связанного с соединением.
func (*UnixConn) SetReadDeadline
func (c *UnixConn) SetReadDeadline(t time.Time) error
SetReadDeadline реализует метод Conn SetReadDeadline.
func (*UnixConn) SetWriteBuffer
func (c *UnixConn) SetWriteBuffer(bytes int) error
SetWriteBuffer устанавливает размер буфера передачи операционной системы, связанного с соединением.
func (*UnixConn) SetWriteDeadline
func (c *UnixConn) SetWriteDeadline(t time.Time) error
SetWriteDeadline реализует метод Conn SetWriteDeadline.
func (*UnixConn) SyscallConn
func (c *UnixConn) SyscallConn() (syscall.RawConn, error)
SyscallConn возвращает необработанное сетевое соединение. Оно реализует интерфейс syscall.Conn.
func (*UnixConn) Write
func (c *UnixConn) Write(b []byte) (int, error)
Write реализует метод Conn Write.
func (*UnixConn) WriteMsgUnix
func (c *UnixConn) WriteMsgUnix(b, oob []byte, addr *UnixAddr) (n, oobn int, err error)
WriteMsgUnix записывает сообщение в addr через c, копируя полезную нагрузку из b и связанные с ней внеполосные данные из oob. Возвращается количество записанных байт полезной нагрузки и внеполосных данных.
Обратите внимание, что если len(b) == 0 и len(oob) > 0, эта функция все равно запишет 1 байт в соединение.
func (*UnixConn) WriteTo
func (c *UnixConn) WriteTo(b []byte, addr Addr) (int, error)
WriteTo реализует метод PacketConn WriteTo.
func (*UnixConn) WriteToUnix
func (c *UnixConn) WriteToUnix(b []byte, addr *UnixAddr) (int, error)
WriteToUnix действует как UnixConn.WriteTo, но принимает UnixAddr.
type UnixListener
type UnixListener struct {
// содержит отфильтрованные или неэкспонированные поля
}
UnixListener - это слушатель сокетов домена Unix. Клиенты обычно должны использовать переменные типа Listener вместо того, чтобы предполагать сокеты домена Unix.
func ListenUnix
func ListenUnix(network string, laddr *UnixAddr) (*UnixListener, error)
ListenUnix действует как Listen для сетей Unix.
Сеть должна быть “unix” или “unixpacket”.
func (*UnixListener) Принять
func (l *UnixListener) Accept() (Conn, error)
Accept реализует метод Accept в интерфейсе Listener. Возвращаемые соединения будут иметь тип *UnixConn.
func (*UnixListener) AcceptUnix
func (l *UnixListener) AcceptUnix() (*UnixConn, error)
AcceptUnix принимает следующий входящий вызов и возвращает новое соединение.
func (*UnixListener) Addr
func (l *UnixListener) Addr() Addr
Addr возвращает сетевой адрес слушателя. Возвращаемый Addr является общим для всех вызовов Addr, поэтому не изменяйте его.
func (*UnixListener) Close
func (l *UnixListener) Close() error
Close прекращает прослушивание Unix-адреса. Уже принятые соединения не закрываются.
func (*UnixListener) File
func (l *UnixListener) File() (f *os.File, err error)
File возвращает копию базового файла os.File. Ответственность за закрытие f по завершении работы лежит на вызывающей стороне. Закрытие l не влияет на f, а закрытие f не влияет на l.
Файловый дескриптор возвращаемого os.File отличается от дескриптора соединения. Попытки изменить свойства оригинала с помощью этого дубликата могут привести к желаемому результату, а могут и не привести.
func (*UnixListener) SetDeadline
func (l *UnixListener) SetDeadline(t time.Time) error
SetDeadline устанавливает крайний срок, связанный со слушателем. Нулевое значение времени отключает дедлайн.
func (*UnixListener) SetUnlinkOnClose
func (l *UnixListener) SetUnlinkOnClose(unlink bool)
SetUnlinkOnClose устанавливает, должен ли базовый файл сокета быть удален из файловой системы при закрытии слушателя.
Поведение по умолчанию заключается в том, что файл сокета отсоединяется только тогда, когда пакет net создал его. То есть, если слушатель и базовый файл сокета были созданы вызовом Listen или ListenUnix, то по умолчанию закрытие слушателя приведет к удалению файла сокета. Но если слушатель был создан вызовом FileListener для использования уже существующего файла сокета, то по умолчанию закрытие слушателя не приведет к удалению файла сокета.
func (*UnixListener) SyscallConn
func (l *UnixListener) SyscallConn() (syscall.RawConn, error)
SyscallConn возвращает необработанное сетевое соединение. Оно реализует интерфейс syscall.Conn.
Возвращаемое RawConn поддерживает только вызов Control. Чтение и запись возвращают ошибку.
type UnknownNetworkError
type UnknownNetworkError string
func (UnknownNetworkError) Error
func (e UnknownNetworkError) Error() string
func (UnknownNetworkError) Temporary
func (e UnknownNetworkError) Temporary() bool
func (UnknownNetworkError) Timeout
func (e UnknownNetworkError) Timeout() bool
Примечания
Ошибки
- В JS и Windows функции FileConn, FileListener и FilePacketConn не реализованы.
- В JS методы и функции, связанные с Interface, не реализованы.
- В AIX, DragonFly BSD, NetBSD, OpenBSD, Plan 9 и Solaris метод MulticastAddrs интерфейса Interface не реализован.
- На всех платформах POSIX чтение из сети “ip4” с помощью метода ReadFrom или ReadFromIP может не возвращать полный пакет IPv4, включая его заголовок, даже если есть свободное место. Это может произойти даже в тех случаях, когда Read или ReadMsgIP могут вернуть полный пакет. По этой причине рекомендуется не использовать эти методы, если важно получить полный пакет.
- Рекомендации по совместимости Go 1 не позволяют нам изменить поведение этих методов; вместо них используйте Read или ReadMsgIP.
- На JS и Plan 9 методы и функции, связанные с IPConn, не реализованы.
- На Windows метод File интерфейса IPConn не реализован.
- В DragonFly BSD и OpenBSD прослушивание сетей “tcp” и “udp” не прослушивает соединения IPv4 и IPv6. Это связано с тем, что трафик IPv4 не будет маршрутизироваться в сокет IPv6 — для поддержки обеих семейств адресов требуются два отдельных сокета. Подробности см. в inet6(4).
- В Windows метод Write syscall.RawConn не интегрирован с сетевым опросником среды выполнения. Он не может ждать, пока соединение станет доступным для записи, и не учитывает deadli
4.15.3 - Пакет net/http встроенных функций и типов языка Go
Пакет 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, когда закончит с ним работать:
resp, err := http.Get("http://example.com/")
if err != nil {
// handle error
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
// ...
Клиенты и транспорты (Clients and Transports)
Для управления HTTP-заголовками, политикой перенаправлений (redirects) и другими настройками клиента, создайте 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, сжатие и др.) настройте http.Transport:
tr := &http.Transport{
MaxIdleConns: 10, // макс. число бездействующих соединений
IdleConnTimeout: 30 * time.Second, // таймаут для idle-соединений
DisableCompression: true, // отключение сжатия (gzip)
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
⚠ Важно:
Client и Transport потокобезопасны (можно использовать в нескольких горутинах).
- Их следует создавать один раз и переиспользовать для эффективности.
Серверы (Servers)
ListenAndServe запускает HTTP-сервер с указанным адресом и обработчиком (handler). Если handler = nil, используется DefaultServeMux:
http.Handle("/foo", fooHandler) // регистрация обработчика для пути "/foo"
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)) // запуск сервера на :8080
Для тонкой настройки сервера создайте http.Server:
s := &http.Server{
Addr: ":8080", // адрес сервера
Handler: myHandler, // кастомный обработчик
ReadTimeout: 10 * time.Second, // таймаут на чтение запроса
WriteTimeout: 10 * time.Second, // таймаут на запись ответа
MaxHeaderBytes: 1 << 20, // макс. размер заголовков (1 MB)
}
log.Fatal(s.ListenAndServe())
Поддержка HTTP/2
С Go 1.6+ HTTP/2 поддерживается автоматически при использовании HTTPS.
Как отключить HTTP/2?
- Для клиента: задать
Transport.TLSNextProto = make(map[string]func(...)) (пустой map).
- Для сервера: задать
Server.TLSNextProto = make(map[string]func(...)).
Через переменную GODEBUG:
GODEBUG=http2client=0 # отключить HTTP/2 для клиентов
GODEBUG=http2server=0 # отключить HTTP/2 для серверов
GODEBUG=http2debug=1 # логировать отладочную информацию
GODEBUG=http2debug=2 # расширенные логи (дампы фреймов)
⚠ Перед отключением HTTP/2 сообщите о проблеме: golang.org/s/http2bug.
Расширенная настройка HTTP/2
Для сложных сценариев (например, ручная конфигурация HTTP/2) используйте пакет golang.org/x/net/http2:
import "golang.org/x/net/http2"
// Для клиента:
http2.ConfigureTransport(tr) // tr — ваш *http.Transport
// Для сервера:
http2.ConfigureServer(s, nil) // s — ваш *http.Server
Ручная настройка через golang.org/x/net/http2 имеет приоритет над встроенной поддержкой HTTP/2 в net/http.
Ключевые термины
DefaultServeMux – стандартный муксер (роутер) HTTP-запросов.
Transport – управление низкоуровневыми параметрами HTTP-соединений.
TLSNextProto – настройка протоколов, работающих поверх TLS (например, HTTP/2).
GODEBUG – переменная окружения для отладки поведения Go-программ.
Константы
const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH" // RFC 5789
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
)
Общие методы HTTP. Если не указано иное, они определены в разделе 4.3 RFC 7231.
const (
StatusContinue = 100 // RFC 9110, 15.2.1
StatusSwitchingProtocols = 101 // RFC 9110, 15.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusEarlyHints = 103 // RFC 8297
StatusOK = 200 // RFC 9110, 15.3.1
StatusCreated = 201 // RFC 9110, 15.3.2
StatusAccepted = 202 // RFC 9110, 15.3.3
StatusNonAuthoritativeInfo = 203 // RFC 9110, 15.3.4
StatusNoContent = 204 // RFC 9110, 15.3.5
StatusResetContent = 205 // RFC 9110, 15.3.6
StatusPartialContent = 206 // RFC 9110, 15.3.7
StatusMultiStatus = 207 // RFC 4918, 11.1
StatusAlreadyReported = 208 // RFC 5842, 7.1
StatusIMUsed = 226 // RFC 3229, 10.4.1
StatusMultipleChoices = 300 // RFC 9110, 15.4.1
StatusMovedPermanently = 301 // RFC 9110, 15.4.2
StatusFound = 302 // RFC 9110, 15.4.3
StatusSeeOther = 303 // RFC 9110, 15.4.4
StatusNotModified = 304 // RFC 9110, 15.4.5
StatusUseProxy = 305 // RFC 9110, 15.4.6
StatusTemporaryRedirect = 307 // RFC 9110, 15.4.8
StatusPermanentRedirect = 308 // RFC 9110, 15.4.9
StatusBadRequest = 400 // RFC 9110, 15.5.1
StatusUnauthorized = 401 // RFC 9110, 15.5.2
StatusPaymentRequired = 402 // RFC 9110, 15.5.3
StatusForbidden = 403 // RFC 9110, 15.5.4
StatusNotFound = 404 // RFC 9110, 15.5.5
StatusMethodNotAllowed = 405 // RFC 9110, 15.5.6
StatusNotAcceptable = 406 // RFC 9110, 15.5.7
StatusProxyAuthRequired = 407 // RFC 9110, 15.5.8
StatusRequestTimeout = 408 // RFC 9110, 15.5.9
StatusConflict = 409 // RFC 9110, 15.5.10
StatusGone = 410 // RFC 9110, 15.5.11
StatusLengthRequired = 411 // RFC 9110, 15.5.12
StatusPreconditionFailed = 412 // RFC 9110, 15.5.13
StatusRequestEntityTooLarge = 413 // RFC 9110, 15.5.14
StatusRequestURITooLong = 414 // RFC 9110, 15.5.15
StatusUnsupportedMediaType = 415 // RFC 9110, 15.5.16
StatusRequestedRangeNotSatisfiable = 416 // RFC 9110, 15.5.17
StatusExpectationFailed = 417 // RFC 9110, 15.5.18
StatusTeapot = 418 // RFC 9110, 15.5.19 (Unused)
StatusMisdirectedRequest = 421 // RFC 9110, 15.5.20
StatusUnprocessableEntity = 422 // RFC 9110, 15.5.21
StatusLocked = 423 // RFC 4918, 11.3
StatusFailedDependency = 424 // RFC 4918, 11.4
StatusTooEarly = 425 // RFC 8470, 5.2.
StatusUpgradeRequired = 426 // RFC 9110, 15.5.22
StatusPreconditionRequired = 428 // RFC 6585, 3
StatusTooManyRequests = 429 // RFC 6585, 4
StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5
StatusUnavailableForLegalReasons = 451 // RFC 7725, 3
StatusInternalServerError = 500 // RFC 9110, 15.6.1
StatusNotImplemented = 501 // RFC 9110, 15.6.2
StatusBadGateway = 502 // RFC 9110, 15.6.3
StatusServiceUnavailable = 503 // RFC 9110, 15.6.4
StatusGatewayTimeout = 504 // RFC 9110, 15.6.5
StatusHTTPVersionNotSupported = 505 // RFC 9110, 15.6.6
StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1
StatusInsufficientStorage = 507 // RFC 4918, 11.5
StatusLoopDetected = 508 // RFC 5842, 7.2
StatusNotExtended = 510 // RFC 2774, 7
StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
)
HTTP коды состояния (HTTP status codes)
Коды состояния HTTP зарегистрированы в IANA. См.: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
Константы
const DefaultMaxHeaderBytes = 1 << 20 // 1 МБ
DefaultMaxHeaderBytes - максимально допустимый размер заголовков в HTTP-запросе. Можно переопределить через [Server.MaxHeaderBytes].
const DefaultMaxIdleConnsPerHost = 2
DefaultMaxIdleConnsPerHost - значение по умолчанию для Transport.MaxIdleConnsPerHost.
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
TimeFormat - формат времени для генерации временных меток в HTTP-заголовках. Аналогичен time.RFC1123, но использует GMT. Время должно быть в UTC.
const TrailerPrefix = "Trailer:"
TrailerPrefix - префикс для ключей [ResponseWriter.Header], указывающий, что значение относится к трейлерам ответа, а не к заголовкам.
Переменные
var (
ErrNotSupported = &ProtocolError{"feature not supported"}
ErrMissingBoundary = &ProtocolError{"no multipart boundary param in Content-Type"}
ErrNotMultipart = &ProtocolError{"request Content-Type isn't multipart/form-data"}
// Устаревшие ошибки...
)
var (
ErrBodyNotAllowed = errors.New("http: request method or response status code does not allow body")
ErrHijacked = errors.New("http: connection has been hijacked")
ErrContentLength = errors.New("http: wrote more than the declared Content-Length")
// Устаревшие ошибки...
)
Ошибки, используемые HTTP-сервером.
var (
ServerContextKey = &contextKey{"http-server"}
LocalAddrContextKey = &contextKey{"local-addr"}
)
Ключи контекста для доступа к серверу и локальному адресу.
var DefaultClient = &Client{}
DefaultClient - клиент по умолчанию, используемый Get, Head и Post.
var DefaultServeMux = &defaultServeMux
DefaultServeMux - ServeMux по умолчанию, используемый Serve.
var ErrAbortHandler = errors.New("net/http: abort Handler")
ErrAbortHandler - значение для прерывания обработчика без логирования стека.
var ErrBodyReadAfterClose = errors.New("http: invalid Read on closed Body")
Ошибка при чтении закрытого тела запроса/ответа.
NoBody - пустое тело запроса (io.ReadCloser), всегда возвращает EOF.
4.15.3.1 - Функции пакета net/http языка Go
Функции пакета http для реализации HTTP-клиента и сервера.
func CanonicalHeaderKey(s string) string
CanonicalHeaderKey возвращает канонический формат заголовка. Преобразует первую букву и буквы после дефиса в верхний регистр, остальные - в нижний. Например, “accept-encoding” → “Accept-Encoding”. Если s содержит пробелы или недопустимые символы, возвращается без изменений.
func DetectContentType
func DetectContentType(data []byte) string
DetectContentType определяет Content-Type данных по алгоритму https://mimesniff.spec.whatwg.org/. Анализирует первые 512 байт. Всегда возвращает валидный MIME-тип, по умолчанию “application/octet-stream”.
func Error
func Error(w ResponseWriter, error string, code int)
Error отправляет ответ с указанным HTTP-кодом и текстом ошибки. Устанавливает заголовки:
- Удаляет Content-Length
- Content-Type: “text/plain; charset=utf-8”
- X-Content-Type-Options: “nosniff”
func Handle
func Handle(pattern string, handler Handler)
Регистрирует обработчик для шаблона в DefaultServeMux.
Пример
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
type countHandler struct {
mu sync.Mutex // guards n
n int
}
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
defer h.mu.Unlock()
h.n++
fmt.Fprintf(w, "count is %d\n", h.n)
}
func main() {
http.Handle("/count", new(countHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
func HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
Регистрирует функцию-обработчик для шаблона в DefaultServeMux.
Пример
package main
import (
"io"
"log"
"net/http"
)
func main() {
h1 := func(w http.ResponseWriter, _ *http.Request) {
io.WriteString(w, "Hello from a HandleFunc #1!\n")
}
h2 := func(w http.ResponseWriter, _ *http.Request) {
io.WriteString(w, "Hello from a HandleFunc #2!\n")
}
http.HandleFunc("/", h1)
http.HandleFunc("/endpoint", h2)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func ListenAndServe
func ListenAndServe(addr string, handler Handler) error
Запускает HTTP-сервер на указанном адресе. Если handler=nil, используется DefaultServeMux.
Пример
package main
import (
"io"
"log"
"net/http"
)
func main() {
// Hello world, the web server
helloHandler := func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello, world!\n")
}
http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func ListenAndServeTLS
func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error
Аналогично ListenAndServe, но для HTTPS-соединений. Требует сертификат и приватный ключ.
Пример
package main
import (
"io"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello, TLS!\n")
})
// One can use generate_cert.go in crypto/tls to generate cert.pem and key.pem.
log.Printf("About to listen on 8443. Go to https://127.0.0.1:8443/")
err := http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)
log.Fatal(err)
}
func MaxBytesReader
func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser
Ограничивает размер тела запроса. В отличие от io.LimitReader:
- Возвращает ReadCloser
- При превышении лимита возвращает ошибку *MaxBytesError
- Закрывает исходный reader при вызове Close
func NotFound
func NotFound(w ResponseWriter, r *Request)
Отправляет ответ с HTTP 404.
func ParseHTTPVersion
func ParseHTTPVersion(vers string) (major, minor int, ok bool)
Разбирает строку версии HTTP (например, “HTTP/1.1” → (1, 1, true)).
func ParseTime
func ParseTime(text string) (t time.Time, err error)
Разбирает временную метку из заголовка, поддерживая три формата: TimeFormat, RFC850 и ANSIC.
func ProxyFromEnvironment
func ProxyFromEnvironment(req *Request) (*url.URL, error)
ProxyFromEnvironment возвращает URL прокси-сервера для указанного запроса на основе переменных окружения:
HTTP_PROXY/https_proxy - для HTTP-запросов
HTTPS_PROXY/https_proxy - для HTTPS-запросов
NO_PROXY/no_proxy - исключения из проксирования
Форматы значений:
- Полный URL (
http://proxy.example.com:8080)
host:port (подразумевается схема http)
Возвращаемые значения:
- Если прокси не задан или запрос исключен через
NO_PROXY → nil, nil
- При некорректном формате → ошибка
- Для
localhost (с портом или без) → nil, nil (специальный случай)
Примеры использования:
proxyUrl, err := http.ProxyFromEnvironment(req)
if err != nil {
// обработка ошибки конфигурации прокси
}
if proxyUrl != nil {
// настроить транспорт с этим прокси
}
Особенности:
- Автоматически выбирает переменную окружения по схеме запроса
- Учитывает исключения в
NO_PROXY
- Всегда пропускает запросы к localhost
- Поддерживает оба регистра переменных (UPPER и lower case)
func ProxyURL
func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error)
Возвращает функцию прокси, всегда возвращающую указанный URL.
func Redirect
func Redirect(w ResponseWriter, r *Request, url string, code int)
Перенаправляет запрос на указанный URL (может быть относительным путем). Код ответа должен быть в диапазоне 3xx (обычно 301, 302 или 303). Если Content-Type не установлен, автоматически добавляет “text/html; charset=utf-8” и простой HTML-ответ. Установка любого значения Content-Type отключает это поведение.
func Serve
func Serve(l net.Listener, handler Handler) error
Принимает входящие HTTP-соединения через listener, создавая новую горутину для каждого соединения. Если handler=nil, используется DefaultServeMux. Поддержка HTTP/2 требует TLS-соединений с “h2” в Config.NextProtos. Всегда возвращает non-nil ошибку.
func ServeContent
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker)
Отвечает на запрос содержимым из ReadSeeker с поддержкой:
- Range-запросов
- Заголовков If-Match/If-None-Match
- If-Modified-Since/If-Unmodified-Since
Автоматически определяет Content-Type (по расширению файла или через DetectContentType). Использует modtime для Last-Modified и проверки If-Modified-Since. Требует работающий Seek() для определения размера.
func ServeFile
func ServeFile(w ResponseWriter, r *Request, name string)
Отправляет содержимое файла или директории. Особенности:
- Отклоняет пути с “..” (защита от traversal-атак)
- Перенаправляет запросы к “/index.html” на родительский каталог
- Использует только параметр name (игнорирует r.URL.Path)
- Относительные пути разрешаются от текущего каталога
func ServeFileFS
func ServeFileFS(w ResponseWriter, r *Request, fsys fs.FS, name string)
Отправляет содержимое указанного файла или директории из файловой системы fsys в ответ на запрос. Файлы в fsys должны реализовывать интерфейс io.Seeker.
Меры предосторожности:
- Если имя (
name) формируется из пользовательского ввода, его необходимо санировать перед вызовом
- Автоматически отклоняет запросы с “..” в
r.URL.Path (защита от path traversal)
- Специальный случай: перенаправляет запросы, оканчивающиеся на “/index.html”, на путь без этого суффикса
Важно: выбор файла/директории осуществляется только по параметру name, r.URL.Path игнорируется (кроме двух указанных случаев).
func ServeTLS
func ServeTLS(l net.Listener, handler Handler, certFile, keyFile string) error
Принимает входящие HTTPS-соединения через listener l, создавая новую горутину для обработки каждого соединения. Требования:
- Если
handler не указан (nil), используется DefaultServeMux
- Необходимо указать файлы сертификата (
certFile) и приватного ключа (keyFile)
- Для цепочки сертификатов
certFile должен содержать: серверный сертификат, промежуточные и корневой CA
Особенности:
- Всегда возвращает non-nil ошибку
- Для корректной работы требуется правильная настройка TLS
Технические детали:
-
Для ServeFileFS:
- Работает с абстрактной файловой системой (
fs.FS)
- Требуется поддержка
Seek() для обработки Range-запросов
- Поведение с “index.html” аналогично классическим веб-серверам
-
Для ServeTLS:
- Автоматически активирует HTTP/2 при поддержке
- Рекомендуется использовать современные параметры шифрования
- Для production-среды следует использовать полную цепочку сертификатов
func SetCookie
func SetCookie(w ResponseWriter, cookie *Cookie)
Добавляет Set-Cookie заголовок в ответ. Некорректные cookie могут игнорироваться.
func StatusText
func StatusText(code int) string
Возвращает текстовое описание HTTP-статуса. Для неизвестных кодов возвращает пустую строку.
4.15.3.2 - Типы пакета net/http языка Go
Типы и методы пакета http для реализации HTTP-клиента и сервера.
type Client
type Client struct {
// Transport определяет механизм выполнения HTTP-запросов.
// Если nil, используется DefaultTransport.
Transport RoundTripper
// CheckRedirect определяет политику обработки редиректов.
// Если задан, вызывается перед каждым редиректом.
// Принимает новый запрос (req) и цепочку выполненных запросов (via).
// Если возвращает ошибку, клиент прекращает выполнение.
// Особый случай: ErrUseLastResponse возвращает последний Response без закрытия тела.
// Если nil, используется политика по умолчанию (максимум 10 редиректов).
CheckRedirect func(req *Request, via []*Request) error
// Jar хранит куки для автоматической подстановки в запросы.
// Обновляется при получении ответов с Set-Cookie.
// Если nil, куки отправляются только если явно заданы в Request.
Jar CookieJar
// Timeout устанавливает общий таймаут для запроса:
// включает подключение, редиректы и чтение тела ответа.
// Таймер продолжает работать после вызова Get/Post/Do.
// Нулевое значение отключает таймаут.
// Для отмены используется контекст запроса.
Timeout time.Duration
}
Client представляет HTTP-клиент. Нулевое значение (DefaultClient) - готовый к использованию клиент с DefaultTransport.
Особенности реализации:
Transport обычно содержит состояние (кешированные TCP-соединения), поэтому клиенты должны переиспользоваться
- Потокобезопасен для использования из нескольких горутин
- Работает на более высоком уровне, чем
RoundTripper, обрабатывая:
- Куки (через
Jar)
- Редиректы (через
CheckRedirect)
- Таймауты
Политика редиректов:
При перенаправлениях клиент:
- Не пересылает sensitive-заголовки (
Authorization, WWW-Authenticate, Cookie) на:
- Домены, не являющиеся поддоменами исходного (с
foo.com на bar.com)
- Особенности Cookie:
- При наличии
Jar: мутировавшие куки исключаются (Jar сам добавит обновленные)
- Без
Jar: исходные куки пересылаются без изменений
Пример использования:
client := &http.Client{
Timeout: 10 * time.Second,
Jar: cookieJar,
}
resp, err := client.Get("https://example.com")
func (*Client) CloseIdleConnections
(добавлено в Go 1.12)
func (c *Client) CloseIdleConnections()
Закрывает все бездействующие keep-alive соединения в Transport. Не прерывает активные соединения. Если Transport не поддерживает этот метод, операция не выполняется.
func (*Client) Do
func (c *Client) Do(req *Request) (*Response, error)
Выполняет HTTP-запрос с учетом настроек клиента (редиректы, куки, аутентификация). Особенности:
- Возвращает ошибку только при проблемах сети или срабатывании политик (CheckRedirect)
- Не считает HTTP-коды ≠ 2xx ошибкой
- Тело ответа (Body) всегда требует закрытия, даже при ошибках
- При редиректах:
- 301/302/303 → GET/HEAD (тело не сохраняется)
- 307/308 → сохраняет метод и тело (если определен Request.GetBody)
- Ошибки имеют тип *url.Error с флагом Timeout()
Пример:
req, _ := http.NewRequest("GET", url, nil)
resp, err := client.Do(req)
if err != nil { /* обработка */ }
defer resp.Body.Close()
func (*Client) Get
func (c *Client) Get(url string) (*Response, error)
Выполняет GET-запрос с автоматическим следованием редиректам (301, 302, 303, 307, 308). Особенности:
- Не возвращает ошибку при кодах ≠ 2xx
- Тело ответа требует закрытия
- Для кастомных заголовков используйте NewRequest + Do
func (*Client) Head
func (c *Client) Head(url string) (*Response, error)
Аналогично Get, но отправляет HEAD-запрос. Также обрабатывает редиректы.
func (*Client) Post
func (c *Client) Post(url, contentType string, body io.Reader) (*Response, error)
Выполняет POST-запрос с указанным Content-Type. Особенности:
- Если body реализует io.Closer, оно закрывается автоматически
- Для сложных запросов используйте NewRequest + Do
func (*Client) PostForm
func (c *Client) PostForm(url string, data url.Values) (*Response, error)
Отправляет POST с x-www-form-urlencoded данными. Автоматически:
- Кодирует данные
- Устанавливает Content-Type
- Обрабатывает редиректы
type ConnState
ConnState представляет состояние клиентского соединения с сервером. Используется в хуке Server.ConnState для отслеживания жизненного цикла соединений.
Состояния соединения:
const (
// StateNew - новое соединение, ожидается отправка запроса
StateNew ConnState = iota
// StateActive - соединение получило данные запроса (1+ байт)
StateActive
// StateIdle - соединение в режиме keep-alive между запросами
StateIdle
// StateHijacked - соединение было захвачено (hijacked)
StateHijacked
// StateClosed - соединение закрыто
StateClosed
)
Особенности:
-
Для HTTP/2:
StateActive активируется при переходе от 0 к 1 активному запросу
- Переход в другое состояние только после завершения всех запросов
-
Терминальные состояния:
StateHijacked - не переходит в StateClosed
StateClosed - конечное состояние
func (ConnState) String
func (c ConnState) String() string
Возвращает строковое представление состояния соединения.
Примеры использования:
- Базовый мониторинг соединений:
server := &http.Server{
ConnState: func(conn net.Conn, state http.ConnState) {
log.Printf("Соединение %v: %v -> %v",
conn.RemoteAddr(),
conn.LocalAddr(),
state.String())
},
}
- Ограничение активных соединений:
var activeConns int32
server := &http.Server{
ConnState: func(conn net.Conn, state http.ConnState) {
switch state {
case http.StateActive:
if atomic.AddInt32(&activeConns, 1) > 100 {
conn.Close() // закрываем при превышении лимита
}
case http.StateClosed, http.StateHijacked:
atomic.AddInt32(&activeConns, -1)
}
},
}
- Логирование времени жизни соединения:
type connInfo struct {
start time.Time
}
server := &http.Server{
ConnState: func(conn net.Conn, state http.ConnState) {
ctx := conn.Context()
if state == http.StateNew {
ctx = context.WithValue(ctx, "connInfo", &connInfo{
start: time.Now(),
})
conn = conn.WithContext(ctx)
return
}
if state == http.StateClosed {
if info, ok := ctx.Value("connInfo").(*connInfo); ok {
duration := time.Since(info.start)
log.Printf("Соединение жило %v", duration)
}
}
},
}
Типичный жизненный цикл соединения:
StateNew → StateActive → StateIdle → StateActive → ... → StateClosed
\ /
→ StateHijacked
Для HTTP/2 состояния меняются реже, так как одно соединение обслуживает множество запросов параллельно.
Вот профессиональный перевод документации типа Cookie с примерами использования:
type Cookie
type Cookie struct {
Name string // Название cookie (обязательное)
Value string // Значение cookie
Quoted bool // Флаг, было ли значение в кавычках
// Опциональные атрибуты:
Path string // Путь действия cookie
Domain string // Домен действия
Expires time.Time // Время истечения
RawExpires string // Оригинальное значение Expires (только для чтения)
// MaxAge определяет срок жизни в секундах:
// 0 - без Max-Age
// <0 - немедленное удаление (аналог Max-Age: 0)
// >0 - срок жизни в секундах
MaxAge int
Secure bool // Только для HTTPS
HttpOnly bool // Недоступно для JavaScript
SameSite SameSite // Политика SameSite
Partitioned bool // Разделенные cookie (CHIPS)
Raw string // Оригинальное значение заголовка
Unparsed []string // Неразобранные атрибуты
}
Тип Cookie представляет HTTP-куки, используемые в заголовках:
Set-Cookie - при установке сервером
Cookie - при отправке клиентом
Соответствует спецификации RFC 6265.
Примеры создания cookie:
// Простая cookie
sessionCookie := &http.Cookie{
Name: "session_id",
Value: "abc123",
}
// Защищенная cookie с атрибутами
secureCookie := &http.Cookie{
Name: "prefs",
Value: "dark_theme=true",
Path: "/",
Domain: "example.com",
MaxAge: 3600, // 1 час
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
func ParseCookie
(добавлено в Go 1.23)
func ParseCookie(line string) ([]*Cookie, error)
Разбирает заголовок Cookie (клиентские куки), возвращая все найденные куки. Одно имя может встречаться несколько раз.
Пример:
cookies, err := http.ParseCookie("name1=val1; name2=val2; name1=val3")
if err != nil { /* обработка ошибки */ }
for _, c := range cookies {
fmt.Printf("%s: %s\n", c.Name, c.Value)
}
// Вывод:
// name1: val1
// name2: val2
// name1: val3
func ParseSetCookie
(добавлено в Go 1.23)
func ParseSetCookie(line string) (*Cookie, error)
Разбирает заголовок Set-Cookie (серверные куки), возвращая одну куку.
Пример:
cookie, err := http.ParseSetCookie("session=abc123; Path=/; HttpOnly; Max-Age=3600")
if err != nil { /* обработка ошибки */ }
fmt.Printf("Cookie: %s=%s, Path=%s\n", cookie.Name, cookie.Value, cookie.Path)
func (*Cookie) String
func (c *Cookie) String() string
Форматирует куку для заголовка:
- Для
Cookie: только имя и значение (name=value)
- Для
Set-Cookie: со всеми атрибутами
Пример:
c := &http.Cookie{Name: "test", Value: "123"}
fmt.Println(c.String()) // "test=123"
c.Path = "/"
c.HttpOnly = true
fmt.Println(c.String()) // "test=123; Path=/; HttpOnly"
func (*Cookie) Valid
func (c *Cookie) Valid() error
Проверяет валидность куки (корректность имени и других атрибутов).
Пример:
err := (&http.Cookie{Name: "invalid name", Value: "test"}).Valid()
if err != nil {
fmt.Println("Invalid cookie:", err)
}
Практические примеры:
- Установка cookie в ResponseWriter:
func handler(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "user",
Value: "john",
Path: "/",
}
http.SetCookie(w, cookie)
}
- Чтение cookie из Request:
func handler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
// Обработка отсутствия cookie
}
fmt.Fprintf(w, "Value: %s", cookie.Value)
}
- Удаление cookie:
func logoutHandler(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1, // Удалить cookie
})
}
- Проверка SameSite политики:
cookie := &http.Cookie{
Name: "csrf_token",
Value: generateToken(),
SameSite: http.SameSiteStrictMode,
Secure: true,
}
Вот перевод документации с примерами:
type CookieJar
type CookieJar interface {
// SetCookies сохраняет полученные куки для указанного URL
SetCookies(u *url.URL, cookies []*Cookie)
// Cookies возвращает куки, которые должны быть отправлены для URL
Cookies(u *url.URL) []*Cookie
}
CookieJar управляет хранением и использованием кук в HTTP-запросах. Реализации должны быть потокобезопасными.
Пример использования:
jar, _ := cookiejar.New(nil)
client := &http.Client{
Jar: jar,
}
// Первый запрос (получаем куки)
resp, _ := client.Get("https://example.com/login")
// Последующие запросы автоматически включают куки
resp, _ = client.Get("https://example.com/dashboard")
type Dir
Dir реализует FileSystem для работы с файловой системой в пределах указанной директории.
Важные предупреждения:
- Может предоставлять доступ к чувствительным файлам
- Следует по символическим ссылкам за пределы директории
- Не фильтрует файлы, начинающиеся с точки (например,
.git)
Пример:
fs := http.Dir("/var/www")
http.Handle("/static/", http.FileServer(fs))
func (Dir) Open
func (d Dir) Open(name string) (File, error)
Открывает файл для чтения, используя os.Open. Пути должны использовать / как разделитель.
Пример:
fs := http.Dir("public")
file, err := fs.Open("css/styles.css")
if err != nil {
// обработка ошибки
}
defer file.Close()
type File
type File interface {
io.Closer
io.Reader
io.Seeker
Readdir(count int) ([]fs.FileInfo, error)
Stat() (fs.FileInfo, error)
}
Интерфейс файла, возвращаемый FileSystem.Open. Аналогичен *os.File.
type FileSystem
type FileSystem interface {
Open(name string) (File, error)
}
Интерфейс для работы с коллекцией файлов. Использует / как разделитель путей.
Пример реализации:
type CustomFS struct {
base string
}
func (c CustomFS) Open(name string) (http.File, error) {
return os.Open(filepath.Join(c.base, name))
}
func FS
func FS(fsys fs.FS) FileSystem
Конвертирует fs.FS в FileSystem. Файлы должны реализовывать io.Seeker.
Пример с embed:
//go:embed static/*
var staticFiles embed.FS
func main() {
http.Handle("/", http.FileServer(http.FS(staticFiles)))
http.ListenAndServe(":8080", nil)
}
Дополнительные примеры:
- Защищенная FileSystem (без доступа к скрытым файлам):
type SafeFS struct {
http.Dir
}
func (s SafeFS) Open(name string) (http.File, error) {
if strings.Contains(name, "..") || strings.HasPrefix(filepath.Base(name), ".") {
return nil, os.ErrNotExist
}
return s.Dir.Open(name)
}
- Кастомный CookieJar:
type MemoryJar struct {
cookies map[string][]*http.Cookie
sync.Mutex
}
func (j *MemoryJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.Lock()
defer j.Unlock()
j.cookies[u.Host] = cookies
}
func (j *MemoryJar) Cookies(u *url.URL) []*http.Cookie {
j.Lock()
defer j.Unlock()
return j.cookies[u.Host]
}
type Flusher
type Flusher interface {
// Flush sends any buffered data to the client.
Flush()
}
Интерфейс Flusher реализуется в ResponseWriter, которые позволяют HTTP-обработчику отправлять буферизованные данные клиенту.
Реализации ResponseWriter по умолчанию HTTP/1.x и HTTP/2 поддерживают Flusher, но обертки ResponseWriter могут этого не делать. Обработчики всегда должны проверять эту возможность во время выполнения.
Обратите внимание, что даже для ResponseWriter, поддерживающих Flush, если клиент подключен через HTTP-прокси, буферизованные данные могут не дойти до клиента до завершения ответа.
type HTTP2Config
type HTTP2Config struct {
// MaxConcurrentStreams опционально задает количество
// параллельных потоков, которые могут быть открыты у пира одновременно.
// Если значение равно нулю, то по умолчанию MaxConcurrentStreams равно как минимум 100.
MaxConcurrentStreams int
// MaxDecoderHeaderTableSize опционально задает верхний предел для
// размера таблицы сжатия заголовков, используемой для декодирования заголовков, отправленных
// пиром.
// Допустимое значение - менее 4 Мбайт.
// Если значение равно нулю или недействительно, используется значение по умолчанию.
MaxDecoderHeaderTableSize int
// MaxEncoderHeaderTableSize опционально задает верхний предел для
// таблицы сжатия заголовков, используемой для отправки заголовков сверстнику.
// Допустимое значение - менее 4 Мбайт.
// Если значение равно нулю или недействительно, используется значение по умолчанию.
MaxEncoderHeaderTableSize int
// MaxReadFrameSize опционально указывает самый большой кадр.
// который эта конечная точка готова прочитать.
// Допустимое значение - от 16КиБ до 16МиБ, включительно.
// Если значение равно нулю или недействительно, используется значение по умолчанию.
MaxReadFrameSize int
// MaxReceiveBufferPerConnection - это максимальный размер
// окна управления потоком для данных, получаемых по соединению.
// Допустимое значение - не менее 64КиБ и не более 4МиБ.
// Если значение недействительно, используется значение по умолчанию.
MaxReceiveBufferPerConnection int
// MaxReceiveBufferPerStream - это максимальный размер
// окна управления потоком для данных, получаемых в потоке (запросе).
// Допустимое значение - менее 4 Мбайт.
// Если значение равно нулю или недействительно, используется значение по умолчанию.
MaxReceiveBufferPerStream int
// SendPingTimeout - таймаут, по истечении которого проверка работоспособности с помощью ping
// кадра будет проведена, если на соединении не будет получено ни одного кадра.
// Если ноль, проверка работоспособности не выполняется.
SendPingTimeout time.Duration
// PingTimeout - таймаут, по истечении которого соединение будет закрыто.
// если не будет получен ответ на пинг.
// Если значение равно нулю, используется значение по умолчанию 15 секунд.
PingTimeout time.Duration
// WriteByteTimeout - таймаут, по истечении которого соединение будет // закрыто, если не удастся записать данные.
// закрыто, если в него не могут быть записаны данные. Таймаут начинается, когда данные // доступны для записи.
// доступны для записи, и продлевается каждый раз, когда записываются какие-либо байты.
WriteByteTimeout time.Duration
// PermitProhibitedCipherSuites, если true, разрешает использование
// наборов шифров, запрещенных спецификацией HTTP/2.
PermitProhibitedCipherSuites bool
// CountError, если не равен nil, вызывается при ошибках HTTP/2.
// Она предназначена для инкрементирования метрики для мониторинга.
// Тип errType содержит только строчные буквы, цифры и символы подчеркивания.
// (a-z, 0-9, _).
CountError func(errType string)
}
HTTP2Config определяет параметры конфигурации HTTP/2, общие для транспорта и сервера.
type Handler
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Обработчик отвечает на HTTP-запрос.
[Handler.ServeHTTP] должен записать заголовки и данные ответа в ResponseWriter, а затем вернуться. Возврат сигнализирует о том, что запрос завершен; использование ResponseWriter или чтение из [Request.Body] после или одновременно с завершением вызова ServeHTTP недопустимо.
В зависимости от программного обеспечения HTTP-клиента, версии протокола HTTP и любых посредников между клиентом и Go-сервером, чтение из [Request.Body] после записи в ResponseWriter может быть невозможным. Осторожные обработчики должны сначала прочитать [Request.Body], а затем ответить.
Кроме чтения тела, обработчики не должны изменять предоставленный Запрос.
Если ServeHTTP паникует, сервер (вызывающий ServeHTTP) предполагает, что эффект паники был изолирован от активного запроса. Он восстанавливает панику, записывает трассировку стека в журнал ошибок сервера и либо закрывает сетевое соединение, либо отправляет HTTP/2 RST_STREAM, в зависимости от протокола HTTP. Чтобы прервать обработчик, чтобы клиент увидел прерванный ответ, но сервер не зафиксировал ошибку, вызовите панику со значением ErrAbortHandler.
Обработчик отвечает на HTTP-запрос.
[Handler.ServeHTTP] должен записать заголовки и данные ответа в ResponseWriter, а затем вернуться. Возврат сигнализирует о том, что запрос завершен; использование ResponseWriter или чтение из [Request.Body] после или одновременно с завершением вызова ServeHTTP недопустимо.
В зависимости от программного обеспечения HTTP-клиента, версии протокола HTTP и любых посредников между клиентом и Go-сервером, чтение из [Request.Body] после записи в ResponseWriter может быть невозможным. Осторожные обработчики должны сначала прочитать [Request.Body], а затем ответить.
Кроме чтения тела, обработчики не должны изменять предоставленный Запрос.
Если ServeHTTP паникует, сервер (вызывающий ServeHTTP) предполагает, что эффект паники был изолирован от активного запроса. Он восстанавливает панику, записывает трассировку стека в журнал ошибок сервера и либо закрывает сетевое соединение, либо отправляет HTTP/2 RST_STREAM, в зависимости от протокола HTTP. Чтобы прервать обработчик, чтобы клиент увидел прерванный ответ, но сервер не зафиксировал ошибку, вызовите панику со значением ErrAbortHandler.
func AllowQuerySemicolons
func AllowQuerySemicolons(h Handler) Handler
AllowQuerySemicolons возвращает обработчик, который обслуживает запросы, преобразуя любые неэскэпированные точки с запятой в URL-запросе в амперсанд и вызывая обработчик h.
Это восстанавливает поведение, существовавшее до версии Go 1.17, когда параметры запроса разделялись как на точки с запятой, так и на амперсанд. (См. golang.org/issue/25192). Обратите внимание, что такое поведение не соответствует поведению многих прокси-серверов, и это несоответствие может привести к проблемам безопасности.
AllowQuerySemicolons должен быть вызван до вызова Request.ParseForm.
func FileServer
func FileServer(root FileSystem) Handler
FileServer возвращает обработчик, который обслуживает HTTP-запросы с содержимым файловой системы, укорененной в root.
В качестве особого случая, возвращаемый файловый сервер перенаправляет любой запрос, заканчивающийся на «/index.html», на тот же путь, без конечного «index.html».
Чтобы использовать реализацию файловой системы операционной системы, используйте http.Dir:
http.Handle(«/», http.FileServer(http.Dir(«/tmp»)))
Чтобы использовать реализацию fs.FS, используйте http.FileServerFS.
Пример
package main
import (
"log"
"net/http"
)
func main() {
// Simple static webserver:
log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("/usr/share/doc"))))
}
Пример DotFileHiding
пакет main
импорт (
«io»
«io/fs»
«log»
«net/http»
«strings»
)
// containsDotFile сообщает, содержит ли имя элемент пути, начинающийся с точки.
// Предполагается, что имя представляет собой файл, разделенный прямыми косыми чертами, как это гарантируется
// интерфейсом http.FileSystem.
func containsDotFile(name string) bool {
parts := strings.Split(name, «/»)
for _, part := range parts {
if strings.HasPrefix(part, «.») {
return true
}
}
return false
}
// dotFileHidingFile - это http.File, используемый в dotFileHidingFileSystem.
// Он используется для обертывания метода Readdir из http.File, чтобы мы могли
// удалять из его вывода файлы и каталоги, начинающиеся с точки.
тип dotFileHidingFile struct {
http.File
}
// Readdir - это обертка вокруг метода Readdir встроенного File.
// которая отфильтровывает все файлы, имя которых начинается с точки.
func (f dotFileHidingFile) Readdir(n int) (fis []fs.FileInfo, err error) {
files, err := f.File.Readdir(n)
for _, file := range files { // Отфильтровываем файлы с точками
if !strings.HasPrefix(file.Name(), «.») {
fis = append(fis, file)
}
}
if err == nil && n > 0 && len(fis) == 0 {
err = io.EOF
}
return
}
// dotFileHidingFileSystem - это http.FileSystem, которая скрывает
// скрытые «dot-файлы» от обслуживания.
тип dotFileHidingFileSystem struct {
http.FileSystem
}
// Open - это обертка вокруг метода Open встроенной файловой системы.
// который выдает ошибку разрешения 403, если в имени есть файл или каталог
// в пути которого имя начинается с точки.
func (fsys dotFileHidingFileSystem) Open(name string) (http.File, error) {
if containsDotFile(name) { // Если файл с точкой, возвращаем ответ 403
return nil, fs.ErrPermission
}
file, err := fsys.FileSystem.Open(name)
if err != nil {
return nil, err
}
return dotFileHidingFile{file}, err
}
func main() {
fsys := dotFileHidingFileSystem{http.Dir(«.»)}
http.Handle(«/», http.FileServer(fsys))
log.Fatal(http.ListenAndServe(«:8080», nil))
}
Пример StripPrefix
пакет main
импорт (
«net/http»
)
func main() {
// Чтобы обслужить каталог на диске (/tmp) по альтернативному URL
// пути (/tmpfiles/), используйте StripPrefix для изменения пути запроса
// путь URL до того, как его увидит файловый сервер:
http.Handle(«/tmpfiles/», http.StripPrefix(«/tmpfiles/», http.FileServer(http.Dir(«/tmp»))))
}
func FileServerFS
func FileServerFS(root fs.FS) Обработчик
FileServerFS возвращает обработчик, который обслуживает HTTP-запросы с содержимым файловой системы fsys. Файлы, предоставляемые fsys, должны реализовывать io.Seeker.
В качестве особого случая, возвращаемый файловый сервер перенаправляет любой запрос, заканчивающийся на «/index.html», на тот же путь, без конечного «index.html».
http.Handle(«/», http.FileServerFS(fsys))
func MaxBytesHandler
func MaxBytesHandler(h Handler, n int64) Handler
MaxBytesHandler возвращает обработчик, который запускает h с его ResponseWriter и [Request.Body], обернутыми MaxBytesReader.
func NotFoundHandler
func NotFoundHandler() Handler
NotFoundHandler возвращает простой обработчик запросов, который отвечает на каждый запрос ответом «404 страница не найдена».
Пример
package main
import (
"fmt"
"log"
"net/http"
)
func newPeopleHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "This is the people handler.")
})
}
func main() {
mux := http.NewServeMux()
// Create sample handler to returns 404
mux.Handle("/resources", http.NotFoundHandler())
// Create sample handler that returns 200
mux.Handle("/resources/people/", newPeopleHandler())
log.Fatal(http.ListenAndServe(":8080", mux))
}
func RedirectHandler
func RedirectHandler(url string, code int) Обработчик
RedirectHandler возвращает обработчик запроса, который перенаправляет каждый полученный запрос на заданный url с использованием заданного кода состояния.
Заданный код должен быть в диапазоне 3xx и обычно является StatusMovedPermanently, StatusFound или StatusSeeOther.
func StripPrefix
func StripPrefix(prefix string, h Handler) Handler
StripPrefix возвращает обработчик, который обслуживает HTTP-запросы, удаляя заданный префикс из Path (и RawPath, если задан) URL запроса и вызывая обработчик h. StripPrefix обрабатывает запрос на путь, который не начинается с префикса, отвечая ошибкой HTTP 404 not found. Префикс должен точно совпадать: если префикс в запросе содержит экранированные символы, ответом также будет ошибка HTTP 404 not found.
Пример
package main
import (
"net/http"
)
func main() {
// To serve a directory on disk (/tmp) under an alternate URL
// path (/tmpfiles/), use StripPrefix to modify the request
// URL's path before the FileServer sees it:
http.Handle("/tmpfiles/", http.StripPrefix("/tmpfiles/", http.FileServer(http.Dir("/tmp"))))
}
func TimeoutHandler
func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
TimeoutHandler возвращает обработчик, который запускает h с заданным лимитом времени.
Новый обработчик вызывает h.ServeHTTP для обработки каждого запроса, но если вызов выполняется дольше установленного лимита времени, обработчик отвечает ошибкой 503 Service Unavailable с заданным сообщением в теле. (Если msg пустое, будет отправлено подходящее сообщение по умолчанию). После такого таймаута запись h в свой ResponseWriter будет возвращать ErrHandlerTimeout.
TimeoutHandler поддерживает интерфейс Pusher, но не поддерживает интерфейсы Hijacker и Flusher.
type HandlerFunc
type HandlerFunc func(ResponseWriter, *Request)
Тип HandlerFunc - это адаптер, позволяющий использовать обычные функции в качестве HTTP-обработчиков. Если f - функция с соответствующей сигнатурой, то HandlerFunc(f) - это обработчик, вызывающий f.
func (HandlerFunc) ServeHTTP
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
ServeHTTP вызывает f(w, r).
type Header map[string][]string
Header представляет пары ключ-значение в HTTP-заголовке.
Ключи должны быть в канонической форме, как возвращает CanonicalHeaderKey.
func (h Header) Add(key, value string)
Add добавляет пару ключ-значение в заголовок. Она добавляется ко всем существующим значениям, связанным с ключом. Ключ не чувствителен к регистру; он канонизируется с помощью CanonicalHeaderKey.
func (h Header) Clone() Header
Clone возвращает копию h или nil, если h - nil.
func (h Header) Del(key string)
Del удаляет значения, связанные с ключом. Ключ не чувствителен к регистру; он канонизируется с помощью CanonicalHeaderKey.
func (h Header) Get(key string) string
Get получает первое значение, связанное с заданным ключом. Если значений, связанных с ключом, нет, Get возвращает “”. Нечувствительно к регистру; textproto.CanonicalMIMEHeaderKey используется для канонизации предоставленного ключа. Get предполагает, что все ключи хранятся в канонической форме. Чтобы использовать неканонические ключи, обращайтесь к карте напрямую.
func (h Header) Set(key, value string)
Set устанавливает записи заголовка, связанные с ключом, в значение одного элемента. Она заменяет все существующие значения, связанные с ключом. Ключ не чувствителен к регистру; он канонизируется с помощью textproto.CanonicalMIMEHeaderKey. Чтобы использовать неканонические ключи, присваивайте их непосредственно карте.
func (h Header) Values(key string) []string
Values возвращает все значения, связанные с заданным ключом. Нечувствительно к регистру; textproto.CanonicalMIMEHeaderKey используется для канонизации заданного ключа. Чтобы использовать неканонические ключи, обращайтесь к карте напрямую. Возвращаемый фрагмент не является копией.
func (h Header) Write(w io.Writer) error
Write записывает заголовок в формате wire.
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error
WriteSubset записывает заголовок в формате wire. Если exclude не равно nil, ключи, для которых exclude[key] == true, не записываются. Ключи не канонизируются перед проверкой карты exclude.
type Hijacker
type Hijacker interface
// Hijack позволяет вызывающей стороне взять на себя управление соединением.
// После вызова Hijack библиотека HTTP-сервера
// больше ничего не будет делать с соединением.
//
// Ответственность за управление // и закрытие соединения ложится на вызывающую сторону.
// и закрывать соединение.
//
// Возвращаемый net.Conn может иметь дедлайны чтения или записи.
// уже установленными, в зависимости от конфигурации
// сервера. Вызывающая сторона несет ответственность за установку
// или очистить эти сроки по мере необходимости.
//
// Возвращаемый файл bufio.Reader может содержать необработанные буферизованные
// данные от клиента.
//
// После вызова Hijack исходное Request.Body не должно
// использоваться. Контекст исходного запроса остается действительным и
// не отменяется до тех пор, пока метод ServeHTTP запроса
// возвращается.
Hijack() (net.Conn, *bufio.ReadWriter, error)
}
Интерфейс Hijacker реализуется в ResponseWriter’ах, которые позволяют HTTP-обработчику взять на себя управление соединением.
ResponseWriter по умолчанию для соединений HTTP/1.x поддерживает Hijacker, но соединения HTTP/2 намеренно не поддерживают его. Обертки ResponseWriter также могут не поддерживать Hijacker. Обработчики всегда должны проверять эту возможность во время выполнения.
Пример
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/hijack", func(w http.ResponseWriter, r *http.Request) {
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
return
}
conn, bufrw, err := hj.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Don't forget to close the connection:
defer conn.Close()
bufrw.WriteString("Now we're speaking raw TCP. Say hi: ")
bufrw.Flush()
s, err := bufrw.ReadString('\n')
if err != nil {
log.Printf("error reading string: %v", err)
return
}
fmt.Fprintf(bufrw, "You said: %q\nBye.\n", s)
bufrw.Flush()
})
}
type MaxBytesError
type MaxBytesError struct {
Limit int64
}
MaxBytesError возвращается MaxBytesReader, когда его лимит чтения превышен.
func (*MaxBytesError) Error
func (e *MaxBytesError) Error() string
type ProtocolError
устаревший
type Protocols
type Protocols struct {
// содержит отфильтрованные или неэкспонированные поля
}
Protocols - это набор протоколов HTTP. Нулевое значение - это пустой набор протоколов.
Поддерживаются следующие протоколы:
- HTTP1 - протоколы HTTP/1.0 и HTTP/1.1. HTTP1 поддерживается как на незащищенных TCP, так и на защищенных TLS соединениях.
- HTTP2 - протокол HTTP/2 через TLS-соединение.
- UnencryptedHTTP2 - протокол HTTP/2 через незащищенное TCP-соединение.
Пример Http1
package main
import (
"log"
"net/http"
)
func main() {
srv := http.Server{
Addr: ":8443",
}
// Serve only HTTP/1.
srv.Protocols = new(http.Protocols)
srv.Protocols.SetHTTP1(true)
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))
}
Пример Http1or2
package main
import (
"log"
"net/http"
)
func main() {
t := http.DefaultTransport.(*http.Transport).Clone()
// Use either HTTP/1 and HTTP/2.
t.Protocols = new(http.Protocols)
t.Protocols.SetHTTP1(true)
t.Protocols.SetHTTP2(true)
cli := &http.Client{Transport: t}
res, err := cli.Get("http://www.google.com/robots.txt")
if err != nil {
log.Fatal(err)
}
res.Body.Close()
}
func (Protocols) HTTP1
func (p Protocols) HTTP1() bool
HTTP1 сообщает, включает ли p протокол HTTP/1.
func (Протоколы) HTTP2
func (p Protocols) HTTP2() bool
HTTP2 сообщает, включает ли p протокол HTTP/2.
func (*Protocols) SetHTTP1
func (p *Protocols) SetHTTP1(ok bool)
SetHTTP1 добавляет или удаляет HTTP/1 из p.
func (*Protocols) SetHTTP2
func (p *Protocols) SetHTTP2(ok bool)
SetHTTP2 добавляет или удаляет HTTP/2 из p.
func (*Protocols) SetUnencryptedHTTP2
func (p *Protocols) SetUnencryptedHTTP2(ok bool)
SetUnencryptedHTTP2 добавляет или удаляет незашифрованный HTTP/2 из p.
func (Protocols) String
func (p Protocols) String() string
func (Protocols) UnencryptedHTTP2
func (p Protocols) UnencryptedHTTP2() bool
UnencryptedHTTP2 сообщает, включает ли p незашифрованный HTTP/2.
type PushOptions
type PushOptions struct {
// Method указывает HTTP-метод для обещанного запроса.
// Если установлен, он должен быть «GET» или «HEAD». Пустое значение означает «GET».
Method string
// Header указывает дополнительные заголовки обещанного запроса. Они не могут
// включать поля псевдозаголовков HTTP/2, такие как «:path» и «:scheme»,
// которые будут добавлены автоматически.
Заголовок Header
}
PushOptions описывает опции для [Pusher.Push].
type Pusher
type Pusher interface {
// Push инициирует HTTP/2 серверный push. При этом создается синтетический
// запрос, используя заданную цель и опции, сериализует этот запрос
// в фрейм PUSH_PROMISE, затем отправляет этот запрос с помощью
// обработчика запроса сервера. Если opts равно nil, используются параметры по умолчанию.
//
// Цель должна быть либо абсолютным путем (например, «/path»), либо абсолютным
// URL, содержащий действительный хост и ту же схему, что и родительский запрос.
// Если цель - это путь, то она наследует схему и хост // родительского запроса.
// родительского запроса.
//
// Спецификация HTTP/2 запрещает рекурсивные и кросс-авторитарные проталкивания.
// Push может обнаружить или не обнаружить эти недействительные проталкивания; однако недействительные
// push будут обнаружены и отменены соответствующими клиентами.
//
// Обработчики, желающие передать URL X, должны вызывать Push перед отправкой любых
// данные, которые могут вызвать запрос на URL X. Это позволяет избежать гонки, когда
// клиент отправляет запрос на X до получения PUSH_PROMISE для X.
//
// Push будет выполняться в отдельной горутине, что делает порядок прибытия
// недетерминированным. Любая требуемая синхронизация должна быть реализована
// вызывающей стороной.
//
// Push возвращает ErrNotSupported, если клиент отключил push или если push
// не поддерживается на базовом соединении.
Ошибка Push(target string, opts *PushOptions)
}
Pusher - это интерфейс, реализованный в ResponseWriters, которые поддерживают HTTP/2 server push. Более подробную информацию можно найти на сайте https://tools.ietf.org/html/rfc7540#section-8.2.
type Request
type Request struct {
// Method задает метод HTTP (GET, POST, PUT и т.д.).
// Для клиентских запросов пустая строка означает GET.
Строка Method
// URL указывает либо запрашиваемый URI (для серверных
// запросов), либо URL для доступа (для клиентских запросов).
//
// Для серверных запросов URL анализируется из URI.
// предоставленного в строке запроса и хранящегося в RequestURI. Для
// большинства запросов поля, кроме Path и RawQuery, будут // пустыми.
// пустыми. (См. RFC 7230, раздел 5.3)
//
// Для клиентских запросов URL's Host указывает сервер, к которому нужно // подключиться.
// соединиться с ним, а поле Request's Host опционально
// определяет значение заголовка Host для отправки в HTTP
// запросе.
URL *url.URL
// Версия протокола для входящих запросов сервера.
//
// Для клиентских запросов эти поля игнорируются. HTTP
// клиентский код всегда использует либо HTTP/1.1, либо HTTP/2.
// Подробнее см. документацию по транспорту.
Proto string // «HTTP/1.0»
ProtoMajor int // 1
ProtoMinor int // 0
// Заголовок содержит поля заголовка запроса, которые либо получены
// сервером, либо должны быть отправлены клиентом.
//
// Если сервер получил запрос со строками заголовка,
//
// Host: example.com
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// fOO: Bar
// foo: two
//
// тогда
//
// Header = map[string][]string{
// «Accept-Encoding»: { «gzip», «deflate» }
// «Accept-Language»: {«en-us»},
// «Foo»: { «Bar», «2»},
// }
//
// Для входящих запросов заголовок Host переводится в поле
// поле Request.Host и удаляется из карты Header.
//
// HTTP определяет, что имена заголовков не чувствительны к регистру. В
// парсер запросов реализует это с помощью CanonicalHeaderKey,
// делая первый символ и все символы, следующие за
// дефисом прописными, а остальные - строчными.
//
// Для клиентских запросов некоторые заголовки, такие как Content-Length
// и Connection, записываются автоматически, когда это необходимо, и
// значения в Header могут игнорироваться. См. документацию
// по методу Request.Write.
Request Header
// Body - это тело запроса.
//
// Для клиентских запросов нулевое тело означает, что запрос не имеет
// тело, например, запрос GET. Транспорт HTTP-клиента
// отвечает за вызов метода Close.
//
// Для серверных запросов тело запроса всегда не является nil
// но при отсутствии тела немедленно возвращается EOF.
// Сервер закроет тело запроса. Обработчику ServeHTTP
// обработчику не нужно.
//
// Тело должно позволять вызывать Read одновременно с Close.
// В частности, вызов Close должен разблокировать Read, ожидающий
// ввода.
Body io.ReadCloser
// GetBody определяет необязательную функцию для возврата новой копии
// Body. Она используется для клиентских запросов, когда перенаправление требует.
// чтения тела более одного раза. Использование GetBody по-прежнему
// требует установки Body.
//
// Для серверных запросов он не используется.
GetBody func() (io.ReadCloser, error)
// ContentLength записывает длину связанного содержимого.
// Значение -1 указывает на то, что длина неизвестна.
// Значения >= 0 указывают, что данное количество байт может быть
// быть прочитано из Body.
//
// Для клиентских запросов значение 0 с ненулевым Body
// также рассматривается как неизвестное.
ContentLength int64
// TransferEncoding перечисляет кодировки передачи данных от крайних к
// внутренней. Пустой список обозначает «идентичную» кодировку.
// TransferEncoding обычно можно игнорировать; кодировка chunked
// автоматически добавляется и удаляется по мере необходимости при отправке и
// получении запросов.
TransferEncoding []string
// Close указывает, закрывать ли соединение после
// ответа на этот запрос (для серверов) или после отправки этого
// запроса и чтения его ответа (для клиентов).
//
// Для серверных запросов HTTP-сервер обрабатывает это автоматически.
// и это поле не нужно обработчикам.
//
// Для клиентских запросов установка этого поля предотвращает повторное использование
// TCP-соединений между запросами к одним и тем же хостам, как если бы
// Transport.DisableKeepAlives был установлен.
Close bool
// Для серверных запросов поле Host указывает хост, на котором
// URL-адрес. Для HTTP/1 (согласно RFC 7230, раздел 5.4), это
// это либо значение заголовка «Host», либо имя хоста.
// указанное в самом URL. Для HTTP/2 это значение заголовка
// поля псевдозаголовка «:authority».
// Оно может иметь вид «хост:порт». Для международных доменных
// имен, Host может быть в форме Punycode или Unicode. Используйте
// golang.org/x/net/idna для преобразования его в любой формат, если
// необходимо.
// Для предотвращения атак перепривязки DNS обработчики сервера должны
// проверять, что заголовок Host имеет значение, для которого обработчик считает себя авторитетным.
// обработчик считает себя авторитетным. Включенный
// ServeMux поддерживает шаблоны, зарегистрированные на определенные хосты.
// называет и таким образом защищает свои зарегистрированные обработчики.
//
// Для клиентских запросов Host опционально переопределяет заголовок Host
// заголовок для отправки. Если он пуст, метод Request.Write использует
// значение URL.Host. Host может содержать международное
// доменное имя.
Host String
// Form содержит разобранные данные формы, включая параметры запроса поля URL
// параметры запроса поля, так и данные формы PATCH, POST или PUT.
// Это поле доступно только после вызова ParseForm.
// HTTP-клиент игнорирует Form и использует вместо нее Body.
Form url.Values
// PostForm содержит разобранные данные формы из параметров тела PATCH, POST
// или параметров тела PUT.
//
// Это поле доступно только после вызова ParseForm.
// HTTP-клиент игнорирует PostForm и использует вместо него Body.
PostForm url.Values
// MultipartForm - это разобранная многочастная форма, включая загрузку файлов.
// Это поле доступно только после вызова ParseMultipartForm.
// HTTP-клиент игнорирует MultipartForm и использует вместо нее Body.
MultipartForm *multipart.Form
// Trailer определяет дополнительные заголовки, которые отправляются после запроса
// body.
//
// Для серверных запросов карта Trailer изначально содержит только
// ключи трейлеров, со значениями nil. (Клиент объявляет, какие трейлеры он
// будет отправлять позже.) Пока обработчик читает из Body, он не должен // ссылаться на Trailer.
// не ссылаться на Trailer. После того как чтение из Body возвращает EOF, Trailer
// может быть прочитан снова и будет содержать значения non-nil, если они были отправлены
// клиентом.
//
// Для клиентских запросов Trailer должен быть инициализирован картой, содержащей
// ключи трейлера для последующей отправки. Значения могут быть nil или их конечные
// значениями. ContentLength должен быть 0 или -1, чтобы отправить чанкированный запрос.
// После отправки HTTP-запроса значения карты могут быть обновлены, пока
// пока читается тело запроса. Как только тело вернется в EOF, вызывающая сторона должна.
// не мутировать Trailer.
//
// Немногие HTTP-клиенты, серверы или прокси поддерживают HTTP-трейлеры.
Trailer Header
// RemoteAddr позволяет HTTP-серверам и другому программному обеспечению записывать.
// сетевой адрес, отправивший запрос, обычно для
// протоколирования. Это поле не заполняется ReadRequest и // не имеет определенного формата.
// не имеет определенного формата. HTTP-сервер в этом пакете
// устанавливает RemoteAddr в адрес «IP:порт» перед вызовом
// обработчика.
// Это поле игнорируется HTTP-клиентом.
RemoteAddr String
// RequestURI - это немодифицированный запрос-цель в
// Request-Line (RFC 7230, раздел 3.1.1), отправленный клиентом
// на сервер. Обычно вместо этого поля следует использовать поле URL.
// Ошибкой является установка этого поля в HTTP-запросе клиента.
RequestURI String
// TLS позволяет HTTP-серверам и другому программному обеспечению записывать
// информацию о TLS-соединении, по которому был получен запрос
// был получен. Это поле не заполняется ReadRequest.
// HTTP-сервер в этом пакете устанавливает поле для
// соединений с поддержкой TLS перед вызовом обработчика;
// в противном случае он оставляет поле нулевым.
// Это поле игнорируется HTTP-клиентом.
TLS *tls.ConnectionState
// Cancel - необязательный канал, закрытие которого указывает на то, что клиентский
// запрос следует считать отмененным. Не все реализации
// RoundTripper могут поддерживать Cancel.
//
// Для серверных запросов это поле неприменимо.
//
// Исправлено: Установите контекст запроса с помощью NewRequestWithContext
// вместо этого. Если поле Cancel и контекст запроса одновременно // установлены.
// установлены, не определено, соблюдается ли Cancel.
Cancel <-chan struct{}
// Response - это ответ перенаправления, который вызвал этот запрос.
// быть создан. Это поле заполняется только во время клиентских
// перенаправления.
Response *Response
// Pattern - шаблон [ServeMux], который соответствует запросу.
// Оно пустое, если запрос не был сопоставлен с шаблоном.
Строка Pattern
// содержит отфильтрованные или неотфильтрованные поля
}
Запрос представляет собой HTTP-запрос, полученный сервером или отправляемый клиентом.
Семантика полей немного отличается при использовании клиентом и сервером. В дополнение к примечаниям к полям, приведенным ниже, смотрите документацию по Request.Write и RoundTripper.
func NewRequest
func NewRequest(method, url string, body io.Reader) (*Request, error)
NewRequest оборачивает NewRequestWithContext, используя context.Background.
func NewRequestWithContext
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error)
NewRequestWithContext возвращает новый запрос, заданный методом, URL и необязательным телом.
Если предоставленное тело также является io.Closer, возвращаемое [Request.Body] устанавливается в body и будет закрыто (возможно, асинхронно) методами клиента Do, Post и PostForm, а также Transport.RoundTrip.
NewRequestWithContext возвращает запрос, подходящий для использования с Client.Do или Transport.RoundTrip. Чтобы создать запрос для использования при тестировании обработчика сервера, используйте функцию net/http/httptest.NewRequest, ReadRequest или вручную обновите поля Request. Для исходящего клиентского запроса контекст контролирует все время жизни запроса и его ответа: получение соединения, отправку запроса, чтение заголовков и тела ответа. Разницу между полями входящего и исходящего запроса смотрите в документации к типу Request.
Если body имеет тип *bytes.Buffer, *bytes.Reader или *strings.Reader, то ContentLength возвращаемого запроса устанавливается в точное значение (вместо -1), GetBody заполняется (чтобы 307 и 308 редиректы могли воспроизвести тело), а Body устанавливается в NoBody, если ContentLength равен 0.
func ReadRequest
func ReadRequest(b *bufio.Reader) (*Request, error)
ReadRequest читает и разбирает входящий запрос от b.
ReadRequest является низкоуровневой функцией и должна использоваться только в специализированных приложениях; большинство кода должно использовать сервер для чтения запросов и обрабатывать их через интерфейс Handler. ReadRequest поддерживает только запросы HTTP/1.x. Для HTTP/2 используйте golang.org/x/net/http2.
func (*Request) AddCookie
func (r *Request) AddCookie(c *Cookie)
AddCookie добавляет cookie в запрос. Согласно разделу 5.4 RFC 6265, AddCookie не добавляет более одного поля заголовка Cookie. Это означает, что все куки, если они есть, записываются в одну строку, разделенную точкой с запятой. AddCookie дезинфицирует только имя и значение c, и не дезинфицирует заголовок Cookie, уже присутствующий в запросе.
func (*Request) BasicAuth
func (r *Request) BasicAuth() (имя пользователя, строка пароля, ok bool)
BasicAuth возвращает имя пользователя и пароль, указанные в заголовке авторизации запроса, если запрос использует базовую аутентификацию HTTP. См. RFC 2617, раздел 2.
func (*Request) Clone
func (r *Request) Clone(ctx context.Context) *Request
Clone возвращает глубокую копию r с измененным контекстом на ctx. Предоставленный ctx должен быть ненулевым.
Clone делает неглубокую копию только поля Body.
Для исходящего клиентского запроса контекст контролирует все время жизни запроса и его ответа: получение соединения, отправку запроса, чтение заголовков и тела ответа.
func (*Request) Context
func (r *Request) Context() context.Context
Context возвращает контекст запроса. Чтобы изменить контекст, используйте Request.Clone или Request.WithContext.
Возвращаемый контекст всегда не имеет значения nil; по умолчанию это фоновый контекст.
Для исходящих клиентских запросов контекст управляет отменой.
Для входящих запросов сервера контекст отменяется при закрытии соединения клиента, отмене запроса (при использовании HTTP/2) или при возврате метода ServeHTTP.
func (*Request) Cookie
func (r *Request) Cookie(name string) (*Cookie, error)
Cookie возвращает именованный cookie, указанный в запросе, или ErrNoCookie, если он не найден. Если несколько cookie соответствуют заданному имени, будет возвращен только один cookie.
func (*Request) Cookies
func (r *Request) Cookies() []*Cookie
Cookies разбирает и возвращает HTTP-куки, отправленные вместе с запросом.
func (*Request) CookiesNamed
func (r *Request) CookiesNamed(name string) []*Cookie
CookiesNamed анализирует и возвращает именованные HTTP-куки, отправленные с запросом, или пустой фрагмент, если ни один из них не соответствует.
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
FormFile возвращает первый файл для предоставленного ключа формы. При необходимости FormFile вызывает Request.ParseMultipartForm и Request.ParseForm.
func (r *Request) FormValue(key string) string
FormValue возвращает первое значение для именованного компонента запроса. Порядок старшинства:
- тело формы в формате application/x-www-form-urlencoded (только POST, PUT, PATCH)
- параметры запроса (всегда)
- тело формы multipart/form-data (всегда)
FormValue вызывает Request.ParseMultipartForm и Request.ParseForm, если это необходимо, и игнорирует любые ошибки, возвращаемые этими функциями. Если ключ отсутствует, FormValue возвращает пустую строку. Чтобы получить доступ к нескольким значениям одного и того же ключа, вызовите ParseForm, а затем проверьте [Request.Form] напрямую.
func (*Request) MultipartReader
func (r *Request) MultipartReader() (*multipart.Reader, error)
MultipartReader возвращает читателя MIME multipart, если это запрос multipart/form-data или multipart/mixed POST, иначе возвращает nil и ошибку. Используйте эту функцию вместо Request.ParseMultipartForm для обработки тела запроса в виде потока.
func (r *Request) ParseForm() error
ParseForm заполняет r.Form и r.PostForm.
Для всех запросов ParseForm разбирает необработанный запрос из URL и обновляет r.Form.
Для запросов POST, PUT и PATCH он также считывает тело запроса, разбирает его как форму и помещает результаты в r.PostForm и r.Form. Параметры тела запроса имеют приоритет перед значениями строки запроса URL в r.Form.
Если размер тела запроса еще не был ограничен MaxBytesReader, его размер ограничивается 10 МБ.
Для других методов HTTP или если Content-Type не является application/x-www-form-urlencoded, тело запроса не считывается, а r.PostForm инициализируется не нулевым, пустым значением.
Request.ParseMultipartForm автоматически вызывает ParseForm. ParseForm является идемпотентным.
func (r *Request) ParseMultipartForm(maxMemory int64) error
ParseMultipartForm разбирает тело запроса как multipart/form-data. Разбирается все тело запроса, и не более maxMemory байт его файловых частей сохраняется в памяти, а остальная часть хранится на диске во временных файлах. При необходимости ParseMultipartForm вызывает Request.ParseForm. Если ParseForm возвращает ошибку, ParseMultipartForm возвращает ее, но при этом продолжает разбирать тело запроса. После одного вызова ParseMultipartForm последующие вызовы не имеют эффекта.
func (*Request) PathValue
func (r *Request) PathValue(name string) string
PathValue возвращает значение именованного подстановочного знака пути в шаблоне ServeMux, который соответствует запросу. Возвращается пустая строка, если запрос не был сопоставлен с шаблоном или в шаблоне нет такого подстановочного знака.
func (*Request) PostFormValue
func (r *Request) PostFormValue(key string) string
PostFormValue возвращает первое значение для именованного компонента в теле запроса POST, PUT или PATCH. Параметры запроса URL игнорируются. При необходимости PostFormValue вызывает Request.ParseMultipartForm и Request.ParseForm и игнорирует любые ошибки, возвращаемые этими функциями. Если ключ отсутствует, PostFormValue возвращает пустую строку.
func (*Request) ProtoAtLeast
func (r *Request) ProtoAtLeast(major, minor int) bool
ProtoAtLeast сообщает, является ли протокол HTTP, используемый в запросе, хотя бы мажорным и минорным.
func (*Request) Referer
func (r *Request) Referer() string
Referer возвращает ссылающийся URL, если он был отправлен в запросе.
Referer пишется неправильно, как и в самом запросе, что является ошибкой с самых ранних дней существования HTTP. Это значение также может быть получено из карты Header как Header[«Referer»]; преимущество предоставления его в виде метода заключается в том, что компилятор может диагностировать программы, использующие альтернативное (правильное английское) написание req.Referrer(), но не может диагностировать программы, использующие Header[«Referrer»].
func (*Request) SetBasicAuth
func (r *Request) SetBasicAuth(имя пользователя, пароль string)
SetBasicAuth устанавливает заголовок авторизации запроса на использование базовой аутентификации HTTP с указанными именем пользователя и паролем.
При использовании HTTP Basic Authentication предоставленные имя пользователя и пароль не шифруются. Обычно их следует использовать только в запросах HTTPS.
Имя пользователя не должно содержать двоеточие. Некоторые протоколы могут предъявлять дополнительные требования к предварительному введению имени пользователя и пароля. Например, при использовании OAuth2 оба аргумента должны быть сначала закодированы в URL с помощью url.QueryEscape.
func (*Request) SetPathValue
func (r *Request) SetPathValue(name, value string)
SetPathValue устанавливает имя в значение, так что последующие вызовы r.PathValue(name) возвращают значение.
func (*Request) UserAgent
func (r *Request) UserAgent() string
UserAgent возвращает User-Agent клиента, если он был отправлен в запросе.
func (*Request) WithContext
func (r *Request) WithContext(ctx context.Context) *Request
WithContext возвращает неглубокую копию r с измененным контекстом на ctx. Предоставленный ctx должен быть ненулевым.
Для исходящего клиентского запроса контекст контролирует все время жизни запроса и его ответа: получение соединения, отправку запроса, чтение заголовков и тела ответа.
Чтобы создать новый запрос с контекстом, используйте NewRequestWithContext. Чтобы создать глубокую копию запроса с новым контекстом, используйте Request.Clone.
func (*Request) Write
func (r *Request) Write(w io.Writer) error
Write записывает HTTP/1.1 запрос, который представляет собой заголовок и тело, в формате wire. Этот метод обрабатывает следующие поля запроса:
Host
URL
Method (defaults to "GET")
Header
ContentLength
TransferEncoding
Body
Если присутствует Body, Content-Length <= 0 и для [Request.TransferEncoding] не установлено значение «identity», Write добавляет в заголовок «Transfer-Encoding: chunked». Тело закрывается после отправки.
func (*Request) WriteProxy
func (r *Request) WriteProxy(w io.Writer) error
WriteProxy похож на Request.Write, но записывает запрос в форме, ожидаемой HTTP-прокси. В частности, Request.WriteProxy записывает начальную строку Request-URI запроса с абсолютным URI, согласно разделу 5.3 RFC 7230, включая схему и хост. В любом случае WriteProxy также записывает заголовок Host, используя либо r.Host, либо r.URL.Host.
type Response
type Response struct {
Status string // например, «200 OK»
StatusCode int // например, 200
Proto string // например, «HTTP/1.0»
ProtoMajor int // например, 1
ProtoMinor int // например, 0
// Header сопоставляет ключи заголовков со значениями. Если в ответе было несколько
// заголовков с одним и тем же ключом, они могут быть объединены с помощью запятой
// разделителями. (RFC 7230, раздел 3.2.2 требует, чтобы несколько заголовков
// были семантически эквивалентны последовательности, разделенной запятыми.) Когда
// значения заголовков дублируются другими полями в этой структуре (например,
// ContentLength, TransferEncoding, Trailer), значения полей являются
// авторитетное.
//
// Ключи в карте канонизированы (см. CanonicalHeaderKey).
Header Header
// Body представляет собой тело ответа.
//
// Тело ответа передается по требованию, когда поле Body
// считывается. Если сетевое соединение обрывается или сервер
// завершает ответ, вызов Body.Read возвращает ошибку.
//
// Клиент и транспорт http гарантируют, что Body всегда
// non-nil, даже в ответах без тела или в ответах с
// телом нулевой длины. Вызывающая сторона обязана // закрыть Body.
// закрыть Body. Транспорт HTTP-клиента по умолчанию не может
// повторно использовать TCP-соединения HTTP/1.x «keep-alive», если тело
// не прочитано до конца и не закрыто.
//
// Тело автоматически дешанкируется, если сервер ответил
// с «chunked» Transfer-Encoding.
//
// Начиная с Go 1.12, тело также будет реализовывать io.Writer
// при успешном ответе «101 Switching Protocols»,
// как это используется в WebSockets и режиме «h2c» HTTP/2.
Body io.ReadCloser
// ContentLength записывает длину связанного содержимого. Значение
// значение -1 указывает на то, что длина неизвестна. Если Request.Method
// является «HEAD», значения >= 0 указывают, что данное количество байт может быть
// быть прочитано из Body.
ContentLength int64
// Содержит кодировки передачи данных от крайнего внешнего к крайнему внутреннему. Значение
// nil, означает, что используется кодировка «identity».
TransferEncoding []string
// Close записывает, указывал ли заголовок, что соединение должно быть
// закрыть после прочтения Body. Значение является рекомендательным для клиентов: ни
// ни ReadResponse, ни Response.Write никогда не закрывают соединение.
Close bool
// Uncompressed сообщает, был ли ответ отправлен в сжатом виде, но
// был распакован пакетом http. Если значение равно true, чтение из
// Body выдает несжатое содержимое вместо сжатого
// содержимое, фактически переданное с сервера, ContentLength устанавливается в -1,
// а поля «Content-Length» и «Content-Encoding» удаляются
// из responseHeader. Чтобы получить оригинальный ответ от
// сервера, установите Transport.DisableCompression в true.
Uncompressed bool
// Трейлер сопоставляет ключи трейлера со значениями в том же
// формате, что и Header.
//
// Изначально трейлер содержит только значения nil, по одному для
// каждого ключа, указанного в заголовке «Trailer» сервера.
// значение. Эти значения не добавляются в Header.
//
// К трейлеру нельзя обращаться одновременно с вызовами Read
// на Body.
//
// После того как Body.Read вернет io.EOF, Trailer будет содержать.
// любые значения трейлера, отправленные сервером.
Trailer Header
// Request - это запрос, который был отправлен для получения данного Response.
// Тело запроса равно нулю (оно уже было использовано).
// Это значение заполняется только для клиентских запросов.
Request *Request
// TLS содержит информацию о TLS-соединении, по которому был получен
// был получен ответ. Он равен nil для незашифрованных ответов.
// Указатель разделяется между ответами и не должен быть // изменен.
// изменяться.
TLS *tls.ConnectionState
}
Response представляет собой ответ на HTTP-запрос.
Клиент и транспорт возвращают ответы от серверов после получения заголовков ответа. Тело ответа передается по запросу по мере считывания поля Body.
func Get
func Get(url string) (resp *Response, err error)
Get выполняет GET на указанный URL. Если ответ содержит один из следующих кодов перенаправления, Get следует за этим перенаправлением, максимум 10 перенаправлений:
301 (Moved Permanently)
302 (найдено)
303 (См. другое)
307 (Временное перенаправление)
308 (постоянное перенаправление)
Ошибка возвращается, если было слишком много перенаправлений или если произошла ошибка протокола HTTP. Ответ не-2xx не приводит к ошибке. Любая возвращаемая ошибка будет иметь тип *url.Error. Метод Timeout значения url.Error сообщит true, если запрос завершился по таймеру.
Если err равен nil, resp всегда содержит ненулевой resp.Body. Вызывающая сторона должна закрыть resp.Body, когда закончит чтение из него.
Get - это обертка вокруг DefaultClient.Get.
Чтобы сделать запрос с пользовательскими заголовками, используйте NewRequest и DefaultClient.Do.
Чтобы сделать запрос с заданным контекстом (context.Context), используйте NewRequestWithContext и DefaultClient.Do.
Пример
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func main() {
res, err := http.Get("http://www.google.com/robots.txt")
if err != nil {
log.Fatal(err)
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode > 299 {
log.Fatalf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body)
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", body)
}
func Head
func Head(url string) (resp *Response, err error)
Head выдает HEAD на указанный URL. Если ответ представляет собой один из следующих кодов перенаправления, Head следует за этим перенаправлением, максимум 10 перенаправлений:
301 (Moved Permanently)
302 (найдено)
303 (См. другое)
307 (Временное перенаправление)
308 (постоянный редирект)
Head - это обертка вокруг DefaultClient.Head.
Чтобы сделать запрос с указанным контекстом, используйте NewRequestWithContext и DefaultClient.Do.
func Post
func Post(url, contentType string, body io.Reader) (resp *Response, err error)
Post выполняет POST на указанный URL.
Вызывающая сторона должна закрыть resp.Body, когда закончит читать из него.
Если предоставленное тело является io.Closer, оно будет закрыто после выполнения запроса.
Post является оберткой вокруг DefaultClient.Post.
Чтобы установить пользовательские заголовки, используйте NewRequest и DefaultClient.Do.
Подробнее о том, как обрабатываются перенаправления, смотрите документацию по методу Client.Do.
Чтобы выполнить запрос с указанным контекстом (context.Context), используйте NewRequestWithContext и DefaultClient.Do.
func PostForm
func PostForm(url string, data url.Values) (resp *Response, err error)
PostForm выполняет POST-запрос на указанный URL, с ключами и значениями данных в URL-кодировке в качестве тела запроса.
Заголовок Content-Type имеет значение application/x-www-form-urlencoded. Чтобы установить другие заголовки, используйте NewRequest и DefaultClient.Do.
Когда err равен nil, resp всегда содержит ненулевое resp.Body. Вызывающая сторона должна закрыть resp.Body, когда закончит чтение из него.
PostForm - это обертка вокруг DefaultClient.PostForm.
Подробнее о том, как обрабатываются перенаправления, смотрите документацию метода Client.Do.
Чтобы выполнить запрос с указанным контекстом (context.Context), используйте NewRequestWithContext и DefaultClient.Do.
func ReadResponse
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)
ReadResponse считывает и возвращает HTTP-ответ от r. Параметр req опционально указывает запрос, который соответствует этому ответу. Если параметр nil, предполагается, что это GET-запрос. Клиенты должны вызвать resp.Body.Close после завершения чтения resp.Body. После этого вызова клиенты могут просмотреть resp.Trailer, чтобы найти пары ключ/значение, включенные в трейлер ответа.
func (*Response) Cookies
func (r *Response) Cookies() []*Cookie
Cookies разбирает и возвращает cookie, установленные в заголовках Set-Cookie.
func (*Response) Location
func (r *Response) Location() (*url.URL, error)
Location возвращает URL заголовка «Location» ответа, если он присутствует. Относительные перенаправления разрешаются относительно [Response.Request]. Возвращается ErrNoLocation, если заголовок Location отсутствует.
func (*Response) ProtoAtLeast
func (r *Response) ProtoAtLeast(major, minor int) bool
ProtoAtLeast сообщает, является ли протокол HTTP, используемый в ответе, хотя бы мажорным и минорным.
func (*Response) Write
func (r *Response) Write(w io.Writer) error
Write записывает r в w в формате ответа сервера HTTP/1.x, включая строку состояния, заголовки, тело и необязательный трейлер.
Этот метод обрабатывает следующие поля ответа r:
StatusCode
ProtoMajor
ProtoMinor
Request.Method
TransferEncoding
Trailer
Body
ContentLength
Header, значения для неканонических ключей будут иметь непредсказуемое поведение
Тело ответа закрывается после отправки.
type ResponseController
type ResponseController struct {
// содержит отфильтрованные или неэкспонированные поля
}
ResponseController используется HTTP-обработчиком для управления ответом.
ResponseController не может быть использован после возврата метода [Handler.ServeHTTP].
func NewResponseController
func NewResponseController(rw ResponseWriter) *ResponseController
NewResponseController создает ResponseController для запроса.
ResponseWriter должен быть исходным значением, переданным в метод [Handler.ServeHTTP], или иметь метод Unwrap, возвращающий исходный ResponseWriter.
Если ResponseWriter реализует любой из следующих методов, ResponseController будет вызывать их по мере необходимости:
Flush()
FlushError() error // альтернативный вариант Flush, возвращающий ошибку
Hijack() (net.Conn, *bufio.ReadWriter, error)
SetReadDeadline(deadline time.Time) error
SetWriteDeadline(deadline time.Time) ошибка
EnableFullDuplex() error
Если ResponseWriter не поддерживает метод, ResponseController возвращает ошибку, соответствующую ErrNotSupported.
func (*ResponseController) EnableFullDuplex
func (c *ResponseController) EnableFullDuplex() error
EnableFullDuplex указывает, что обработчик запроса будет чередовать чтение из [Request.Body] с записью в ResponseWriter.
Для запросов HTTP/1 HTTP-сервер Go по умолчанию потребляет всю непрочитанную часть тела запроса перед началом записи ответа, что не позволяет обработчикам одновременно читать из запроса и записывать ответ. Вызов EnableFullDuplex отключает это поведение и позволяет обработчикам продолжать читать из запроса, одновременно записывая ответ.
Для запросов HTTP/2 HTTP-сервер Go всегда разрешает одновременное чтение и запись ответа.
func (*ResponseController) Flush
func (c *ResponseController) Flush() error
Flush сбрасывает буферизованные данные клиенту.
func (*ResponseController) Hijack
func (c *ResponseController) Hijack() (net.Conn, *bufio.ReadWriter, error)
Hijack позволяет вызывающей стороне взять на себя управление соединением. Подробности см. в интерфейсе Hijacker.
func (*ResponseController) SetReadDeadline
func (c *ResponseController) SetReadDeadline(deadline time.Time) error
SetReadDeadline устанавливает крайний срок для чтения всего запроса, включая тело. Чтение из тела запроса после превышения установленного срока вернет ошибку. Нулевое значение означает отсутствие дедлайна.
Установка крайнего срока чтения после того, как он был превышен, не продлевает его.
func (*ResponseController) SetWriteDeadline
func (c *ResponseController) SetWriteDeadline(deadline time.Time) error
SetWriteDeadline устанавливает крайний срок для написания ответа. Запись в тело ответа после превышения установленного срока не блокируется, но может быть успешной, если данные были забуферизированы. Нулевое значение означает отсутствие крайнего срока.
Установка крайнего срока записи после его превышения не приведет к его продлению.
type ResponseWriter
type ResponseWriter interface {
// Header возвращает карту заголовков, которая будет отправлена
// [ResponseWriter.WriteHeader]. Карта [Header] также является механизмом, с помощью которого
// реализации [Handler] могут устанавливать HTTP-трейлеры.
//
// Изменение карты заголовков после вызова [ResponseWriter.WriteHeader] (или
// [ResponseWriter.Write]) не имеет никакого эффекта, если только код состояния HTTP не был класса
// 1xx или измененные заголовки являются трейлерами.
//
// Существует два способа установки трейлеров. Предпочтительным является.
// заранее объявить в заголовках, какие трейлеры вы будете впоследствии
// отправлять, устанавливая в заголовке «Trailer» имена
// ключей трейлеров, которые появятся позже. В этом случае эти
// ключи карты заголовков рассматриваются так, как если бы они были
// трейлерами. См. пример. Второй способ - для трейлеров.
// ключей, не известных [Обработчику] до первого [ResponseWriter.Write],
// является префикс ключей карты [Header] с помощью константного значения [TrailerPrefix]
// константным значением.
//
// Чтобы подавить автоматические заголовки ответа (например, «Дата»), установите
// их значение в nil.
Header() Header
// Write записывает данные в соединение как часть HTTP-ответа.
//
// Если функция [ResponseWriter.WriteHeader] еще не была вызвана, Write вызывает
// WriteHeader(http.StatusOK) перед записью данных. Если заголовок
// не содержит строки Content-Type, Write добавляет набор Content-Type
// к результату передачи начальных 512 байт записанных данных в
// [DetectContentType]. Кроме того, если общий размер всех записанных
// данных меньше нескольких КБ и нет вызовов Flush, то
// заголовок Content-Length добавляется автоматически.
//
// В зависимости от версии протокола HTTP и клиента, вызов
// Write или WriteHeader может предотвратить последующее чтение // Request.Body.
// Request.Body. Для запросов HTTP/1.x обработчики должны считывать любые // необходимые данные тела запроса перед записью ответа.
// необходимые данные тела запроса перед записью ответа. После того как
// заголовки будут смыты (либо из-за явного вызова Flusher.Flush
// вызова или записи достаточного количества данных, чтобы вызвать флэш), тело запроса
// может быть недоступно. Для запросов HTTP/2 HTTP-сервер Go разрешает // обработчикам продолжать считывать // тело запроса.
// обработчикам продолжать читать тело запроса, одновременно // записывая ответ.
// записи ответа. Однако такое поведение может поддерживаться не // всеми клиентами HTTP/2.
// всеми клиентами HTTP/2. Обработчики должны читать перед записью, если
// по возможности, чтобы обеспечить максимальную совместимость.
Write([]byte) (int, error)
// WriteHeader отправляет заголовок ответа HTTP с указанным
// кодом состояния.
//
// Если WriteHeader не вызывается явно, то первый вызов Write
// вызовет неявный WriteHeader(http.StatusOK).
// Таким образом, явные вызовы WriteHeader используются в основном для.
// отправки кодов ошибок или информационных ответов 1xx.
//
// Предоставленный код должен быть действительным кодом состояния HTTP 1xx-5xx.
// Может быть записано любое количество заголовков 1xx, за которыми следует не более
// один заголовок 2xx-5xx. Заголовки 1xx отправляются немедленно, но заголовки 2xx-5xx
// заголовки могут буферизироваться. Используйте интерфейс Flusher для отправки
// буферизованных данных. Карта заголовков очищается при отправке заголовков 2xx-5xx.
// при отправке заголовков 2xx-5xx, но не при отправке заголовков 1xx.
//
// Сервер автоматически отправляет заголовок 100 (Continue)
// при первом чтении из тела запроса, если в запросе есть
// заголовок «Expect: 100-continue».
WriteHeader(statusCode int)
}
Интерфейс ResponseWriter используется HTTP-обработчиком для создания HTTP-ответа.
ResponseWriter не может быть использован после возврата [Handler.ServeHTTP].
Пример трейлеры
HTTP-трейлеры — это набор пар «ключ/значение», подобных заголовкам, которые следуют после HTTP-ответа, а не перед ним.
package main
import (
"io"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/sendstrailers", func(w http.ResponseWriter, req *http.Request) {
// Before any call to WriteHeader or Write, declare
// the trailers you will set during the HTTP
// response. These three headers are actually sent in
// the trailer.
w.Header().Set("Trailer", "AtEnd1, AtEnd2")
w.Header().Add("Trailer", "AtEnd3")
w.Header().Set("Content-Type", "text/plain; charset=utf-8") // normal header
w.WriteHeader(http.StatusOK)
w.Header().Set("AtEnd1", "value 1")
io.WriteString(w, "This HTTP response has both headers before this text and trailers at the end.\n")
w.Header().Set("AtEnd2", "value 2")
w.Header().Set("AtEnd3", "value 3") // These will appear as trailers.
})
}
type RoundTripper
type RoundTripper interface {
// RoundTrip выполняет одну HTTP-транзакцию, возвращая
// ответ на предоставленный запрос.
//
// RoundTrip не должен пытаться интерпретировать ответ. В
// частности, RoundTrip должен вернуть err == nil, если он получил
// ответ, независимо от кода состояния ответа HTTP.
// Ненулевое значение err должно быть зарезервировано на случай неудачи в получении
// ответа. Аналогично, RoundTrip не должен пытаться.
// обрабатывать детали протокола более высокого уровня, такие как перенаправления,
// аутентификация или cookies.
//
// RoundTrip не должен модифицировать запрос, кроме как.
// потребления и закрытия тела запроса. RoundTrip может.
// читать поля запроса в отдельной горутине. Вызывающие стороны
// не должны мутировать или повторно использовать запрос до тех пор, пока не будет закрыто тело ответа
// Body не будет закрыто.
//
// RoundTrip должен всегда закрывать тело, в том числе при ошибках,
// но в зависимости от реализации может делать это в отдельной
// горутине даже после возвращения RoundTrip. Это означает, что
// вызывающие стороны, желающие повторно использовать тело для последующих запросов
// должны дождаться вызова Close, прежде чем сделать это.
//
// Поля URL и Header запроса должны быть инициализированы.
RoundTrip(*Request) (*Response, error)
}
RoundTripper - это интерфейс, представляющий возможность выполнения одной HTTP-транзакции с получением ответа на заданный запрос.
RoundTripper должен быть безопасным для одновременного использования несколькими горутинами.
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Таймаут: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
DefaultTransport является реализацией Transport по умолчанию и используется DefaultClient. Он устанавливает сетевые соединения по мере необходимости и кэширует их для повторного использования при последующих вызовах. Он использует HTTP-прокси по указанию переменных окружения HTTP_PROXY, HTTPS_PROXY и NO_PROXY (или их строчных версий).
func NewFileTransport
func NewFileTransport(fs FileSystem) RoundTripper
NewFileTransport возвращает новый RoundTripper, обслуживающий предоставленную FileSystem. Возвращаемый RoundTripper игнорирует URL host в своих входящих запросах, а также большинство других свойств запроса.
Типичным случаем использования NewFileTransport является регистрация «файлового» протокола на транспорте, как показано ниже:
t := &http.Transport{}
t.RegisterProtocol(«file», http.NewFileTransport(http.Dir(«/»)))
c := &http.Client{Transport: t}
res, err := c.Get(«file:///etc/passwd»)
...
func NewFileTransportFS
func NewFileTransportFS(fsys fs.FS) RoundTripper
NewFileTransportFS возвращает новый RoundTripper, обслуживающий предоставленную файловую систему fsys. Возвращаемый RoundTripper игнорирует URL host в своих входящих запросах, а также большинство других свойств запроса. Файлы, предоставляемые fsys, должны реализовывать io.Seeker.
Типичным случаем использования NewFileTransportFS является регистрация «файлового» протокола в транспорте, как показано ниже:
fsys := os.DirFS(«/»)
t := &http.Transport{}
t.RegisterProtocol(«file», http.NewFileTransportFS(fsys))
c := &http.Client{Transport: t}
res, err := c.Get(«file:///etc/passwd»)
...
type SameSite
SameSite позволяет серверу определить атрибут cookie, делающий невозможным для браузера отправку этого cookie вместе с межсайтовыми запросами. Основная цель - снизить риск утечки межсайтовой информации и обеспечить некоторую защиту от атак подделки межсайтовых запросов.
Подробности см. на сайте https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00.
const (
SameSiteDefaultMode SameSite = iota + 1
SameSiteLaxMode
SameSiteStrictMode
SameSiteNoneMode
)
type ServeMux
type ServeMux struct {
// содержит отфильтрованные или неотфильтрованные поля
}
ServeMux - это мультиплексор HTTP-запросов. Он сопоставляет URL каждого входящего запроса со списком зарегистрированных шаблонов и вызывает обработчик шаблона, который наиболее точно соответствует URL.
Шаблоны
Шаблоны могут соответствовать методу, хосту и пути запроса. Некоторые примеры:
- «/index.html» соответствует пути „/index.html“ для любого хоста и метода.
- «GET /static/» соответствует GET-запросу, путь которого начинается с „/static/“.
- «example.com/» соответствует любому запросу к хосту „example.com“.
- «example.com/{$}» соответствует запросам с хостом „example.com“ и путем „/“.
- «/b/{bucket}/o/{objectname...}» соответствует путям, первый сегмент которых - „b“, а третий - „o“. Имя «bucket» обозначает второй сегмент, а «objectname» - оставшуюся часть пути.
В общем случае шаблон выглядит следующим образом
Все три части необязательны; «/» является допустимым шаблоном. Если присутствует METHOD, за ним должен следовать хотя бы один пробел или табуляция.
Буквальные (т. е. не содержащие символы дикого символа) части шаблона соответствуют соответствующим частям запроса с учетом регистра.
Шаблон без метода соответствует любому методу. Шаблон с методом GET соответствует запросам GET и HEAD. В противном случае метод должен совпадать в точности.
Шаблон без хоста соответствует всем хостам. Шаблон с указанием хоста соответствует URL-адресам только на этом хосте.
Путь может включать сегменты подстановочных знаков вида {NAME} или {NAME…}. Например, «/b/{bucket}/o/{objectname…}». Имя подстановочного знака должно быть действительным идентификатором Go. Подстановочные знаки должны быть полными сегментами пути: им должна предшествовать косая черта, а за ней следовать либо косая черта, либо конец строки. Например, «/b_{bucket}» не является правильным шаблоном.
Обычно подстановочный знак соответствует только одному сегменту пути, заканчивающемуся следующим буквенным слешем (не %2F) в URL-адресе запроса. Но если присутствует «…», то подстановочный знак соответствует оставшейся части пути URL, включая косые черты. (Поэтому подстановочный знак «…» не должен появляться нигде, кроме конца шаблона). Соответствие для подстановочного знака можно получить, вызвав Request.PathValue с именем подстановочного знака. Косая черта в пути действует как анонимный подстановочный знак «…».
Специальный подстановочный знак {$} совпадает только с концом URL. Например, шаблон «/{$}» соответствует только пути «/», в то время как шаблон «/» соответствует всем путям.
Для сопоставления пути шаблонов и пути входящих запросов сегментно разгруппировываются. Так, например, путь «/a%2Fb/100%25» рассматривается как состоящий из двух сегментов, «a/b» и «100%». Шаблон «/a%2fb/» соответствует ему, а шаблон «/a/b/» - нет.
Приоритет
Если запросу соответствуют два или более шаблонов, то приоритет имеет наиболее специфичный шаблон. Шаблон P1 более специфичен, чем P2, если P1 соответствует строгому подмножеству запросов P2; то есть, если P2 соответствует всем запросам P1 и более. Если ни один из шаблонов не является более специфичным, то они конфликтуют. Из этого правила есть одно исключение для обратной совместимости: если два шаблона в противном случае будут конфликтовать, но один из них имеет хост, а другой - нет, то приоритет имеет шаблон с хостом. Если шаблон, переданный в ServeMux.Handle или ServeMux.HandleFunc, конфликтует с другим шаблоном, который уже зарегистрирован, эти функции паникуют.
В качестве примера общего правила можно привести «/images/thumbnails/», который является более конкретным, чем «/images/», поэтому оба шаблона могут быть зарегистрированы. Первый соответствует путям, начинающимся с «/images/thumbnails/», а второй будет соответствовать любому другому пути в поддереве «/images/».
В качестве другого примера рассмотрим шаблоны «GET /» и «/index.html»: оба соответствуют GET-запросу на «/index.html», но первый шаблон соответствует всем остальным GET- и HEAD-запросам, а второй - любому запросу на «/index.html», который использует другой метод. Шаблоны конфликтуют.
Перенаправление по следам косой черты
Рассмотрим ServeMux с обработчиком для поддерева, зарегистрированного с использованием слэша в конце или подстановочного символа «…». Если ServeMux получает запрос на корень поддерева без скрепки, он перенаправляет запрос, добавляя скрепку. Это поведение можно отменить, зарегистрировав отдельный путь без косой черты или подстановочного знака «…». Например, регистрация «/images/» заставляет ServeMux перенаправлять запрос «/images» на «/images/», если только «/images» не был зарегистрирован отдельно.
Санирование запросов
ServeMux также заботится о дезинфекции пути запроса URL и заголовка Host, удаляя номер порта и перенаправляя любой запрос, содержащий сегменты . или … или повторяющиеся косые черты, на эквивалентный, более чистый URL. Сбегающие элементы пути, такие как «%2e» для «.» и «%2f» для «/», сохраняются и не считаются разделителями при маршрутизации запроса.
Совместимость
Синтаксис шаблонов и поведение ServeMux при сопоставлении значительно изменились в Go 1.22. Чтобы восстановить старое поведение, установите переменную окружения GODEBUG в значение «httpmuxgo121=1». Эта настройка считывается один раз, при запуске программы; изменения во время выполнения будут проигнорированы.
Изменения, несовместимые с предыдущими, включают:
- Дикие символы в 1.21 - это обычные литеральные сегменты пути. Например, шаблон «/{x}» будет соответствовать только этому пути в 1.21, но будет соответствовать любому односегментному пути в 1.22.
- В 1.21 ни один шаблон не отклонялся, если он не был пустым или не конфликтовал с существующим шаблоном. В 1.22 синтаксически некорректные шаблоны приводят к панике ServeMux.Handle и ServeMux.HandleFunc. Например, в 1.21 шаблоны «/{» и «/a{x}» совпадают сами по себе, но в 1.22 они недействительны и при регистрации вызовут панику.
- В 1.22 каждый сегмент шаблона разгруппировывается; в 1.21 этого не делалось. Например, в 1.22 шаблон «/%61» соответствует пути «/a» («%61» - управляющая последовательность URL для «a»), а в 1.21 он соответствовал бы только пути «/%2561» (где «%25» - управляющая последовательность для знака процента).
- При сопоставлении шаблонов с путями в версии 1.22 каждый сегмент пути раскрывается, а в версии 1.21 раскрывается весь путь. Это изменение в основном влияет на то, как обрабатываются пути с эскейпами %2F, расположенными рядом со слешами. Подробности см. на https://go.dev/issue/21955.
func NewServeMux
func NewServeMux() *ServeMux
NewServeMux выделяет и возвращает новый ServeMux.
func (*ServeMux) Handle
func (mux *ServeMux) Handle(pattern string, handler Handler)
Handle регистрирует обработчик для заданного шаблона. Если заданный шаблон конфликтует с уже зарегистрированным, Handle паникует.
Пример
package main
import (
"fmt"
"net/http"
)
type apiHandler struct{}
func (apiHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}
func main() {
mux := http.NewServeMux()
mux.Handle("/api/", apiHandler{})
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// The "/" pattern matches everything, so we need to check
// that we're at the root here.
if req.URL.Path != "/" {
http.NotFound(w, req)
return
}
fmt.Fprintf(w, "Welcome to the home page!")
})
}
func (*ServeMux) HandleFunc
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
HandleFunc регистрирует функцию-обработчик для заданного шаблона. Если заданный шаблон конфликтует с уже зарегистрированным, HandleFunc паникует.
func (*ServeMux) Handler
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
Handler возвращает обработчик, который будет использоваться для данного запроса, консультируясь с r.Method, r.Host и r.URL.Path. Всегда возвращается ненулевой обработчик. Если путь не является каноническим, то обработчик будет внутренне сгенерированным обработчиком, который перенаправляет на канонический путь. Если хост содержит порт, он игнорируется при подборе обработчиков.
Путь и хост используются без изменений для запросов CONNECT.
Обработчик также возвращает зарегистрированный шаблон, соответствующий запросу, или, в случае внутренне сгенерированных перенаправлений, путь, который будет соответствовать после выполнения перенаправления.
Если для запроса не существует зарегистрированного обработчика, Handler возвращает обработчик «страница не найдена» и пустой шаблон.
func (*ServeMux) ServeHTTP
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
ServeHTTP отправляет запрос обработчику, чей шаблон наиболее точно соответствует URL запроса.
type Server
type Server struct {
// Addr опционально указывает TCP-адрес сервера для прослушивания,
// в форме «host:port». Если он пуст, то используется «:http» (порт 80).
// Имена служб определены в RFC 6335 и назначены IANA.
// Подробнее о формате адресов см. в разделе net.Dial.
Addr string
Handler Handler // обработчик для вызова, http.DefaultServeMux, если nil
// DisableGeneralOptionsHandler, если true, передает запросы «OPTIONS *» обработчику,
// в противном случае отвечает 200 OK и Content-Length: 0.
DisableGeneralOptionsHandler bool
// TLSConfig опционально предоставляет конфигурацию TLS для использования
// функциями ServeTLS и ListenAndServeTLS. Обратите внимание, что это значение
// клонируется ServeTLS и ListenAndServeTLS, поэтому не
// невозможно изменить конфигурацию с помощью методов типа
// tls.Config.SetSessionTicketKeys. Чтобы использовать
// SetSessionTicketKeys, используйте Server.Serve с TLS-слушателем
// вместо него.
TLSConfig *tls.Config
// ReadTimeout - максимальная продолжительность чтения всего // запроса.
// запроса, включая тело. Нулевое или отрицательное значение означает.
// что таймаута не будет.
//
// Поскольку ReadTimeout не позволяет обработчикам принимать по каждому запросу
// решения о допустимом сроке выполнения или // скорости загрузки каждого тела запроса, большинство пользователей предпочтут использовать ReadTimeout.
// скорости загрузки, большинство пользователей предпочтут использовать
// ReadHeaderTimeout. Вполне допустимо использовать их оба.
ReadTimeout time.Duration
// ReadHeaderTimeout - это количество времени, разрешенное для чтения
// заголовков запросов. Срок чтения соединения сбрасывается.
// после чтения заголовков, и обработчик может решить, что
// считается слишком медленным для тела запроса. Если значение равно нулю, используется значение
// ReadTimeout используется. Если отрицательно, или если ноль и ReadTimeout
// равно нулю или отрицательно, то таймаут не используется.
ReadHeaderTimeout time.Duration
// WriteTimeout - максимальная продолжительность до завершения тайминга
// записи ответа. Она сбрасывается каждый раз, когда считывается новый
// чтении заголовка запроса. Как и ReadTimeout, он не // позволяет обработчикам принимать решения.
// позволяет обработчикам принимать решения на основе каждого запроса.
// Нулевое или отрицательное значение означает, что таймаута не будет.
WriteTimeout time.Duration
// IdleTimeout - это максимальное время ожидания
// следующего запроса, когда включены keep-alives. Если значение равно нулю, то используется значение
// из ReadTimeout используется. Если отрицательно, или если ноль и ReadTimeout
// равно нулю или отрицательно, таймаут не используется.
IdleTimeout time.Duration
// MaxHeaderBytes управляет максимальным количеством байтов, которые
// сервер будет читать, разбирая ключи и // значения заголовка запроса, включая строку запроса.
// значения, включая строку запроса. Оно не ограничивает
// размер тела запроса.
// Если значение равно нулю, используется значение DefaultMaxHeaderBytes.
MaxHeaderBytes int
// TLSNextProto опционально указывает функцию, которая принимает на себя
// владение предоставленным TLS-соединением, когда происходит обновление протокола ALPN
// произошло обновление протокола. Ключом карты является протокол.
// согласованное имя. Аргумент Handler должен использоваться для.
// обработки HTTP-запросов и инициализирует TLS запроса
// и RemoteAddr, если они еще не заданы. Соединение
// автоматически закрывается при возврате функции.
// Если TLSNextProto не равно nil, поддержка HTTP/2 не включается
// автоматически.
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
// ConnState задает необязательную функцию обратного вызова, которая
// вызывается при изменении состояния клиентского соединения. См.
// Тип ConnState и связанные с ним константы для получения подробной информации.
ConnState func(net.Conn, ConnState)
// ErrorLog задает необязательный регистратор ошибок при принятии // соединения, неожиданного поведения обработчиков.
// соединения, неожиданного поведения обработчиков и
// базовых ошибок файловой системы.
// Если nil, то протоколирование осуществляется через стандартный логгер пакета log.
ErrorLog *log.Logger
// BaseContext опционально указывает функцию, которая возвращает
// базовый контекст для входящих запросов на этом сервере.
// Предоставленный слушатель - это конкретный слушатель, который
// собирается начать принимать запросы.
// Если BaseContext равен nil, по умолчанию используется context.Background().
// Если не nil, то должен быть возвращен не nil контекст.
BaseContext func(net.Listener) context.Context
// ConnContext опционально задает функцию, которая изменяет
// контекст, используемый для нового соединения c. Предоставляемый ctx
// является производным от базового контекста и имеет ServerContextKey
// значение.
ConnContext func(ctx context.Context, c net.Conn) context.Context
// HTTP2 настраивает HTTP/2 соединения.
//
// Это поле пока не имеет никакого эффекта.
// См. https://go.dev/issue/67813.
HTTP2 *HTTP2Config
// Протоколы - это набор протоколов, принимаемых сервером.
//
// Если Protocols включает UnencryptedHTTP2, сервер будет принимать
// незашифрованные соединения HTTP/2. Сервер может обслуживать как
// HTTP/1 и незашифрованный HTTP/2 на одном и том же адресе и порту.
//
// Если значение Protocols равно nil, по умолчанию обычно используются HTTP/1 и HTTP/2.
// Если TLSNextProto не имеет значения nil и не содержит записи «h2»,
// по умолчанию используется только HTTP/1.
Protocols *Protocols
// содержит отфильтрованные или неэкспонированные поля
}
Server определяет параметры для запуска HTTP-сервера. Нулевое значение для Server является допустимой конфигурацией.
func (*Server) Close
func (s *Server) Close() error
Close немедленно закрывает все активные net.Listeners и все соединения в состоянии StateNew, StateActive или StateIdle. Для изящного завершения работы используйте Server.Shutdown.
Close не пытается закрыть (и даже не знает об этом) любые перехваченные соединения, такие как WebSockets.
Close возвращает любую ошибку, возникшую при закрытии базового слушателя(ей) сервера.
func (*Server) ListenAndServe
func (s *Server) ListenAndServe() error
ListenAndServe прослушивает сетевой TCP-адрес s.Addr и затем вызывает Serve для обработки запросов на входящих соединениях. Принимаемые соединения настроены на включение TCP keep-alives.
Если s.Addr пуст, используется «:http».
ListenAndServe всегда возвращает ошибку, отличную от нуля. После Server.Shutdown или Server.Close возвращаемая ошибка будет ErrServerClosed.
func (*Server) ListenAndServeTLS
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error
ListenAndServeTLS прослушивает сетевой TCP-адрес s.Addr и затем вызывает ServeTLS для обработки запросов на входящих TLS-соединениях. Принимаемые соединения настроены на включение TCP keep-alives.
Если не заполнены ни TLSConfig.Certificates, ни TLSConfig.GetCertificate сервера, необходимо предоставить файлы, содержащие сертификат и соответствующий закрытый ключ для сервера. Если сертификат подписан центром сертификации, certFile должен представлять собой объединение сертификата сервера, всех промежуточных сертификатов и сертификата центра сертификации.
Если s.Addr пуст, используется «:https».
ListenAndServeTLS всегда возвращает ошибку, отличную от нуля. После Server.Shutdown или Server.Close возвращаемая ошибка - ErrServerClosed.
func (*Server) RegisterOnShutdown
func (s *Server) RegisterOnShutdown(f func())
RegisterOnShutdown регистрирует функцию для вызова на Server.Shutdown. Она может использоваться для изящного завершения соединений, которые подверглись обновлению протокола ALPN или были перехвачены. Эта функция должна запускать специфическое для данного протокола изящное отключение, но не должна ждать завершения отключения.
func (*Server) Serve
func (s *Server) Serve(l net.Listener) error
Serve принимает входящие соединения на слушателе l, создавая для каждого новую сервисную горутину. Сервисные программы читают запросы и затем вызывают s.Handler для ответа на них.
Поддержка HTTP/2 включена только в том случае, если Слушатель возвращает соединения *tls.Conn и они были настроены на «h2» в TLS Config.NextProtos.
Serve всегда возвращает ненулевую ошибку и закрывает l. После Server.Shutdown или Server.Close возвращаемая ошибка будет ErrServerClosed.
func (*Server) ServeTLS
func (s *Server) ServeTLS(l net.Listener, certFile, keyFile string) error
ServeTLS принимает входящие соединения на слушателе l, создавая для каждого новую сервисную горутину. Сервисные программы выполняют настройку TLS, а затем считывают запросы, вызывая s.Handler для ответа на них.
Если не заполнены ни TLSConfig.Certificates, ни TLSConfig.GetCertificate, ни config.GetConfigForClient сервера, необходимо предоставить файлы, содержащие сертификат и соответствующий закрытый ключ для сервера. Если сертификат подписан центром сертификации, файл certFile должен быть конкатенацией сертификата сервера, всех промежуточных сертификатов и сертификата центра сертификации.
ServeTLS всегда возвращает ошибку, отличную от нуля. После Server.Shutdown или Server.Close возвращаемая ошибка будет ErrServerClosed.
func (*Server) SetKeepAlivesEnabled
func (s *Server) SetKeepAlivesEnabled(v bool)
SetKeepAlivesEnabled управляет тем, включены ли HTTP keep-alives. По умолчанию keep-alives всегда включены. Только очень ограниченные ресурсы или серверы, находящиеся в процессе выключения, должны отключать их.
func (*Server) Shutdown
func (s *Server) Shutdown(ctx context.Context) error
Выключение изящно отключает сервер, не прерывая активных соединений. Shutdown работает, сначала закрывая все открытые слушатели, затем закрывая все простаивающие соединения, а затем ожидая неопределенное время, пока соединения не вернутся в состояние простоя, и затем выключается. Если предоставленный контекст истекает до завершения выключения, Shutdown возвращает ошибку контекста, в противном случае возвращается любая ошибка, возникшая при закрытии базового слушателя (слушателей) сервера.
Когда вызывается Shutdown, Serve, ListenAndServe и ListenAndServeTLS немедленно возвращают ErrServerClosed. Убедитесь, что программа не завершается, а ждет возврата Shutdown.
Shutdown не пытается закрыть или дождаться перехваченных соединений, таких как WebSockets. Вызывающая Shutdown программа должна отдельно уведомить такие долгоживущие соединения о закрытии и дождаться их закрытия, если это необходимо. Способ регистрации функций уведомления о выключении см. в Server.RegisterOnShutdown.
После вызова Shutdown на сервере его нельзя использовать повторно; будущие вызовы таких методов, как Serve, будут возвращать ErrServerClosed.
Пример
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
)
func main() {
var srv http.Server
idleConnsClosed := make(chan struct{})
go func() {
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt)
<-sigint
// We received an interrupt signal, shut down.
if err := srv.Shutdown(context.Background()); err != nil {
// Error from closing listeners, or context timeout:
log.Printf("HTTP server Shutdown: %v", err)
}
close(idleConnsClosed)
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
// Error starting or closing listener:
log.Fatalf("HTTP server ListenAndServe: %v", err)
}
<-idleConnsClosed
}
type Transport
type Transport struct {
// Proxy определяет функцию для возврата прокси для данного
// запроса. Если функция возвращает ошибку, отличную от нуля, то
// запрос будет прерван с указанной ошибкой.
//
// Тип прокси определяется схемой URL. «http»,
// «https», «socks5» и «socks5h» поддерживаются. Если схема пуста,
// предполагается «http».
// «socks5» обрабатывается так же, как и «socks5h».
//
// Если URL прокси содержит подкомпонент userinfo,
// в прокси-запросе будут переданы имя пользователя и пароль
// в заголовке Proxy-Authorization.
//
// Если Proxy равно nil или возвращает nil *URL, прокси не используется.
Proxy func(*Request) (*url.URL, error)
// OnProxyConnectResponse вызывается, когда транспорт получает HTTP-ответ от
// прокси для запроса CONNECT. Он вызывается перед проверкой ответа 200 OK.
// Если он возвращает ошибку, запрос завершается с этой ошибкой.
OnProxyConnectResponse func(ctx context.Context, proxyURL *url.URL, connectReq *Request, connectRes *Response) error
// DialContext задает функцию dial для создания незашифрованных TCP-соединений.
// Если DialContext равен nil (и устаревшая Dial ниже также равна nil),
// то транспорт набирает номер, используя пакет net.
//
// DialContext работает параллельно с вызовами RoundTrip.
// Вызов RoundTrip, инициирующий набор номера, может в конечном итоге использовать
// соединение, набранное ранее, когда предыдущее соединение
// становится нерабочим до завершения последующего DialContext.
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// Dial определяет функцию dial для создания незашифрованных TCP-соединений.
//
// Dial выполняется одновременно с вызовами RoundTrip.
// Вызов RoundTrip, инициирующий набор, может в конечном итоге использовать
// соединение, набранное ранее, если предыдущее соединение
// становится нерабочим до завершения последующего Dial.
//
// Исправлено: Вместо этого используйте DialContext, который позволяет транспорту
// отменять дозвоны, как только они больше не нужны.
// Если оба значения установлены, приоритет имеет DialContext.
Dial func(network, addr string) (net.Conn, error)
// DialTLSContext задает необязательную функцию набора номера для создания
// TLS-соединений для непроксированных HTTPS-запросов.
//
// Если DialTLSContext равен nil (и устаревшая DialTLS ниже также равна nil),
// используются DialContext и TLSClientConfig.
//
// Если DialTLSContext установлен, крючки Dial и DialContext не используются для HTTPS
// запросы, а параметры TLSClientConfig и TLSHandshakeTimeout
// игнорируются. Предполагается, что возвращаемый net.Conn уже // прошел квитирование TLS.
// прошло рукопожатие TLS.
DialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
// DialTLS определяет необязательную функцию набора номера для создания
// TLS-соединений для непроксированных HTTPS-запросов.
//
// Исправлено: Вместо этого используйте DialTLSContext, который позволяет транспорту
// отменять циклы, как только они больше не нужны.
// Если оба значения установлены, приоритет имеет DialTLSContext.
DialTLS func(network, addr string) (net.Conn, error)
// TLSClientConfig задает конфигурацию TLS для использования с
// tls.Client.
// Если nil, используется конфигурация по умолчанию.
// Если не nil, то поддержка HTTP/2 может быть не включена по умолчанию.
TLSClientConfig *tls.Config
// TLSHandshakeTimeout задает максимальное количество времени.
// ожидание рукопожатия TLS. Ноль означает отсутствие таймаута.
TLSHandshakeTimeout time.Duration
// DisableKeepAlives, если true, отключает HTTP keep-alives и
// будет использовать соединение с сервером только для одного
// HTTP-запроса.
//
// Это не связано с аналогичным названием TCP keep-alives.
DisableKeepAlives bool
// DisableCompression, если значение равно true, запрещает транспорту
// запрашивать сжатие с помощью заголовка запроса «Accept-Encoding: gzip»
// заголовком запроса, если запрос не содержит существующего
// значение Accept-Encoding. Если Транспорт запрашивает gzip самостоятельно
// самостоятельно и получает gzip-ответ, то он прозрачно
// декодируется в Response.Body. Однако если пользователь
// явно запросил gzip, он не будет автоматически // распакован.
// не сжимается.
DisableCompression bool
// MaxIdleConns контролирует максимальное количество простаивающих (keep-alive)
// соединений на всех хостах. Ноль означает отсутствие ограничения.
MaxIdleConns int
// MaxIdleConnsPerHost, если ненулевое значение, контролирует максимальное количество простаивающих
// (keep-alive) соединений для каждого хоста. Если ноль,
// используется значение по умолчаниюMaxIdleConnsPerHost.
MaxIdleConnsPerHost int
// MaxConnsPerHost опционально ограничивает общее количество
// соединений на хост, включая соединения в состоянии дозвона,
// активном и неактивном состояниях. При нарушении лимита соединения будут блокироваться.
//
// Ноль означает отсутствие ограничения.
MaxConnsPerHost int
// IdleConnTimeout - это максимальное количество времени, в течение которого неработающее
// (keep-alive) соединение будет простаивать перед закрытием
// само.
// Ноль означает отсутствие ограничений.
IdleConnTimeout time.Duration
// ResponseHeaderTimeout, если ненулевое значение, определяет количество
// времени для ожидания заголовков ответа сервера после полной
// записи запроса (включая его тело, если таковое имеется). Это
// время не включает время на чтение тела ответа.
ResponseHeaderTimeout time.Duration
// ExpectContinueTimeout, если ненулевое значение, определяет количество
// времени для ожидания первых заголовков ответа сервера после полной
// записи заголовков запроса, если запрос содержит
// заголовок «Expect: 100-continue». Ноль означает отсутствие таймаута и
// приводит к тому, что тело запроса отправляется немедленно, без
// ожидания одобрения сервера.
// Это время не включает время отправки заголовка запроса.
ExpectContinueTimeout time.Duration
// TLSNextProto определяет, как транспорт переключается на.
// альтернативному протоколу (например, HTTP/2) после TLS ALPN
// согласования протокола. Если транспорт набирает TLS-соединение
// с непустым именем протокола и TLSNextProto содержит
// запись в карте для этого ключа (например, «h2»), то функция
// вызывается с полномочиями запроса (например, «example.com»
// или «example.com:1234») и TLS-соединением. Функция
// должна возвращать RoundTripper, который затем обрабатывает запрос.
// Если TLSNextProto не равно nil, поддержка HTTP/2 не включается
// автоматически.
TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper
// ProxyConnectHeader опционально указывает заголовки, которые следует отправлять на
// прокси во время запросов CONNECT.
// Чтобы динамически установить заголовок, смотрите GetProxyConnectHeader.
ProxyConnectHeader Header
// GetProxyConnectHeader опционально указывает функцию для возврата
// заголовки для отправки в proxyURL во время запроса CONNECT к
// ip:port target.
// Если она возвращает ошибку, то раундтрип транспорта завершается с ошибкой.
// этой ошибкой. Он может вернуть (nil, nil), чтобы не добавлять заголовки.
// Если GetProxyConnectHeader не равен nil, ProxyConnectHeader
// игнорируется.
GetProxyConnectHeader func(ctx context.Context, proxyURL *url.URL, target string) (Header, error)
// MaxResponseHeaderBytes задает ограничение на то, сколько
// байтов в ответе сервера
// заголовок.
//
// Ноль означает использование ограничения по умолчанию.
MaxResponseHeaderBytes int64
// WriteBufferSize определяет размер буфера записи, используемого
// при записи на транспорт.
// Если ноль, то используется значение по умолчанию (в настоящее время 4 КБ).
WriteBufferSize int
// ReadBufferSize задает размер буфера чтения, используемого
// при чтении с транспорта.
// Если ноль, то используется значение по умолчанию (в настоящее время 4 КБ).
ReadBufferSize int
// ForceAttemptHTTP2 определяет, будет ли включен HTTP/2 при ненулевом значении
// Dial, DialTLS, или DialContext func или TLSClientConfig.
// По умолчанию использование любого из этих полей консервативно отключает HTTP/2.
// Чтобы использовать пользовательские конфигурации дозвона или TLS и при этом пытаться HTTP/2
// обновления, установите это значение в true.
ForceAttemptHTTP2 bool
// HTTP2 настраивает HTTP/2 соединения.
//
// Это поле пока не имеет никакого эффекта.
// См. https://go.dev/issue/67813.
HTTP2 *HTTP2Config
// Протоколы - это набор протоколов, поддерживаемых транспортом.
//
// Если Protocols включает UnencryptedHTTP2 и не включает HTTP1,
// транспорт будет использовать незашифрованный HTTP/2 для запросов к URL http://.
//
// Если Protocols равно nil, то по умолчанию обычно используется только HTTP/1.
// Если ForceAttemptHTTP2 равен true, или если TLSNextProto содержит запись «h2»,
// по умолчанию используются HTTP/1 и HTTP/2.
Protocols *Protocols
// содержит отфильтрованные или неэкспонированные поля
}
Transport - это реализация RoundTripper, поддерживающая HTTP, HTTPS и HTTP-прокси (для HTTP или HTTPS с CONNECT).
По умолчанию Transport кэширует соединения для последующего повторного использования. Это может оставить много открытых соединений при доступе ко многим хостам. Таким поведением можно управлять с помощью метода Transport.CloseIdleConnections и полей [Transport.MaxIdleConnsPerHost] и [Transport.DisableKeepAlives].
Транспорты следует использовать повторно, а не создавать по мере необходимости. Транспорты безопасны для одновременного использования несколькими горутинами.
Транспорт - это низкоуровневый примитив для выполнения HTTP- и HTTPS-запросов. Для высокоуровневой функциональности, такой как куки и редиректы, смотрите раздел Клиент.
Транспорт использует HTTP/1.1 для HTTP URL и либо HTTP/1.1, либо HTTP/2 для HTTPS URL, в зависимости от того, поддерживает ли сервер HTTP/2, и как настроен транспорт. DefaultTransport поддерживает HTTP/2. Чтобы явно включить HTTP/2 на транспорте, установите [Transport.Protocols].
Ответы с кодами состояния в диапазоне 1xx либо обрабатываются автоматически (100 expect-continue), либо игнорируются. Исключением является код состояния HTTP 101 (Switching Protocols), который считается терминальным статусом и возвращается Transport.RoundTrip. Чтобы увидеть проигнорированные ответы 1xx, используйте ClientTrace.Got1xxResponse пакета трассировки httptrace.
Transport повторяет запрос при возникновении сетевой ошибки только в том случае, если соединение уже было успешно использовано и если запрос является идемпотентным и либо не имеет тела, либо определено его [Request.GetBody]. HTTP-запросы считаются идемпотентными, если они имеют HTTP-методы GET, HEAD, OPTIONS или TRACE; или если их карта заголовков содержит запись «Idempotency-Key» или «X-Idempotency-Key». Если значение ключа idempotency является фрагментом нулевой длины, запрос рассматривается как idempotent, но заголовок не передается по проводам.
func (*Transport) CancelRequest deprecated
func (*Transport) Clone
func (t *Transport) Clone() *Transport
Clone возвращает глубокую копию экспортированных полей t.
func (*Transport) CloseIdleConnections
func (t *Транспорт) CloseIdleConnections()
CloseIdleConnections закрывает все соединения, которые были ранее подключены в результате предыдущих запросов, но теперь простаивают в состоянии «keep-alive». Она не прерывает используемые в данный момент соединения.
func (*Transport) RegisterProtocol
func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper)
RegisterProtocol регистрирует новый протокол с помощью схемы. Транспорт будет передавать запросы, использующие заданную схему, в rt. Ответственность rt заключается в имитации семантики HTTP-запросов.
RegisterProtocol может быть использован другими пакетами для обеспечения реализаций схем протоколов типа «ftp» или «file».
Если rt.RoundTrip возвращает ErrSkipAltProtocol, транспорт будет сам обрабатывать Transport.RoundTrip для этого одного запроса, как если бы протокол не был зарегистрирован.
func (*Transport) RoundTrip
func (t *Transport) RoundTrip(req *Request) (*Response, error)
RoundTrip реализует интерфейс RoundTripper.
Для более высокоуровневой поддержки HTTP-клиентов (например, обработка cookies и перенаправлений) смотрите Get, Post и тип Client.
Как и в интерфейсе RoundTripper, типы ошибок, возвращаемых RoundTrip, не определены.
4.15.4 - Описание пакета для управления маршрутизацией github.com/julienschmidt/httprouter для GO
HttpRouter — это легкий высокопроизводительный маршрутизатор HTTP-запросов (также называемый мультиплексором или просто мультиплексором) для Go.
HttpRouter
HttpRouter — это легковесный высокопроизводительный маршрутизатор HTTP-запросов (также называемый мультиплексором или просто “mux”) для Go.
В отличие от стандартного мультиплексора пакета net/http в Go, этот роутер поддерживает:
- Переменные в шаблонах маршрутов
- Соответствие HTTP-методам запроса
- Лучшую масштабируемость
Роутер оптимизирован для высокой производительности и малого потребления памяти. Эффективно работает даже с очень длинными путями и большим количеством маршрутов благодаря использованию сжимающей динамической trie-структуры (радиксное дерево).
Ключевые особенности
1. Только точные совпадения
В отличие от других роутеров (например, http.ServeMux), где URL может соответствовать нескольким шаблонам (с приоритетами типа “самое длинное совпадение” или “первый зарегистрированный — первый обработанный”), здесь запрос может соответствовать только одному маршруту или ни одному. Это исключает неожиданные совпадения, что полезно для SEO и UX.
2. Автоматическая обработка слэшей
Роутер автоматически перенаправляет при отсутствии или избытке trailing slash (косой черты в конце пути), если для нового пути есть обработчик. Можно отключить.
3. Коррекция пути
Дополнительные возможности:
- Исправление регистра символов (например, для
CAPTAIN CAPS LOCK)
- Удаление избыточных элементов (
../, //)
- Case-insensitive поиск с редиректом
4. Параметры в путях
Динамические сегменты пути (например, /user/:id) извлекаются без ручного парсинга URL. Реализовано с минимальными накладными расходами.
5. Zero Garbage
Процесс сопоставления и диспетчеризации не создает мусора (zero bytes allocation). Единственные выделения памяти:
- Создание slice для параметров пути
- Создание контекста и объекта запроса (только в стандартном Handler API)
- В 3-аргументном API при отсутствии параметров — аллокаций нет вообще.
6. Максимальная производительность
Реализация оптимизирована (см. бенчмарки). Используется радиксное дерево для эффективного сопоставления длинных путей.
7. Обработка паник
Можно установить PanicHandler для перехвата паник во время обработки запроса. Роутер восстановит работу и отправит клиенту ошибку.
8. Идеально для API
- Поощряет построение RESTful API с иерархической структурой
- Нативная поддержка OPTIONS-запросов
- Автоматические ответы
405 Method Not Allowed
- Возможность кастомизации
NotFound и MethodNotAllowed обработчиков
- Поддержка статических файлов
Технические детали
Используется радиксное дерево (trie) с компрессией для:
- Эффективного хранения длинных путей
- Быстрого поиска даже при тысячах маршрутов
- Минимизации использования памяти
Возможности
Именованные параметры
Как видно из примеров, :name представляет собой именованный параметр. Его значения доступны через httprouter.Params — это срез (slice) параметров. Получить значение можно двумя способами:
- По индексу в срезе
- Через метод
ByName(name): например, параметр :name извлекается вызовом ByName("name")
Важно:
При использовании стандартного http.Handler (через router.Handler или http.HandlerFunc) вместо 3-аргументного API HttpRouter, именованные параметры хранятся в контексте запроса (request.Context). Подробнее см. раздел «Почему это не работает с http.Handler?».
Особенности именованных параметров:
- Соответствуют только одному сегменту пути
Пример шаблона: /user/:user
| Путь |
Совпадение |
/user/gordon |
✅ |
/user/you |
✅ |
/user/gordon/profile |
❌ |
/user/ |
❌ |
Ограничение:
Так как роутер использует только явные совпадения, нельзя зарегистрировать одновременно статический маршрут и параметр для одного сегмента. Например, эти шаблоны не могут сосуществовать для одного HTTP-метода:
/user/new (статический)
/user/:user (параметр)
При этом маршрутизация для разных методов запроса (GET, POST и т.д.) обрабатывается независимо.
Параметры Catch-All (перехват всего)
Имеют формат *name и, как следует из названия, соответствуют любому пути. Всегда должны находиться в конце шаблона.
Пример шаблона: /src/*filepath
| Путь |
Совпадение |
/src/ |
✅ |
/src/somefile.go |
✅ |
/src/subdir/somefile.go |
✅ |
Особенности:
- Перехватывают все оставшиеся сегменты пути, включая слэши
- Полезны для реализации «обработчиков-прокси» (например, статических файлов в подкаталогах)
Как это работает?
Роутер использует древовидную структуру, активно использующую общие префиксы — по сути, это компактное префиксное дерево (радиксное дерево). Узлы с общим префиксом имеют общего родителя. Вот пример структуры дерева для GET-запросов:
Приоритет Путь Обработчик
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>
Здесь каждый *<num> представляет адрес обработчика (указатель на функцию). При прохождении пути от корня до листа формируется полный маршрут, например \blog\:post\, где :post — это параметр-заполнитель. В отличие от хэш-таблиц, дерево позволяет работать с динамическими параметрами, поскольку сопоставление происходит по шаблонам, а не через сравнение хэшей. Бенчмарки подтверждают эффективность этого подхода.
Оптимизации структуры:
- Иерархичность URL:
Ограниченный набор символов в путях URL создает множество общих префиксов, что позволяет декомпозировать задачу маршрутизации на меньшие подзадачи.
- Раздельные деревья для методов:
Для каждого HTTP-метода (GET, POST и т.д.) строится отдельное дерево. Это:
- Экономит память (не требует хранения
map[метод]->обработчик в каждом узле).
- Сокращает пространство поиска до актуального метода.
- Приоритизация узлов:
Дочерние узлы сортируются по приоритету — количеству обработчиков в поддеревьях. Это дает:
- Быстрый доступ к популярным маршрутам.
- Приоритетную обработку длинных путей (с максимальной «стоимостью»).
Визуализация приоритетов:
├------------
├---------
├-----
├----
├--
├--
└-
Почему это не работает с http.Handler?
Работает! Роутер сам реализует интерфейс http.Handler и предоставляет адаптеры для интеграции стандартных http.Handler и http.HandlerFunc в качестве httprouter.Handle.
Доступ к параметрам:
Для http.Handler именованные параметры доступны через контекст запроса:
func Hello(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
fmt.Fprintf(w, "hello, %s!\n", params.ByName("name"))
}
Альтернативный вариант:
params := r.Context().Value(httprouter.ParamsKey)
Автоматические OPTIONS-ответы и CORS
Для кастомизации автоматических ответов на OPTIONS-запросы (например, для поддержки CORS preflight или добавления заголовков) используйте обработчик Router.GlobalOPTIONS:
router.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Если это CORS preflight запрос
if r.Header.Get("Access-Control-Request-Method") != "" {
header := w.Header()
// Разрешаем методы из заголовка Allow
header.Set("Access-Control-Allow-Methods", r.Header.Get("Allow"))
// Открываем доступ для всех доменов
header.Set("Access-Control-Allow-Origin", "*")
}
// Возвращаем статус 204 No Content
w.WriteHeader(http.StatusNoContent)
})
Где найти Middleware X?
HttpRouter — это исключительно высокопроизводительный роутер с минималистичным функционалом. Поскольку он реализует интерфейс http.Handler, вы можете:
- Цеплять любые совместимые middleware перед роутером (например, из библиотеки Gorilla).
- Создавать собственные middleware — это достаточно просто.
- Использовать фреймворки на основе HttpRouter.
Мультидомены и поддомены
Пример реализации маршрутизации для разных доменов/поддоменов:
// HostSwitch - карта для хранения обработчиков по доменам
type HostSwitch map[string]http.Handler
// Реализуем интерфейс http.Handler
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Ищем обработчик для текущего хоста
if handler := hs[r.Host]; handler != nil {
handler.ServeHTTP(w, r)
} else {
http.Error(w, "Forbidden", http.StatusForbidden) // Или редирект
}
}
func main() {
// Инициализация роутера
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
// Настройка HostSwitch
hs := make(HostSwitch)
hs["example.com:12345"] = router // Для основного домена
hs["api.example.com:12345"] = anotherRouter // Для поддомена
// Запуск сервера
log.Fatal(http.ListenAndServe(":12345", hs))
}
Ключевые моменты:
- Каждый домен/поддомен может иметь собственный роутер
- Порт указывается вместе с доменом (
example.com:12345)
- Для необрабатываемых доменов возвращается 403 Forbidden (можно заменить на редирект)
Базовая аутентификация (Basic Auth)
Пример реализации HTTP Basic Authentication (RFC 2617) для обработчиков:
package main
import (
"fmt"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)
// BasicAuth - middleware для проверки учетных данных
func BasicAuth(h httprouter.Handle, requiredUser, requiredPassword string) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Получаем учетные данные из заголовка
user, password, hasAuth := r.BasicAuth()
if hasAuth && user == requiredUser && password == requiredPassword {
// Если аутентификация успешна - передаем запрос обработчику
h(w, r, ps)
} else {
// Иначе запрашиваем аутентификацию
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
}
}
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Открытая зона!\n")
}
func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Защищенная зона!\n")
}
func main() {
user := "gordon"
pass := "secret!"
router := httprouter.New()
router.GET("/", Index)
router.GET("/protected/", BasicAuth(Protected, user, pass))
log.Fatal(http.ListenAndServe(":8080", router))
}
Цепочка обработчиков через NotFound
Примечание: Для корректной работы может потребоваться отключить Router.HandleMethodNotAllowed.
Вы можете использовать другой http.Handler (например, дополнительный роутер) для обработки запросов, которые не были найдены основным роутером:
router.NotFound = anotherRouter
Обслуживание статических файлов
Обработчик NotFound можно использовать для раздачи статических файлов из корневого пути /:
// Раздаем файлы из директории ./public
router.NotFound = http.FileServer(http.Dir("public"))
Однако такой подход нарушает строгие правила маршрутизации. Рекомендуется:
- Использовать выделенные подпути:
router.ServeFiles("/static/*filepath", http.Dir("public"))
- Или явно задавать маршруты:
router.GET("/files/*filepath", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join("public", ps.ByName("filepath")))
})
Веб-фреймворки на основе HttpRouter
Если HttpRouter кажется вам слишком минималистичным, рассмотрите эти высокоуровневые сторонние фреймворки, построенные на его основе:
Популярные решения
-
Gin
API в стиле Martini с значительно лучшей производительностью.
Особенности: Middleware-цепочки, JSON-валидация, маршрутизация без рефлексии.
-
Ace (устаревший, но исторически значимый)
Один из первых быстрых фреймворков для Go.
Преемник: Перешел в Gin.
-
api2go
Полноценная реализация JSON API с поддержкой JSON:API спецификации.
Использование: Создание RESTful API с CRUD-операциями.
Для специфичных задач
-
Goat
Минималистичный REST API сервер.
Философия: “Меньше кода — больше производительности”.
-
Hitch
Интегрирует HttpRouter с контекстом и middleware.
Ключевая особенность: Простота связывания компонентов.
-
Kami
Работает через x/net/context.
Для чего: Создание масштабируемых приложений с пробросом контекста.
-
Siesta
Композиция HTTP-обработчиков с поддержкой контекста.
Паттерн: “Middleware как сервисы”.
Специализированные
-
Medeina
Вдохновлен Ruby-фреймворками Roda и Cuba.
Подход: Древовидная маршрутизация.
-
pbgo
Мини-фреймворк для RPC/REST на основе Protobuf.
Сценарии: Микросервисы с gRPC-like API.
-
xmux
Форк HttpRouter с поддержкой net/context.
Отличие: Наследует производительность с расширенным API.
Для продакшена
-
Hikaru
Поддержка standalone-режима и Google App Engine.
Плюсы: Кроссплатформенность.
-
River
Упрощенный REST-сервер для быстрого прототипирования.
Фишка: Нулевая настройка для базовых сценариев.
-
httpway
Добавляет middleware-цепочки и graceful shutdown.
Особенность: Совместимость с native HTTP-пакетами.
Как выбрать?
- Для API: Gin, api2go, pbgo
- Микросервисы: Siesta, xmux
- Минимализм: Goat, River
- Унаследованные проекты: Ace (переход на Gin)
Использование
Это всего лишь краткое введение, подробности смотрите в GoDoc. Давайте начнем с простого примера:
package main
import (
"fmt"
"net/http"
"log"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
log.Fatal(http.ListenAndServe(":8080", router))
}
Принцип работы маршрутизации
Роутер сопоставляет входящие запросы по HTTP-методу и пути. Если для комбинации метод-путь зарегистрирован обработчик, запрос передается соответствующей функции. Для стандартных методов существуют сокращенные функции регистрации:
router.GET("/path", handler) // GET
router.POST("/path", handler) // POST
router.PUT("/path", handler) // PUT
router.PATCH("/path", handler) // PATCH
router.DELETE("/path", handler) // DELETE
Для других методов используйте универсальный метод:
router.Handle("OPTIONS", "/path", handler)
Типы параметров пути
| Синтаксис |
Тип |
:name |
Именованный параметр |
*name |
Catch-all параметр |
1. Именованные параметры
Динамические сегменты пути. Соответствуют любому значению до следующего / или конца пути.
Шаблон:
/blog/:category/:post
Примеры соответствия:
| Запрос |
Совпадение |
Параметры |
/blog/go/request-routers |
✅ |
category="go", post="request-routers" |
/blog/go/request-routers/ |
❌ (редирект) |
- |
/blog/go/ |
❌ |
- |
/blog/go/request-routers/comments |
❌ |
- |
2. Catch-all параметры
Соответствуют любой части пути до конца, включая /. Всегда должны быть последним элементом.
Шаблон:
/files/*filepath
Примеры соответствия:
| Запрос |
Совпадение |
Параметры |
/files/ |
✅ |
filepath="/" |
/files/LICENSE |
✅ |
filepath="/LICENSE" |
/files/templates/article.html |
✅ |
filepath="/templates/article.html" |
/files |
❌ (редирект) |
- |
Работа с параметрами
Параметры хранятся в срезе структур Param (ключ-значение) и передаются обработчику третьим аргументом:
func Handler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Получение значения по имени
user := ps.ByName("user") // для :user или *user
// Получение по индексу (с доступом к имени параметра)
param := ps[2]
key := param.Key // имя 3-го параметра
value := param.Value // значение 3-го параметра
}
Особенности:
- Доступ к параметрам за O(1) по имени (использует внутреннюю оптимизацию)
- Индексный доступ полезен при обработке неизвестных параметров
- Параметры автоматически URL-decoded
Важные нюансы
-
Редиректы:
- Для путей с
/ в конце роутер автоматически делает редирект на каноничную версию
- Можно отключить через
router.RedirectTrailingSlash = false
-
Кодировка:
// Пример значения с спецсимволами
// Запрос: /search/%D1%82%D0%B5%D1%81%D1%82
query := ps.ByName("query") // автоматически декодируется в "тест"
-
Производительность:
- Параметры не аллоцируют память при отсутствии в пути
- Используется slice pooling для повторного использования структур Param
Переменные
var ParamsKey = paramsKey{}
ParamsKey — это ключ контекста запроса, под которым хранятся параметры URL.
Функции
CleanPath
func CleanPath(p string) string
CleanPath — это URL-версия функции path.Clean. Она возвращает канонизированный путь, удаляя элементы . и ...
Правила обработки (применяются итеративно до полной обработки):
-
Слэши
Заменяет множественные слэши на один: /// → /
-
Текущая директория (.)
Удаляет элементы .:
/././path → /path
-
Родительская директория (..)
Удаляет комбинации ../ с предыдущим не-.. элементом:
/a/b/../c → /a/c
/a/b/../../c → /c
-
Корневые ..
Заменяет /.. в начале пути на /:
/../a → /a
-
Пустой результат
Если после обработки путь пуст, возвращает /:
`` → /
Примеры:
CleanPath("//foo///bar") // "/foo/bar"
CleanPath("/./foo/../bar") // "/bar"
CleanPath("/../") // "/"
CleanPath("") // "/"
Особенности:
- Не выполняет URL-декодирование (работает с уже декодированным путем)
- Сохраняет регистр символов
- Полезен для нормализации путей перед маршрутизацией
Технические детали
-
Использование в роутере:
HttpRouter автоматически применяет CleanPath к входящим запросам перед сопоставлением маршрутов.
-
Безопасность:
Защищает от path traversal атак:
// Запрос "/secret/../../etc/passwd" будет преобразован в "/etc/passwd",
// но роутер обработает его только если есть явный маршрут
-
Производительность:
Реализация использует zero-allocation алгоритм для минимизации нагрузк
Типы
type Handle
type Handle func(http.ResponseWriter, *http.Request, Params)
Handle — функция, которая может быть зарегистрирована для обработки HTTP-запросов. Аналог http.HandlerFunc, но с третьим параметром для значений параметров URL.
type Param (добавлено в v1.1.0)
type Param struct {
Key string // Ключ параметра
Value string // Значение параметра
}
Param представляет отдельный параметр URL, состоящий из ключа и значения.
type Params (добавлено в v1.1.0)
Params — это срез параметров Param, возвращаемый роутером. Срез упорядочен: первый параметр URL соответствует первому элементу среза. Безопасен для доступа по индексу.
Методы:
ParamsFromContext (добавлено в v1.2.0)
func ParamsFromContext(ctx context.Context) Params
Извлекает параметры URL из контекста запроса. Возвращает nil, если параметров нет.
ByName (добавлено в v1.1.0)
func (ps Params) ByName(name string) string
Возвращает значение первого параметра с указанным ключом. Если параметр не найден, возвращает пустую строку.
type Router
type Router struct {
// Автоматический редирект при несовпадении пути,
// но наличии обработчика для пути с/без завершающего слэша.
// Пример: /foo/ → /foo (код 301 для GET, 307 для других методов)
RedirectTrailingSlash bool
// Автокоррекция пути при отсутствии обработчика:
// 1. Удаляет избыточные элементы (../, //)
// 2. Поиск без учета регистра
// 3. Редирект на исправленный путь (301/307)
RedirectFixedPath bool
// Проверка допустимых методов при неудачной маршрутизации.
// Если включено, отправляет 405 Method Not Allowed с заголовком Allow.
HandleMethodNotAllowed bool
// Автоматическая обработка OPTIONS-запросов.
// Пользовательские обработчики OPTIONS имеют приоритет.
HandleOPTIONS bool
// Глобальный обработчик для автоматических OPTIONS-запросов.
// Вызывается только если HandleOPTIONS=true и нет специфичного обработчика.
GlobalOPTIONS http.Handler
// Обработчик для ненайденных маршрутов (по умолчанию — http.NotFound).
NotFound http.Handler
// Обработчик для недопустимых методов (код 405).
// По умолчанию — http.Error с StatusMethodNotAllowed.
MethodNotAllowed http.Handler
// Обработчик паник (код 500).
// Предотвращает аварийное завершение сервера.
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
// содержит скрытые или неэкспортируемые поля
}
Router реализует интерфейс http.Handler и предоставляет конфигурируемую систему маршрутизации.
Ключевые особенности
-
Гибкость обработчиков:
Handle поддерживает параметры через Params
- Совместимость со стандартными
http.Handler/http.HandlerFunc
-
Безопасность:
ParamsFromContext для безопасного доступа к параметрам из middleware
PanicHandler для обработки критических ошибок
-
Производительность:
- Срезы
Params избегают аллокаций памяти
- Прямой доступ к параметрам по индексу (
O(1))
Пример использования параметров:
router.GET("/user/:id", func(w http.ResponseWriter, r *http.Request, ps Params) {
id := ps.ByName("id") // или ps[0].Value
})
Методы типа Router
func New() *Router
Создает и возвращает новый инициализированный роутер. Автокоррекция путей (включая обработку trailing slashes) включена по умолчанию.
Методы-сокращения для HTTP-методов
| Метод |
Описание |
DELETE(path string, handle Handle) |
Сокращение для router.Handle(http.MethodDelete, path, handle) |
GET(path string, handle Handle) |
Сокращение для GET-запросов |
HEAD(path string, handle Handle) (v1.1.0+) |
Сокращение для HEAD-запросов |
OPTIONS(path string, handle Handle) (v1.1.0+) |
Сокращение для OPTIONS-запросов |
PATCH(path string, handle Handle) |
Сокращение для PATCH-запросов |
POST(path string, handle Handle) |
Сокращение для POST-запросов |
PUT(path string, handle Handle) |
Сокращение для PUT-запросов |
func (r *Router) DELETE(path string, handle Handle)
func (r *Router) GET(path string, handle Handle)
Пример:
router := httprouter.New()
router.GET("/users", listUsers)
router.POST("/users", createUser)
func (*Router) Handle(method, path string, handle Handle)
Регистрирует обработчик для указанного HTTP-метода и пути.
router.Handle("PROPFIND", "/resource", handleResource)
func (*Router) Handler(method, path string, handler http.Handler)
Адаптер для использования стандартного http.Handler. Параметры доступны через контекст:
router.Handler("GET", "/user/:id", customHandler)
// В обработчике:
params := httprouter.ParamsFromContext(r.Context())
func (*Router) HandlerFunc(method, path string, handler http.HandlerFunc)
Адаптер для http.HandlerFunc:
router.HandlerFunc("GET", "/", indexHandler)
func (*Router) Lookup(method, path string) (Handle, Params, bool)
Ручной поиск обработчика для комбинации метод+путь. Возвращает:
- Обработчик
- Параметры пути
- Флаг необходимости редиректа (добавить/убрать trailing slash)
Пример использования в middleware:
if handle, ps, _ := router.Lookup(r.Method, r.URL.Path); handle != nil {
// Кастомная обработка
}
func (*Router) ServeFiles(path string, root http.FileSystem)
Обслуживает статические файлы из указанной файловой системы. Путь должен содержать /*filepath в конце:
// Доступ к /var/www/file.txt по URL /src/file.txt
router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
Особенности:
- Использует стандартный
http.FileServer
- Для 404 ошибок применяется
http.NotFound, а не роутерский NotFound-обработчик
func (*Router) ServeHTTP(w http.ResponseWriter, req *http.Request)
Позволяет роутеру удовлетворять интерфейсу http.Handler:
http.ListenAndServe(":8080", router)
Особенности использования
-
Порядок регистрации:
Нет приоритета “первый зарегистрированный — первый обработанный”. Каждый путь+метод соответствует ровно одному обработчику.
-
Производительность:
Все методы регистрации используют единую оптимизированную логику:
// Эти вызовы эквивалентны по производительности
router.GET("/path", handler)
router.Handle("GET", "/path", handler)
-
Безопасность:
ServeFiles автоматически защищает от directory traversal атак:
// Запрос `/src/../../../etc/passwd` будет отклонен
4.16 - Пакет OAuth2 языка программирования Go
Пакет oauth2 содержит реализацию клиента для спецификации OAuth 2.0.
sequenceDiagram
participant ClientApp
participant User
participant AuthServer
participant ResourceServer
%% Регистрация нового пользователя
Note over ClientApp: 1. Создание нового пользователя
ClientApp->>AuthServer: POST /register (не часть OAuth)
AuthServer-->>ClientApp: client_id, client_secret
%% Авторизация (Authorization Code Flow)
Note over ClientApp,User: 2. Авторизация пользователя
ClientApp->>User: Redirect to AuthURL (config.AuthCodeURL())
User->>AuthServer: Авторизация/согласие
AuthServer->>User: Redirect с code и state
User->>ClientApp: Передача code
ClientApp->>AuthServer: Exchange code на token (config.Exchange())
AuthServer-->>ClientApp: access_token, refresh_token
%% Использование токена
Note over ClientApp,ResourceServer: 3. Доступ к ресурсам
ClientApp->>ResourceServer: Запрос с токеном (transport)
ResourceServer-->>ClientApp: Данные пользователя
%% Другие поддерживаемые события
Note over ClientApp,AuthServer: 4. Другие сценарии
%% Refresh Token Flow
ClientApp->>AuthServer: Запрос нового токена (TokenSource.Token())
AuthServer-->>ClientApp: Новый access_token
%% Client Credentials Flow
ClientApp->>AuthServer: Запрос токена (clientcredentials.Config)
AuthServer-->>ClientApp: Сервисный токен
%% Device Flow
ClientApp->>AuthServer: Запрос device code (DeviceAuth())
AuthServer-->>ClientApp: user_code, verification_uri
User->>AuthServer: Ввод user_code
ClientApp->>AuthServer: Polling для токена (DeviceAccessToken())
AuthServer-->>ClientApp: Устройственный токен
Ключевые элементы модели:
-
Типы пакета OAuth2:
Config - основной объект конфигурации
Endpoint - URLs сервера авторизации
Token - содержит access/refresh токены
TokenSource - механизм обновления токенов
Transport - HTTP-транспорт с авторизацией
-
Основные методы:
AuthCodeURL() - генерация URL авторизации
Exchange() - обмен code на токен
Client() - создание авторизованного HTTP-клиента
TokenSource() - автоматическое обновление токенов
-
Поддерживаемые потоки:
- Authorization Code Flow (основной)
- Client Credentials Flow (для сервисов)
- Device Flow (для ТВ/IoT)
- Refresh Token Flow
-
Дополнительные компоненты:
- PKCE (защита от CSRF)
- Различные AuthStyle (методы аутентификации)
- Кастомные AuthCodeOption
Эта модель охватывает полный жизненный цикл OAuth 2.0 в Go-приложениях, от регистрации клиента до доступа к защищенным ресурсам.
Переменные
var HTTPClient internal.ContextKey
HTTPClient - это ключ контекста, используемый с context.WithValue для ассоциации *http.Client с контекстом.
var NoContext = context.TODO()
NoContext - контекст по умолчанию, который следует использовать, если не применяется собственный context.Context.
Устарело: Вместо этого используйте context.Background или context.TODO.
Функции
func GenerateVerifier
func GenerateVerifier() string
GenerateVerifier генерирует верификатор кода PKCE со случайными 32 октетами. Соответствует рекомендациям RFC 7636.
Новый верификатор должен генерироваться для каждой авторизации. Полученный верификатор следует передавать в Config.AuthCodeURL или Config.DeviceAuth с S256ChallengeOption, а в Config.Exchange или Config.DeviceAccessToken - с VerifierOption.
func NewClient
func NewClient(ctx context.Context, src TokenSource) *http.Client
NewClient создает *http.Client из context.Context и TokenSource. Возвращенный клиент действителен только в течение времени жизни контекста.
Примечание: если пользовательский *http.Client предоставлен через context.Context, он используется только для получения токена и не влияет на *http.Client, возвращаемый из NewClient.
Особый случай: если src равен nil, возвращается не-OAuth2 клиент с использованием предоставленного контекста.
Устарело.
func S256ChallengeFromVerifier
func S256ChallengeFromVerifier(verifier string) string
S256ChallengeFromVerifier возвращает challenge-код PKCE, полученный из верификатора методом S256.
Предпочтительнее использовать S256ChallengeOption, где это возможно.
Типы
type AuthCodeOption
type AuthCodeOption interface {
// содержит неэкспортируемые методы
}
AuthCodeOption передается в Config.AuthCodeURL.
Варианты:
var (
// AccessTypeOnline и AccessTypeOffline - параметры для Options.AuthCodeURL.
// Они изменяют поле "access_type" в URL, возвращаемом AuthCodeURL.
AccessTypeOnline AuthCodeOption = SetAuthURLParam("access_type", "online")
AccessTypeOffline AuthCodeOption = SetAuthURLParam("access_type", "offline")
// ApprovalForce требует от пользователя подтверждения запроса разрешений
ApprovalForce AuthCodeOption = SetAuthURLParam("prompt", "consent")
)
Объяснение AuthCodeOption
AuthCodeOption - это интерфейс, который позволяет настраивать параметры Authorization Code Flow в OAuth 2.0. Он используется для:
- Модификации URL авторизации
- Передачи дополнительных параметров на endpoint авторизации
- Контроля поведения OAuth-диалога
Основные варианты использования
1. Управление типом доступа
// Запрос online-токена (по умолчанию)
url := conf.AuthCodeURL("state", oauth2.AccessTypeOnline)
// Запрос offline-токена с refresh token
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
Различие:
AccessTypeOnline - токен действителен только во время текущей сессии
AccessTypeOffline - позволяет получить refresh token для долгоживущих доступов
2. Принудительное подтверждение разрешений
// Пользователь всегда увидит диалог подтверждения
url := conf.AuthCodeURL("state", oauth2.ApprovalForce)
Применение: Когда нужно гарантировать, что пользователь явно подтвердит запрашиваемые разрешения, даже если уже давал согласие ранее.
Дополнительные возможности
3. PKCE (защита от CSRF)
verifier := oauth2.GenerateVerifier()
url := conf.AuthCodeURL(
"state",
oauth2.AccessTypeOffline,
oauth2.S256ChallengeOption(verifier),
)
4. Кастомные параметры
// Добавление произвольных параметров
customParam := oauth2.SetAuthURLParam("custom_param", "value")
url := conf.AuthCodeURL("state", customParam)
Техническая реализация
Интерфейс реализуется через скрытые методы, а на практике используются:
SetAuthURLParam - для установки произвольных параметров
- Предопределенные варианты (AccessTypeOnline и др.)
- PKCE-реализации
Пример комплексного использования
func GetAuthURL(config *oauth2.Config) string {
verifier := oauth2.GenerateVerifier()
return config.AuthCodeURL(
"random-state-123",
oauth2.AccessTypeOffline, // Запрашиваем refresh token
oauth2.ApprovalForce, // Всегда показывать диалог подтверждения
oauth2.S256ChallengeOption(verifier), // Включение PKCE
oauth2.SetAuthURLParam("hd", "example.com"), // Ограничение домена
)
}
Ключевые преимущества:
- Гибкость настройки OAuth-потока
- Поддержка современных механизмов безопасности
- Единообразный API для всех параметров
- Возможность расширения функциональности
func S256ChallengeOption
func S256ChallengeOption(verifier string) AuthCodeOption
S256ChallengeOption создает PKCE challenge из верификатора методом S256. Должен передаваться только в Config.AuthCodeURL или Config.DeviceAuth.
func SetAuthURLParam
func SetAuthURLParam(key, value string) AuthCodeOption
SetAuthURLParam создает AuthCodeOption для передачи параметров key/value на endpoint авторизации провайдера.
func VerifierOption
func VerifierOption(verifier string) AuthCodeOption
VerifierOption возвращает AuthCodeOption с верификатором кода PKCE. Должен передаваться только в Config.Exchange или Config.DeviceAccessToken.
type AuthStyle
AuthStyle определяет способ аутентификации запросов токенов.
Варианты:
const (
// Автоопределение стиля аутентификации
AuthStyleAutoDetect AuthStyle = 0
// Передача client_id и client_secret в теле POST
AuthStyleInParams AuthStyle = 1
// Использование HTTP Basic Authorization OAuth2 RFC 6749 section 2.3.1.
AuthStyleInHeader AuthStyle = 2
)
Объяснение AuthStyle
AuthStyle определяет способ передачи учетных данных клиента (client_id и client_secret) при запросе токена в OAuth 2.0 flow. Это важный аспект безопасности, который влияет на:
- Способ передачи конфиденциальных данных
- Совместимость с различными OAuth-провайдерами
- Соответствие стандартам безопасности
Детальное описание вариантов
1. AuthStyleAutoDetect (значение 0)
AuthStyleAutoDetect AuthStyle = 0
Поведение:
- Автоматически определяет предпочтительный метод на основе:
- Ответа сервера
- Известных особенностей провайдера
- Пробует разные методы при необходимости
Пример использования:
endpoint := oauth2.Endpoint{
AuthURL: "https://provider.com/auth",
TokenURL: "https://provider.com/token",
AuthStyle: oauth2.AuthStyleAutoDetect,
}
Когда использовать: Когда провайдер поддерживает оба метода или его требования неизвестны.
2. AuthStyleInParams (значение 1)
AuthStyleInParams AuthStyle = 1
Поведение:
Пример использования:
endpoint := oauth2.Endpoint{
AuthURL: "https://provider.com/auth",
TokenURL: "https://provider.com/token",
AuthStyle: oauth2.AuthStyleInParams,
}
Когда использовать:
- Когда провайдер явно требует этот метод
- Для совместимости со старыми OAuth-реализациями
AuthStyleInHeader AuthStyle = 2
Поведение:
Пример использования:
endpoint := oauth2.Endpoint{
AuthURL: "https://provider.com/auth",
TokenURL: "https://provider.com/token",
AuthStyle: oauth2.AuthStyleInHeader,
}
Преимущества:
- Более безопасный метод (не попадает в логи)
- Рекомендуется стандартом OAuth 2.0
- Поддерживается большинством современных провайдеров
Практические рекомендации
-
Для публичных клиентов (SPA, мобильные приложения):
// Не используйте client_secret, только PKCE
conf := &oauth2.Config{
ClientID: "public_client_id",
Scopes: []string{"openid"},
Endpoint: oauth2.Endpoint{
AuthStyle: oauth2.AuthStyleInHeader, // Или AutoDetect
},
}
-
Для конфиденциальных клиентов (серверные приложения):
conf := &oauth2.Config{
ClientID: "client_id",
ClientSecret: "client_secret",
Scopes: []string{"api:read"},
Endpoint: oauth2.Endpoint{
AuthStyle: oauth2.AuthStyleInHeader, // Предпочтительно
},
}
-
При проблемах совместимости:
// Пробуем разные варианты
styles := []oauth2.AuthStyle{
oauth2.AuthStyleInHeader,
oauth2.AuthStyleInParams,
}
for _, style := range styles {
endpoint.AuthStyle = style
// Пробуем запрос токена
}
Соответствие стандартам
AuthStyleInHeader соответствует RFC 6749 section 2.3.1
AuthStyleInParams - устаревший, но широко поддерживаемый метод
AuthStyleAutoDetect - удобная обертка для максимальной совместимости
Выбор правильного AuthStyle критически важен для безопасности и работоспособности OAuth-интеграции.
type Config
type Config struct {
// ClientID - публичный идентификатор вашего приложения,
// выдается провайдером при регистрации OAuth-клиента
ClientID string
// ClientSecret - секретный ключ вашего приложения,
// должен храниться безопасно (не в клиентском коде)
ClientSecret string
// Endpoint - содержит URL-адреса сервера авторизации:
// AuthURL - endpoint для получения authorization code
// TokenURL - endpoint для обмена code на access token
Endpoint Endpoint
// RedirectURL - URL, на который провайдер перенаправит
// пользователя после авторизации. Должен точно совпадать
// с URL, зарегистрированным у провайдера
RedirectURL string
// Scopes - запрашиваемые области доступа (разрешения),
// определяют, к каким ресурсам будет доступ у приложения
Scopes []string
}
Config описывает стандартный 3-этапный OAuth2-поток с информацией о клиентском приложении и URL endpoint’ов сервера.
Объяснение Config
Структура Config является центральной конфигурацией для OAuth 2.0 Authorization Code Flow (3-legged OAuth). Она содержит все необходимые параметры для:
- Идентификации приложения (клиента)
- Настройки взаимодействия с OAuth-провайдером
- Управления процессом авторизации
Детальная расшифровка полей структуры
type Config struct {
// ClientID - публичный идентификатор вашего приложения,
// выдается провайдером при регистрации OAuth-клиента
ClientID string
// ClientSecret - секретный ключ вашего приложения,
// должен храниться безопасно (не в клиентском коде)
ClientSecret string
// Endpoint - содержит URL-адреса сервера авторизации:
// AuthURL - endpoint для получения authorization code
// TokenURL - endpoint для обмена code на access token
Endpoint Endpoint
// RedirectURL - URL, на который провайдер перенаправит
// пользователя после авторизации. Должен точно совпадать
// с URL, зарегистрированным у провайдера
RedirectURL string
// Scopes - запрашиваемые области доступа (разрешения),
// определяют, к каким ресурсам будет доступ у приложения
Scopes []string
}
Основные методы Config
1. AuthCodeURL - генерация URL для авторизации
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string
Пример использования:
// Базовый вариант
authURL := config.AuthCodeURL("random-state-123")
// С дополнительными параметрами
authURL := config.AuthCodeURL(
"state-xyz",
oauth2.AccessTypeOffline, // запросить refresh token
oauth2.ApprovalForce, // всегда показывать диалог подтверждения
oauth2.SetAuthURLParam("hd", "example.com"), // ограничение домена
)
2. Exchange - обмен authorization code на токен
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error)
Пример использования:
// Обработка callback'а с code
func handleCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := config.Exchange(r.Context(), code)
if err != nil {
// обработка ошибки
}
// использование токена...
}
3. Client - создание HTTP-клиента с авторизацией
func (c *Config) Client(ctx context.Context, t *Token) *http.Client
Пример использования:
// Создание клиента с автоматическим обновлением токенов
client := config.Client(context.Background(), token)
// Использование клиента для API-запросов
resp, err := client.Get("https://api.example.com/userinfo")
if err != nil {
// обработка ошибки
}
4. TokenSource - создание источника токенов
func (c *Config) TokenSource(ctx context.Context, t *Token) oauth2.TokenSource
Пример использования:
// Создание источника токенов с автоматическим обновлением
tokenSource := config.TokenSource(context.Background(), initialToken)
// Получение свежего токена
newToken, err := tokenSource.Token()
if err != nil {
// обработка ошибки
}
Полный пример использования
package main
import (
"context"
"fmt"
"log"
"net/http"
"golang.org/x/oauth2"
)
func main() {
config := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"user:read", "files:write"},
RedirectURL: "https://yourapp.com/callback",
Endpoint: oauth2.Endpoint{
AuthURL: "https://auth.example.com/authorize",
TokenURL: "https://auth.example.com/token",
},
}
// Шаг 1: Перенаправление пользователя на авторизацию
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
fmt.Printf("Перейдите по ссылке для авторизации:\n%v\n", authURL)
// Шаг 2: Обработка callback'а (эмулируем получение code)
var code string
fmt.Print("Введите полученный code: ")
if _, err := fmt.Scan(&code); err != nil {
log.Fatal(err)
}
// Шаг 3: Обмен code на токен
token, err := config.Exchange(context.Background(), code)
if err != nil {
log.Fatal(err)
}
// Шаг 4: Использование токена
client := config.Client(context.Background(), token)
resp, err := client.Get("https://api.example.com/user")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// Обработка ответа...
fmt.Println("Успешный запрос к API!")
}
Особенности и рекомендации
-
Безопасность:
- Никогда не храните ClientSecret в клиентском коде
- Всегда используйте HTTPS для RedirectURL
- Реализуйте проверку параметра state для защиты от CSRF
-
Жизненный цикл токенов:
- AccessToken обычно имеет ограниченное время жизни
- RefreshToken (если есть) позволяет получать новые AccessToken
- TokenSource автоматически обрабатывает обновление токенов
-
Для production-использования:
- Добавьте обработку ошибок на всех этапах
- Реализуйте хранение токенов между сессиями
- Добавьте PKCE для улучшенной безопасности
Тип Config предоставляет полный набор инструментов для реализации OAuth 2.0 Authorization Code Flow в Go-приложениях, охватывая все этапы процесса авторизации.
Пример
package main
import (
"context"
"fmt"
"log"
"golang.org/x/oauth2"
)
func main() {
ctx := context.Background()
conf := &oauth2.Config{
ClientID: "ВАШ_CLIENT_ID",
ClientSecret: "ВАШ_CLIENT_SECRET",
Scopes: []string{"ДОСТУП1", "ДОСТУП2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth", // URL авторизации
TokenURL: "https://provider.com/o/oauth2/token", // URL получения токена
},
}
// Используем PKCE для защиты от CSRF атак
// Спецификация: https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6
verifier := oauth2.GenerateVerifier() // Генерируем верификатор для PKCE
// Перенаправляем пользователя на страницу согласия для запроса разрешений
// на указанные выше области доступа (scopes)
url := conf.AuthCodeURL(
"state", // Уникальный state для защиты
oauth2.AccessTypeOffline, // Запрашиваем refresh-токен
oauth2.S256ChallengeOption(verifier), // Добавляем PKCE challenge
)
fmt.Printf("Перейдите по URL для авторизации: %v", url)
// Получаем authorization code из redirect URL.
// Exchange выполнит обмен кода на токен доступа.
// HTTP клиент от conf.Client будет автоматически обновлять токен.
var code string
if _, err := fmt.Scan(&code); err != nil {
log.Fatal(err)
}
// Обмениваем код на токен, передавая верификатор PKCE
tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier))
if err != nil {
log.Fatal(err)
}
// Создаем HTTP клиент с автоматическим обновлением токенов
client := conf.Client(ctx, tok)
client.Get("...") // Делаем авторизованные запросы
}
Пример CustomHTTP
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"golang.org/x/oauth2"
)
func main() {
ctx := context.Background()
// Конфигурация OAuth2 клиента
conf := &oauth2.Config{
ClientID: "ВАШ_CLIENT_ID", // Идентификатор клиента
ClientSecret: "ВАШ_CLIENT_SECRET", // Секретный ключ клиента
Scopes: []string{"ДОСТУП1", "ДОСТУП2"}, // Запрашиваемые разрешения
Endpoint: oauth2.Endpoint{
TokenURL: "https://provider.com/o/oauth2/token", // URL для получения токена
AuthURL: "https://provider.com/o/oauth2/auth", // URL для авторизации
},
}
// Перенаправляем пользователя на страницу авторизации для получения разрешений
// на указанные области доступа (scopes)
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline) // "state" для защиты от CSRF
fmt.Printf("Перейдите по ссылке для авторизации: %v", url)
// Получаем код авторизации из redirect URL.
// Exchange выполняет обмен кода на токен доступа.
// HTTP клиент, возвращаемый conf.Client, будет автоматически обновлять токен.
var code string
if _, err := fmt.Scan(&code); err != nil {
log.Fatal(err)
}
// Используем кастомный HTTP клиент с таймаутом 2 секунды для запроса токена
httpClient := &http.Client{Timeout: 2 * time.Second}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
// Получаем токен доступа
tok, err := conf.Exchange(ctx, code)
if err != nil {
log.Fatal(err)
}
// Создаем HTTP клиент с автоматической авторизацией
client := conf.Client(ctx, tok)
_ = client // Используйте client для авторизованных запросов
}
func (*Config) AuthCodeURL
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string
AuthCodeURL возвращает URL страницы согласия OAuth 2.0 провайдера, которая явно запрашивает разрешения для требуемых областей доступа (scopes).
Параметры:
-
state
Случайное значение (опaque), используемое клиентом для поддержания состояния между запросом и callback-вызовом. Сервер авторизации включает это значение при перенаправлении пользователя обратно в клиентское приложение.
-
opts (дополнительные опции)
Может включать:
AccessTypeOnline или AccessTypeOffline - тип доступа
ApprovalForce - принудительное подтверждение разрешений
Защита от CSRF-атак:
-
Рекомендуемый способ:
Включите PKCE challenge через S256ChallengeOption.
Примечание: Не все серверы поддерживают PKCE.
-
Альтернативный способ:
Генерация случайного параметра state с последующей проверкой после обмена токена.
Ссылки на стандарты:
func (*Config) Client
func (c *Config) Client(ctx context.Context, t *Token) *http.Client
Клиент возвращает HTTP-клиента, используя предоставленный токен. Токен будет автоматически обновляться по мере необходимости. Базовый HTTP-транспорт будет получен с использованием предоставленного контекста. Возвращенный клиент и его Transport не должны быть изменены.
func (*Config) DeviceAccessToken
func (c *Config) DeviceAccessToken(ctx context.Context, da *DeviceAuthResponse, opts ...AuthCodeOption) (*Token, error)
DeviceAccessToken опрашивает сервер для обмена device code на токен.
func (*Config) DeviceAuth
func (c *Config) DeviceAuth(ctx context.Context, opts ...AuthCodeOption) (*DeviceAuthResponse, error)
DeviceAuth возвращает структуру с device code для авторизации на другом устройстве.
Пример
var config Config
ctx := context.Background()
response, err := config.DeviceAuth(ctx)
if err != nil {
panic(err)
}
fmt.Printf("please enter code %s at %s\n", response.UserCode, response.VerificationURI)
token, err := config.DeviceAccessToken(ctx, response)
if err != nil {
panic(err)
}
fmt.Println(token)
func (*Config) Exchange
func (c *Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error)
Выполняет обмен authorization code на токен доступа.
Использование:
Вызывается после перенаправления пользователя обратно на Redirect URI (URL, полученный из AuthCodeURL).
Параметры:
ctx - контекст, может содержать кастомный HTTP-клиент (см. переменную HTTPClient)
code - authorization code из параметра http.Request.FormValue("code")
opts - опции, при использовании PKCE должен включать VerifierOption
Безопасность:
- Обязательно проверяйте параметр
state (из http.Request.FormValue("state")) перед вызовом для защиты от CSRF
- Для PKCE передавайте верификатор через VerifierOption
func (*Config) PasswordCredentialsToken
func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error)
Получает токен по связке username/password (Resource Owner Password Credentials flow).
Рекомендации RFC 6749:
Следует использовать ТОЛЬКО при:
- Высокой степени доверия между клиентом и владельцем ресурса
- Когда клиент является частью ОС или привилегированного приложения
- При отсутствии других доступных способов авторизации
Ссылка: RFC 6749 Section 4.3
Параметры:
ctx - может содержать кастомный HTTP-клиент
username, password - учетные данные владельца ресурса
Примечание:
Этот grant type считается менее безопасным и должен применяться в исключительных случаях.
func (*Config) TokenSource
func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource
TokenSource возвращает источник токенов с автоматическим обновлением.
type DeviceAuthResponse
type DeviceAuthResponse struct {
// DeviceCode - код устройства для OAuth-потока
DeviceCode string `json:"device_code"`
// UserCode - код, который пользователь должен ввести
// на странице верификации
UserCode string `json:"user_code"`
// VerificationURI - URL страницы, куда пользователь
// должен ввести user code
VerificationURI string `json:"verification_uri"`
// VerificationURIComplete (опционально) - полный URL верификации
// с уже подставленным user code. Обычно отображается пользователю
// в графической форме (например, QR-код)
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
// Expiry - время истечения срока действия
// device code и user code
Expiry time.Time `json:"expires_in,omitempty"`
// Interval - интервал в секундах между запросами
// на проверку авторизации (polling)
Interval int64 `json:"interval,omitempty"`
}
DeviceAuthResponse описывает успешный ответ Device Authorization по RFC 8628.
Объяснение DeviceAuthResponse
DeviceAuthResponse представляет ответ сервера авторизации в OAuth 2.0 Device Authorization Grant Flow (RFC 8628). Этот тип используется для реализации “потока устройства” - специального сценария авторизации для устройств с ограниченными возможностями ввода (Smart TV, IoT устройства, принтеры и т.д.).
Основные функции:
- Получение кодов верификации для пользователя
- Предоставление инструкций для завершения авторизации
- Управление процессом polling для получения токена
Пример полного цикла использования
1. Инициализация потока устройства
func InitDeviceFlow(config *oauth2.Config) (*oauth2.DeviceAuthResponse, error) {
ctx := context.Background()
// Запрос параметров device flow
deviceAuth, err := config.DeviceAuth(ctx)
if err != nil {
return nil, fmt.Errorf("ошибка инициализации device flow: %v", err)
}
return deviceAuth, nil
}
2. Отображение инструкций пользователю
func ShowUserInstructions(deviceAuth *oauth2.DeviceAuthResponse) {
fmt.Printf("1. Перейдите на страницу: %s\n", deviceAuth.VerificationURI)
fmt.Printf("2. Введите код: %s\n", deviceAuth.UserCode)
// Если доступен QR-код
if deviceAuth.VerificationURIComplete != "" {
fmt.Printf("Или отсканируйте QR-код: %s\n", deviceAuth.VerificationURIComplete)
}
fmt.Printf("Код действителен до: %v\n", deviceAuth.Expiry)
}
3. Получение токена (polling)
func PollForToken(config *oauth2.Config, deviceAuth *oauth2.DeviceAuthResponse) (*oauth2.Token, error) {
ctx := context.Background()
for {
// Запрос токена с использованием device code
token, err := config.DeviceAccessToken(ctx, deviceAuth)
if err == nil {
return token, nil
}
// Обработка ошибок
if oauth2Err, ok := err.(*oauth2.RetrieveError); ok {
if oauth2Err.ErrorCode == "authorization_pending" {
// Ожидание следующего polling-запроса
time.Sleep(time.Duration(deviceAuth.Interval) * time.Second)
continue
}
}
return nil, fmt.Errorf("ошибка получения токена: %v", err)
}
}
Полный пример интеграции
package main
import (
"context"
"fmt"
"log"
"time"
"golang.org/x/oauth2"
)
func main() {
config := &oauth2.Config{
ClientID: "your-client-id",
Scopes: []string{"user.read"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
DeviceURL: "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode",
},
}
// 1. Инициализация device flow
deviceAuth, err := config.DeviceAuth(context.Background())
if err != nil {
log.Fatalf("Ошибка инициализации: %v", err)
}
// 2. Показ инструкций пользователю
fmt.Println("\n=== Инструкции по авторизации ===")
fmt.Printf("Пожалуйста, откройте %s\n", deviceAuth.VerificationURI)
fmt.Printf("И введите код: %s\n\n", deviceAuth.UserCode)
// 3. Polling для получения токена
fmt.Println("Ожидание авторизации...")
token, err := PollForToken(config, deviceAuth)
if err != nil {
log.Fatalf("Ошибка получения токена: %v", err)
}
fmt.Printf("\nУспешная авторизация! Токен: %v\n", token.AccessToken)
}
Особенности работы с DeviceAuthResponse
-
Таймауты и интервалы:
- Учитывайте
Expiry - код устройства имеет ограниченное время жизни
- Соблюдайте
Interval между polling-запросами
-
Пользовательский опыт:
- Для лучшего UX используйте
VerificationURIComplete (QR-код)
- Предусмотрите возможность отмены ожидания
-
Безопасность:
- Не кэшируйте device code без необходимости
- Обрабатывайте все возможные ошибки сервера
-
Поддерживаемые провайдеры:
- Microsoft Identity Platform
- Google OAuth 2.0
- Другие совместимые с RFC 8628
Этот тип особенно полезен для:
- Устройств без браузера
- Приложений с ограниченным вводом
- Сценариев, где нельзя использовать стандартный OAuth flow
func (DeviceAuthResponse) MarshalJSON
func (d DeviceAuthResponse) MarshalJSON() ([]byte, error)
func (*DeviceAuthResponse) UnmarshalJSON
func (c *DeviceAuthResponse) UnmarshalJSON(data []byte) error
type Endpoint
type Endpoint struct {
AuthURL string // URL endpoint'а авторизации (для получения authorization code)
DeviceAuthURL string // URL для Device Flow авторизации (RFC 8628)
TokenURL string // URL endpoint'а получения токенов (для обмена code на token)
AuthStyle AuthStyle // Предпочтительный метод аутентификации клиента
}
Предназначение: Содержит все необходимые URL для OAuth 2.0 flow.
Использование: Конфигурация клиента для взаимодействия с OAuth провайдером.
type RetrieveError
type RetrieveError struct {
Response *http.Response // HTTP-ответ сервера
Body []byte // Тело ответа (может быть усечено)
ErrorCode string // Код ошибки OAuth (RFC 6749 Section 5.2)
ErrorDescription string // Человекочитаемое описание ошибки
ErrorURI string // Ссылка на документацию по ошибке
}
RetrieveError - ошибка, возвращаемая при неверном статусе HTTP или ошибке OAuth2.
Предназначение: Ошибки, возвращаемые OAuth сервером.
Особенности:
- Соответствует стандарту OAuth 2.0 (RFC 6749)
- Включает как HTTP-детали, так и специфичные OAuth ошибки
type Token
type Token struct {
AccessToken string // Основной токен доступа
TokenType string // Тип токена (обычно "Bearer")
RefreshToken string // Токен для обновления (опционально)
Expiry time.Time // Время истечения срока действия
ExpiresIn int64 // Срок жизни токена в секундах
}
Token содержит учетные данные для авторизации запросов.
Предназначение: Хранит OAuth 2.0 токены и метаданные.
- AccessToken всегда обязателен
- RefreshToken может отсутствовать в некоторых flow
- Expiry вычисляется из ExpiresIn при получении токена
type TokenSource
type TokenSource interface {
Token() (*Token, error) // Возвращает текущий/новый токен
}
TokenSource - любой источник, который может возвращать токен.
Предназначение: Абстракция для источников токенов.
Реализации:
- StaticTokenSource (фиксированный токен)
- ReuseTokenSource (с автоматическим обновлением)
- Config.TokenSource (получение токенов из Config)
type Transport
type Transport struct {
Source TokenSource // Источник токенов для авторизации
Base http.RoundTripper // Базовый RoundTripper (по умолчанию http.DefaultTransport)
}
Transport - это http.RoundTripper для OAuth 2.0 запросов.
Предназначение: Добавляет OAuth-авторизацию к HTTP-запросам.
Принцип работы:
- Получает токен из Source
- Добавляет Authorization header
- Делегирует запрос базовому RoundTripper
4.16.1 - Пакет jwt (JSON Web Token)
Пакет jwt реализует поток OAuth 2.0 с использованием JSON Web Token, известный как “двухногая OAuth 2.0” (two-legged OAuth 2.0).
Спецификация: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12
Типы
type Config
type Config struct {
// Email - идентификатор OAuth-клиента
Email string
// PrivateKey содержит содержимое RSA-ключа или PEM-файла
PrivateKey []byte
// PrivateKeyID - необязательный идентификатор ключа
PrivateKeyID string
// Subject - необязательный пользователь для имперсонации
Subject string
// Scopes - запрашиваемые области доступа
Scopes []string
// TokenURL - endpoint для JWT-потока
TokenURL string
// Expires - срок действия токена
Expires time.Duration
// Audience - целевая аудитория запроса
Audience string
// PrivateClaims - кастомные JWT-claims
PrivateClaims map[string]any
// UseIDToken - использовать ID-токен вместо access-токена
UseIDToken bool
}
Config содержит конфигурацию для получения токенов через JWT (“двухногая OAuth 2.0”).
Пример
package main
import (
"context"
"golang.org/x/oauth2/jwt"
)
func main() {
ctx := context.Background()
conf := &jwt.Config{
Email: "xxx@developer.com",
// The contents of your RSA private key or your PEM file
// that contains a private key.
// If you have a p12 file instead, you
// can use `openssl` to export the private key into a pem file.
//
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
//
// It only supports PEM containers with no passphrase.
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."),
Subject: "user@example.com",
TokenURL: "https://provider.com/o/oauth2/token",
}
// Initiate an http.Client, the following GET request will be
// authorized and authenticated on the behalf of user@example.com.
client := conf.Client(ctx)
client.Get("...")
}
Методы Config
func (*Config) Client
func (c *Config) Client(ctx context.Context) *http.Client
Client возвращает HTTP-клиент, который автоматически добавляет Authorization-заголовки с токенами, полученными из конфигурации.
Возвращенный клиент и его Transport не должны изменяться.
func (*Config) TokenSource
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource
TokenSource возвращает источник токенов JWT, используя конфигурацию и HTTP-клиент из переданного контекста.
Особенности реализации
-
Формат ключей:
- Поддерживаются RSA-ключи и PEM-файлы без пароля
- Для конвертации PKCS#12 в PEM используйте:
openssl pkcs12 -in key.p12 -out key.pem -nodes
-
Кастомные claims:
-
Аудитория:
- Если Audience не указан, используется TokenURL
-
Тип токена:
- При UseIDToken=true будет использоваться ID-токен вместо access-токена
4.16.2 - Пакет jws (JSON Web Signature)
Пакет jws предоставляет частичную реализацию кодирования и декодирования JSON Web Signature. Существует для поддержки пакета golang.org/x/oauth2.
Устарело: этот пакет не предназначен для публичного использования и может быть удален в будущем. Существует только для внутреннего использования. Рекомендуется использовать другой JWS-пакет или скопировать этот пакет в свой исходный код.
Функции
func Encode
func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error)
Encode кодирует подписанный JWS с предоставленными заголовком и набором claims. Использует crypto/rsa.SignPKCS1v15 с указанным RSA-приватным ключом.
func EncodeWithSigner
func EncodeWithSigner(header *Header, c *ClaimSet, sg Signer) (string, error)
EncodeWithSigner кодирует заголовок и набор claims с использованием предоставленного подписывающего устройства.
func Verify
func Verify(token string, key *rsa.PublicKey) error
Verify проверяет, была ли подпись JWT-токена создана приватным ключом, соответствующим предоставленному публичному ключу.
Типы
type ClaimSet
type ClaimSet struct {
Iss string `json:"iss"` // email клиентского приложения
Scope string `json:"scope,omitempty"` // запрашиваемые разрешения
Aud string `json:"aud"` // целевая аудитория (опционально)
Exp int64 `json:"exp"` // время истечения (Unix epoch)
Iat int64 `json:"iat"` // время выдачи (Unix epoch)
Typ string `json:"typ,omitempty"` // тип токена (опционально)
Sub string `json:"sub,omitempty"` // email для делегированного доступа
Prn string `json:"prn,omitempty"` // устаревший аналог Sub
PrivateClaims map[string]any `json:"-"` // кастомные claims
}
ClaimSet содержит информацию о JWT-подписи, включая запрашиваемые разрешения, цель токена, издателя, время выдачи и срок действия.
func Decode
func Decode(payload string) (*ClaimSet, error)
Decode декодирует набор claims из JWS-полезной нагрузки.
type Header struct {
Algorithm string `json:"alg"` // алгоритм подписи
Typ string `json:"typ"` // тип токена
KeyID string `json:"kid,omitempty"` // идентификатор ключа (опционально)
}
Header представляет заголовок для подписанных JWS-полезных нагрузок.
type Signer
type Signer func(data []byte) (sig []byte, err error)
Signer возвращает подпись для предоставленных данных.
Особенности реализации
-
Поддержка алгоритмов:
- Основная реализация использует RSA с PKCS1v15
- Возможность подключения кастомных подписывающих устройств
-
Устаревшие поля:
- Поле
Prn сохраняется для обратной совместимости
- Рекомендуется использовать
Sub вместо Prn
-
Кастомные claims:
- Поддерживаются через поле PrivateClaims
- Не включаются в стандартную JSON-маршализацию
-
Безопасность:
- Пакет помечен как устаревший для публичного использования
- Рекомендуется использовать более полные реализации JWS
4.16.3 - Пакет authhandler (Three-Legged OAuth 2.0)
Пакет authhandler реализует TokenSource для поддержки “трехногового OAuth 2.0” через кастомный AuthorizationHandler.
Функции
func TokenSource
func TokenSource(
ctx context.Context,
config *oauth2.Config,
state string,
authHandler AuthorizationHandler,
) oauth2.TokenSource
TokenSource возвращает oauth2.TokenSource, который получает access-токены, используя трехноговый OAuth-поток.
Параметры:
ctx - контекст для операции Exchange
config - полная конфигурация OAuth (AuthURL, TokenURL, Scope)
state - уникальная строка состояния для защиты от CSRF
authHandler - обработчик авторизации для получения согласия пользователя
Особенности:
- Проверяет соответствие параметра
state в запросе и ответе
- Обменивает auth-код на OAuth-токен после проверки
func TokenSourceWithPKCE
func TokenSourceWithPKCE(
ctx context.Context,
config *oauth2.Config,
state string,
authHandler AuthorizationHandler,
pkce *PKCEParams,
) oauth2.TokenSource
TokenSourceWithPKCE - расширенная версия с поддержкой PKCE (Proof Key for Code Exchange).
Дополнительный параметр:
pkce - параметры для защиты от CSRF через code challenge и code verifier
Рекомендации:
Типы
type AuthorizationHandler
type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)
AuthorizationHandler - обработчик для трехногового OAuth, который:
- Перенаправляет пользователя по URL для получения согласия
- Возвращает auth-код и состояние после подтверждения
type PKCEParams
type PKCEParams struct {
Challenge string // Зашифрованный code verifier (base64-url)
ChallengeMethod string // Метод шифрования (напр. S256)
Verifier string // Оригинальный секрет (незашифрованный)
}
PKCEParams содержит параметры для защиты потока PKCE.
Особенности реализации
-
Безопасность:
- Обязательная проверка параметра
state
- Поддержка современных методов защиты PKCE
-
Гибкость:
- Возможность кастомной обработки авторизации
- Поддержка различных методов шифрования PKCE
-
Рекомендации:
- Всегда использовать уникальный
state для каждого запроса
- Для мобильных приложений предпочтительнее TokenSourceWithPKCE
4.16.4 - Пакет clientcredentials OAuth 2.0 Client Credentials Flow
Пакет реализует поток OAuth 2.0 учетные данные клиента client credentials, также известный как двухногая OAuth 2.0
Используется, когда:
- Клиент действует от своего имени
- Клиент является владельцем ресурса
- Запрашивается доступ к защищенным ресурсам на основе предварительной авторизации
Спецификация: https://tools.ietf.org/html/rfc6749#section-4.4
Тип Config
type Config struct {
// ClientID - идентификатор приложения
ClientID string
// ClientSecret - секрет приложения
ClientSecret string
// TokenURL - endpoint сервера для получения токенов
TokenURL string
// Scopes - запрашиваемые разрешения (опционально)
Scopes []string
// EndpointParams - дополнительные параметры запроса
EndpointParams url.Values
// AuthStyle - способ аутентификации клиента
AuthStyle oauth2.AuthStyle
}
Config описывает двухноговый OAuth2 поток, содержащий информацию о клиентском приложении и URL endpoint’ов сервера.
Методы Config
func (*Config) Client
func (c *Config) Client(ctx context.Context) *http.Client
Возвращает HTTP-клиент, который:
- Автоматически добавляет токены авторизации
- Обновляет токены по истечении срока
- Использует HTTP-клиент из контекста (через oauth2.HTTPClient)
Важно: возвращенный клиент и его Transport не должны изменяться.
func (*Config) Token
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error)
Получает токен, используя учетные данные клиента (client credentials).
Может использовать кастомный HTTP-клиент из контекста.
func (*Config) TokenSource
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource
Возвращает источник токенов, который:
- Возвращает текущий токен, пока он действителен
- Автоматически обновляет токен при истечении срока
- Использует client ID и client secret для обновления
Рекомендация: большинству пользователей следует использовать Config.Client вместо прямого использования TokenSource.
Особенности реализации
-
Сценарии использования:
- Сервер-серверное взаимодействие
- Микросервисная архитектура
- Фоновые процессы без участия пользователя
-
Безопасность:
- ClientSecret должен храниться защищенно
- Рекомендуется использовать HTTPS для всех запросов
-
Гибкость:
- Поддержка различных методов аутентификации (AuthStyle)
- Возможность передачи дополнительных параметров (EndpointParams)
4.17 - Описание пакета string языка программирования Go
Пакет strings реализует простые функции для работы со строками в кодировке UTF-8.
Информацию о строках UTF-8 в Go см. на сайте https://blog.golang.org/strings.
4.17.1 - Функции пакета string языка программирования Go
Подробное описание функций пакета string языка программирования Go
func Clone
func Clone(s string) string
Clone возвращает новую копию s. Оно гарантирует создание копии s в новом выделении памяти, что может быть важно при сохранении только небольшой подстроки из гораздо более длинной строки.
Использование Clone может помочь таким программам использовать меньше памяти.
Конечно, поскольку использование Clone создает копию, чрезмерное использование Clone может привести к увеличению потребления памяти программами. Обычно Clone следует использовать редко и только в тех случаях, когда профилирование показывает, что это необходимо. Для строк длиной ноль будет возвращена строка "" и выделение памяти не будет производиться.
Пример
package main
import (
"fmt"
"strings"
"unsafe"
)
func main() {
s := "abc"
clone := strings.Clone(s)
fmt.Println(s == clone)
fmt.Println(unsafe.StringData(s) == unsafe.StringData(clone))
}
func Compare
func Compare(a, b string) int
Compare возвращает целое число, сравнивающее две строки лексикографически. Результат будет 0, если a == b, -1, если a < b, и +1, если a > b.
Используйте Compare, когда вам нужно выполнить трехстороннее сравнение (например, с помощью slices.SortFunc). Обычно более понятно и всегда быстрее использовать встроенные операторы сравнения строк ==, <, > и т. д.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.Compare("a", "b"))
fmt.Println(strings.Compare("a", "a"))
fmt.Println(strings.Compare("b", "a"))
}
func Contains
func Contains(s, substr string) bool
Contains сообщает, находится ли substr в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.Contains("seafood", "foo"))
fmt.Println(strings.Contains("seafood", "bar"))
fmt.Println(strings.Contains("seafood", ""))
fmt.Println(strings.Contains("", ""))
}
Output:
true
false
true
true
func ContainsAny
func ContainsAny(s, chars string) bool
ContainsAny сообщает, находятся ли какие-либо кодовые точки Unicode в chars в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ContainsAny("team", "i"))
fmt.Println(strings.ContainsAny("fail", "ui"))
fmt.Println(strings.ContainsAny("ure", "ui"))
fmt.Println(strings.ContainsAny("failure", "ui"))
fmt.Println(strings.ContainsAny("foo", ""))
fmt.Println(strings.ContainsAny("", ""))
}
Output:
false
true
true
true
false
false
func ContainsFunc
func ContainsFunc(s string, f func(rune) bool) bool
ContainsFunc сообщает, удовлетворяют ли какие-либо кодовые точки Unicode r в s условию f(r).
Пример
package main
import (
"fmt"
"strings"
)
func main() {
f := func(r rune) bool {
return r == 'a' || r == 'e' || r == 'i' || r == 'o' || r == 'u'
}
fmt.Println(strings.ContainsFunc("hello", f))
fmt.Println(strings.ContainsFunc("rhythms", f))
}
func ContainsRune
func ContainsRune(s string, r rune) bool
ContainsRune сообщает, находится ли кодовая точка Unicode r в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
// Finds whether a string contains a particular Unicode code point.
// The code point for the lowercase letter "a", for example, is 97.
fmt.Println(strings.ContainsRune("aardvark", 97))
fmt.Println(strings.ContainsRune("timeout", 97))
}
func Count
func Count(s, substr string) int
Count подсчитывает количество непересекающихся вхождений substr в s. Если substr является пустой строкой, Count возвращает 1 + количество кодовых точек Unicode в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.Count("cheese", "e"))
fmt.Println(strings.Count("five", "")) // before & after each rune
}
func Cut
func Cut(s, sep string) (before, after string, found bool)
Cut разрезает s вокруг первого вхождения sep, возвращая текст до и после sep. Результат found сообщает, встречается ли sep в s. Если sep не встречается в s, cut возвращает s, «», false.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
show := func(s, sep string) {
before, after, found := strings.Cut(s, sep)
fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
}
show("Gopher", "Go")
show("Gopher", "ph")
show("Gopher", "er")
show("Gopher", "Badger")
}
Output:
Cut("Gopher", "Go") = "", "pher", true
Cut("Gopher", "ph") = "Go", "er", true
Cut("Gopher", "er") = "Goph", "", true
Cut("Gopher", "Badger") = "Gopher", "", false
func CutPrefix
func CutPrefix(s, prefix string) (after string, found bool)
CutPrefix возвращает s без указанной начальной префиксной строки и сообщает, был ли найден префикс. Если s не начинается с prefix, CutPrefix возвращает s, false. Если prefix является пустой строкой, CutPrefix возвращает s, true.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
show := func(s, sep string) {
after, found := strings.CutPrefix(s, sep)
fmt.Printf("CutPrefix(%q, %q) = %q, %v\n", s, sep, after, found)
}
show("Gopher", "Go")
show("Gopher", "ph")
}
Output:
CutPrefix("Gopher", "Go") = "pher", true
CutPrefix("Gopher", "ph") = "Gopher", false
func CutSuffix
func CutSuffix(s, suffix string) (before string, found bool)
CutSuffix возвращает s без указанной конечной строки суффикса и сообщает, был ли найден суффикс. Если s не заканчивается суффиксом, CutSuffix возвращает s, false. Если суффикс является пустой строкой, CutSuffix возвращает s, true.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
show := func(s, sep string) {
before, found := strings.CutSuffix(s, sep)
fmt.Printf("CutSuffix(%q, %q) = %q, %v\n", s, sep, before, found)
}
show("Gopher", "Go")
show("Gopher", "er")
}
Output:
CutSuffix("Gopher", "Go") = "Gopher", false
CutSuffix("Gopher", "er") = "Goph", true
func EqualFold
func EqualFold(s, t string) bool
EqualFold сообщает, равны ли s и t, интерпретируемые как строки UTF-8, при простом преобразовании регистра Unicode, которое является более общей формой нечувствительности к регистру.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.EqualFold("Go", "go"))
fmt.Println(strings.EqualFold("AB", "ab")) // true because comparison uses simple case-folding
fmt.Println(strings.EqualFold("ß", "ss")) // false because comparison does not use full case-folding
}
func Fields
func Fields(s string) []string
Fields разбивает строку s по каждому вхождению одного или нескольких последовательных пробелов, как определено в unicode.IsSpace, возвращая массив подстрок s или пустой массив, если s содержит только пробелы.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz "))
}
Output:
Fields are: ["foo" "bar" "baz"]
func FieldsFunc
func FieldsFunc(s string, f func(rune) bool) []string
FieldsFunc разбивает строку s на каждом проходе кодовых точек Unicode c, удовлетворяющих f(c), и возвращает массив фрагментов s. Если все кодовые точки в s удовлетворяют f(c) или строка пуста, возвращается пустой фрагмент.
FieldsFunc не дает никаких гарантий относительно порядка вызова f(c) и предполагает, что f всегда возвращает одно и то же значение для данного c.
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
fmt.Printf("Fields are: %q", strings.FieldsFunc(" foo1;bar2,baz3...", f))
}
Output:
Fields are: ["foo1" "bar2" "baz3"]
func FieldsFuncSeq
func FieldsFuncSeq(s string, f func(rune) bool) iter.Seq[string]
FieldsFuncSeq возвращает итератор над подстроками s, разбитыми по последовательностям кодовых точек Unicode, удовлетворяющих f(c). Итератор возвращает те же строки, что и FieldsFunc(s), но без построения среза.
func FieldsSeq
func FieldsSeq(s string) iter.Seq[string]
FieldsSeq возвращает итератор над подстроками s, разбитыми по последовательностям пробельных символов, как определено в unicode.IsSpace. Итератор возвращает те же строки, что и Fields(s), но без построения среза.
func HasPrefix
func HasPrefix(s, prefix string) bool
HasPrefix сообщает, начинается ли строка s с префикса.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.HasPrefix("Gopher", "Go"))
fmt.Println(strings.HasPrefix("Gopher", "C"))
fmt.Println(strings.HasPrefix("Gopher", ""))
}
func HasSuffix
func HasSuffix(s, suffix string) bool
HasSuffix сообщает, заканчивается ли строка s суффиксом.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.HasSuffix("Amigo", "go"))
fmt.Println(strings.HasSuffix("Amigo", "O"))
fmt.Println(strings.HasSuffix("Amigo", "Ami"))
fmt.Println(strings.HasSuffix("Amigo", ""))
}
Output:
true
false
false
true
func Index
func Index(s, substr string) int
Index возвращает индекс первого вхождения substr в s или -1, если substr отсутствует в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.Index("chicken", "ken"))
fmt.Println(strings.Index("chicken", "dmr"))
}
func IndexAny
func IndexAny(s, chars string) int
IndexAny возвращает индекс первого вхождения любого кодового пункта Unicode из chars в s, или -1, если ни один кодовый пункт Unicode из chars не присутствует в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.IndexAny("chicken", "aeiouy"))
fmt.Println(strings.IndexAny("crwth", "aeiouy"))
}
func IndexByte
func IndexByte(s string, c byte) int
IndexByte возвращает индекс первого вхождения c в s, или -1, если c отсутствует в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.IndexByte("golang", 'g'))
fmt.Println(strings.IndexByte("gophers", 'h'))
fmt.Println(strings.IndexByte("golang", 'x'))
}
func IndexFunc
func IndexFunc(s string, f func(rune) bool) int
IndexFunc возвращает индекс в s первого кодового пункта Unicode, удовлетворяющего f(c), или -1, если такового нет.
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
f := func(c rune) bool {
return unicode.Is(unicode.Han, c)
}
fmt.Println(strings.IndexFunc("Hello, 世界", f))
fmt.Println(strings.IndexFunc("Hello, world", f))
}
func IndexRune
func IndexRune(s string, r rune) int
IndexRune возвращает индекс первого вхождения кодовой точки Unicode r, или -1, если rune отсутствует в s. Если r является utf8.RuneError, то возвращается первое вхождение любой недопустимой последовательности байтов UTF-8.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.IndexRune("chicken", 'k'))
fmt.Println(strings.IndexRune("chicken", 'd'))
}
func Join
func Join(elems []string, sep string) string
Join объединяет элементы своего первого аргумента, создавая одну строку. Разделитель sep помещается между элементами в результирующей строке.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
s := []string{"foo", "bar", "baz"}
fmt.Println(strings.Join(s, ", "))
}
func LastIndex
func LastIndex(s, substr string) int
LastIndex возвращает индекс последнего вхождения substr в s, или -1, если substr отсутствует в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.Index("go gopher", "go"))
fmt.Println(strings.LastIndex("go gopher", "go"))
fmt.Println(strings.LastIndex("go gopher", "rodent"))
}
func LastIndexAny
func LastIndexAny(s, chars string) int
LastIndexAny возвращает индекс последнего вхождения любого кодового пункта Unicode из chars в s, или -1, если в s нет кодовых пунктов Unicode из chars.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.LastIndexAny("go gopher", "go"))
fmt.Println(strings.LastIndexAny("go gopher", "rodent"))
fmt.Println(strings.LastIndexAny("go gopher", "fail"))
}
func LastIndexByte
func LastIndexByte(s string, c byte) int
LastIndexByte возвращает индекс последнего вхождения c в s, или -1, если c отсутствует в s.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.LastIndexByte("Hello, world", 'l'))
fmt.Println(strings.LastIndexByte("Hello, world", 'o'))
fmt.Println(strings.LastIndexByte("Hello, world", 'x'))
}
func LastIndexFunc
func LastIndexFunc(s string, f func(rune) bool) int
LastIndexFunc возвращает индекс в s последней кодовой точки Unicode, удовлетворяющей f(c), или -1, если таковой нет.
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Println(strings.LastIndexFunc("go 123", unicode.IsNumber))
fmt.Println(strings.LastIndexFunc("123 go", unicode.IsNumber))
fmt.Println(strings.LastIndexFunc("go", unicode.IsNumber))
}
func Lines
func Lines(s string) iter.Seq[string]
Lines возвращает итератор по строкам, отделенным символом новой строки, в строке s. Строки, возвращаемые итератором, включают в себя символы новой строки, отделяющие их. Если s пуста, итератор не возвращает никаких строк. Если s не заканчивается символом новой строки, последняя возвращаемая строка не будет заканчиваться символом новой строки. Возвращает итератор однократного использования.
func Map
func Map(mapping func(rune) rune, s string) string
Map возвращает копию строки s со всеми ее символами, измененными в соответствии с функцией отображения. Если отображение возвращает отрицательное значение, символ удаляется из строки без замены.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
rot13 := func(r rune) rune {
switch {
case r >= 'A' && r <= 'Z':
return 'A' + (r-'A'+13)%26
case r >= 'a' && r <= 'z':
return 'a' + (r-'a'+13)%26
}
return r
}
fmt.Println(strings.Map(rot13, "'Twas brillig and the slithy gopher..."))
}
Output:
'Gjnf oevyyvt naq gur fyvgul tbcure...
func Repeat
func Repeat(s string, count int) string
Repeat возвращает новую строку, состоящую из count копий строки s.
Функция выдает ошибку, если count отрицательно или если результат (len(s) * count) переполняет буфер.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println("ba" + strings.Repeat("na", 2))
}
func Replace
func Replace(s, old, new string, n int) string
Replace возвращает копию строки s, в которой первые n неперекрывающихся экземпляров old заменены на new. Если old пустое, оно совпадает в начале строки и после каждой последовательности UTF-8, давая до k+1 замен для строки k-рун. Если n < 0, количество замен не ограничено.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2))
fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1))
}
Output:
oinky oinky oink
moo moo moo
func ReplaceAll
func ReplaceAll(s, old, new string) string
ReplaceAll возвращает копию строки s со всеми неперекрывающимися вхождениями old, замененными на new. Если old пустое, оно совпадает с началом строки и после каждой последовательности UTF-8, давая до k+1 замен для строки из k рун.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ReplaceAll("oink oink oink", "oink", "moo"))
}
func Split
func Split(s, sep string) []string
Split разделяет s на все подстроки, разделенные sep, и возвращает набор подстрок между этими разделителями.
Если s не содержит sep, а sep не пустой, Split возвращает набор длиной 1, единственным элементом которого является s.
Если sep пустой, Split разделяет после каждой последовательности UTF-8. Если и s, и sep пустые, Split возвращает пустой набор.
Это эквивалентно SplitN с количеством -1.
Чтобы разделить вокруг первого вхождения разделителя, см. Cut.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("%q\n", strings.Split("a,b,c", ","))
fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a "))
fmt.Printf("%q\n", strings.Split(" xyz ", ""))
fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins"))
}
Output:
["a" "b" "c"]
["" "man " "plan " "canal panama"]
[" " "x" "y" "z" " "]
[""]
func SplitAfter
func SplitAfter(s, sep string) []string
SplitAfter разделяет s на все подстроки после каждого вхождения sep и возвращает срез этих подстрок.
Если s не содержит sep, а sep не пустой, SplitAfter возвращает срез длиной 1, единственным элементом которого является s.
Если sep пусто, SplitAfter разделяет после каждой последовательности UTF-8. Если и s, и sep пусты, SplitAfter возвращает пустой срез.
Это эквивалентно SplitAfterN с количеством -1.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("%q\n", strings.SplitAfter("a,b,c", ","))
}
func SplitAfterN
func SplitAfterN(s, sep string, n int) []string
SplitAfterN разделяет s на подстроки после каждого вхождения sep и возвращает срез этих подстрок.
Число определяет количество подстрок, которые будут возвращены:
n > 0: не более n подстрок; последняя подстрока будет неразделенным остатком;
n == 0: результат равен nil (ноль подстрок);
n < 0: все подстроки.
Крайние случаи для s и sep (например, пустые строки) обрабатываются, как описано в документации для SplitAfter.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("%q\n", strings.SplitAfterN("a,b,c", ",", 2))
}
func SplitAfterSeq
func SplitAfterSeq(s, sep string) iter.Seq[string]
SplitAfterSeq возвращает итератор над подстроками s, разделенными после каждого вхождения sep. Итератор возвращает те же строки, которые были бы возвращены SplitAfter(s, sep), но без построения слайса. Он возвращает итератор однократного использования.
func SplitN
func SplitN(s, sep string, n int) []string
SplitN разделяет s на подстроки, разделенные sep, и возвращает срез подстрок между этими разделителями.
Число определяет количество подстрок, которые будут возвращены:
n > 0: не более n подстрок; последняя подстрока будет неразделенной остаточной частью;
n == 0: результат равен nil (ноль подстрок);
n < 0: все подстроки.
Крайние случаи для s и sep (например, пустые строки) обрабатываются, как описано в документации по Split.
Чтобы разделить вокруг первого вхождения разделителя, см. Cut.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("%q\n", strings.SplitN("a,b,c", ",", 2))
z := strings.SplitN("a,b,c", ",", 0)
fmt.Printf("%q (nil = %v)\n", z, z == nil)
}
Output:
["a" "b,c"]
[] (nil = true)
func SplitSeq
func SplitSeq(s, sep string) iter.Seq[string]
SplitSeq возвращает итератор по всем подстрокам s, разделенным sep. Итератор возвращает те же строки, что и Split(s, sep), но без создания слайса. Он возвращает итератор однократного использования.
func Title (устарело)
func ToLower
func ToLower(s string) string
ToLower возвращает s со всеми буквами Unicode, преобразованными в нижний регистр.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToLower("Gopher"))
}
func ToLowerSpecial
func ToLowerSpecial(c unicode.SpecialCase, s string) string
ToLowerSpecial возвращает копию строки s со всеми буквами Unicode, преобразованными в нижний регистр с использованием преобразования регистра, указанного c.
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "Örnek İş"))
}
func ToTitle
func ToTitle(s string) string
ToTitle возвращает копию строки s, в которой все буквы Unicode преобразованы в заглавные.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
// Compare this example to the Title example.
fmt.Println(strings.ToTitle("her royal highness"))
fmt.Println(strings.ToTitle("loud noises"))
fmt.Println(strings.ToTitle("брат"))
}
HER ROYAL HIGHNESS
LOUD NOISES
БРАТ
func ToTitleSpecial
func ToTitleSpecial(c unicode.SpecialCase, s string) string
ToTitleSpecial возвращает копию строки s, в которой все буквы Unicode преобразованы в заглавные, с приоритетом специальных правил преобразования регистра.
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Println(strings.ToTitleSpecial(unicode.TurkishCase, "dünyanın ilk borsa yapısı Aizonai kabul edilir"))
}
Output:
DÜNYANIN İLK BORSA YAPISI AİZONAİ KABUL EDİLİR
func ToUpper
func ToUpper(s string) string
ToUpper возвращает s, в которой все буквы Unicode преобразованы в заглавные.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("Gopher"))
}
func ToUpperSpecial
func ToUpperSpecial(c unicode.SpecialCase, s string) string
ToUpperSpecial возвращает копию строки s, в которой все буквы Unicode преобразованы в верхний регистр с использованием преобразования регистра, указанного в c.
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "örnek iş"))
}
func ToValidUTF8
func ToValidUTF8(s, replacement string) string
ToValidUTF8 возвращает копию строки s, в которой каждая последовательность недопустимых байтов UTF-8 заменена строкой replacement, которая может быть пустой.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("%s\n", strings.ToValidUTF8("abc", "\uFFFD"))
fmt.Printf("%s\n", strings.ToValidUTF8("a\xffb\xC0\xAFc\xff", ""))
fmt.Printf("%s\n", strings.ToValidUTF8("\xed\xa0\x80", "abc"))
}
func Trim
func Trim(s, cutset string) string
Trim возвращает фрагмент строки s, из которого удалены все начальные и конечные кодовые точки Unicode, содержащиеся в cutset.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Print(strings.Trim("¡¡¡Hello, Gophers!!!", "!¡"))
}
func TrimFunc
func TrimFunc(s string, f func(rune) bool) string
TrimFunc возвращает фрагмент строки s, из которого удалены все начальные и конечные кодовые точки Unicode c, удовлетворяющие f(c).
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Print(strings.TrimFunc("¡¡¡Hello, Gophers!!!", func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
}))
}
func TrimLeft
func TrimLeft(s, cutset string) string
TrimLeft возвращает фрагмент строки s, из которого удалены все начальные кодовые точки Unicode, содержащиеся в cutset.
Чтобы удалить префикс, используйте вместо этого TrimPrefix.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Print(strings.TrimLeft("¡¡¡Hello, Gophers!!!", "!¡"))
}
Output:
Hello, Gophers!!!
func TrimLeftFunc
func TrimLeftFunc(s string, f func(rune) bool) string
TrimLeftFunc возвращает фрагмент строки s, из которого удалены все начальные кодовые точки Unicode c, удовлетворяющие f(c).
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Print(strings.TrimLeftFunc("¡¡¡Hello, Gophers!!!", func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
}))
}
Output:
Hello, Gophers!!!
func TrimPrefix
func TrimPrefix(s, prefix string) string
TrimPrefix возвращает s без указанного начального префикса string. Если s не начинается с префикса, s возвращается без изменений.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
var s = "¡¡¡Hello, Gophers!!!"
s = strings.TrimPrefix(s, "¡¡¡Hello, ")
s = strings.TrimPrefix(s, "¡¡¡Howdy, ")
fmt.Print(s)
}
func TrimRight
func TrimRight(s, cutset string) string
TrimRight возвращает фрагмент строки s, из которого удалены все конечные кодовые точки Unicode, содержащиеся в cutset.
Чтобы удалить суффикс, используйте вместо этого TrimSuffix.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Print(strings.TrimRight("¡¡¡Hello, Gophers!!!", "!¡"))
}
Output:
¡¡¡Hello, Gophers
func TrimRightFunc
func TrimRightFunc(s string, f func(rune) bool) string
TrimRightFunc возвращает фрагмент строки s, из которого удалены все конечные кодовые точки Unicode c, удовлетворяющие f(c).
Пример
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Print(strings.TrimRightFunc("¡¡¡Hello, Gophers!!!", func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
}))
}
Output:
¡¡¡Hello, Gophers
func TrimSpace
func TrimSpace(s string) string
TrimSpace возвращает фрагмент строки s, из которого удалены все ведущие и конечные пробелы, как определено в Unicode.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.TrimSpace(" \t\n Hello, Gophers \n\t\r\n"))
}
func TrimSuffixy
func TrimSuffix(s, suffix string) string
TrimSuffix возвращает s без указанного конечного суффикса string. Если s не заканчивается суффиксом, s возвращается без изменений.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
var s = "¡¡¡Hello, Gophers!!!"
s = strings.TrimSuffix(s, ", Gophers!!!")
s = strings.TrimSuffix(s, ", Marmots!!!")
fmt.Print(s)
}
4.17.2 - Описание типов пакета string языка программирования Go
Описание типов и их функций из пакета string для языка Go с примерами
type Builder
type Builder struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Builder используется для эффективного построения строки с помощью методов Builder.Write. Он минимизирует копирование памяти. Нулевое значение готово к использованию. Не копируйте Builder, отличное от нуля.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
var b strings.Builder
for i := 3; i >= 1; i-- {
fmt.Fprintf(&b, "%d...", i)
}
b.WriteString("ignition")
fmt.Println(b.String())
}
Output:
3...2...1...ignition
Объяснение Builder
Builder из пакета strings - это специальный тип для эффективного построения строк в Go. Он особенно полезен, когда вам нужно собрать строку из множества частей, так как он минимизирует копирование памяти и аллокации.
Основные преимущества Builder:
- Эффективность памяти - избегает лишних копирований данных
- Простота использования - предоставляет удобные методы для добавления содержимого
- Готовность к использованию - нулевое значение (
var b strings.Builder) сразу готово к работе
Основные методы:
Write() - добавляет байты
WriteString() - добавляет строку
WriteByte() - добавляет один байт
WriteRune() - добавляет руну (Unicode символ)
String() - возвращает собранную строку
Reset() - очищает builder для повторного использования
Len() - возвращает текущую длину
Cap() - возвращает текущую емкость
Grow() - заранее резервирует память
Примеры использования:
1. Базовый пример
package main
import (
"fmt"
"strings"
)
func main() {
var builder strings.Builder
builder.WriteString("Hello, ")
builder.WriteString("World!")
builder.WriteByte(' ')
builder.WriteRune('😊')
result := builder.String()
fmt.Println(result) // Hello, World! 😊
}
2. Эффективная конкатенация множества строк
func joinStrings(words []string) string {
var builder strings.Builder
// Заранее выделяем память для приблизительного размера
total := 0
for _, w := range words {
total += len(w)
}
builder.Grow(total)
for _, word := range words {
builder.WriteString(word)
}
return builder.String()
}
3. Построение JSON или HTML
func buildHTMLPage(title, body string) string {
var builder strings.Builder
builder.Grow(len(title) + len(body) + 50) // Примерный расчет
builder.WriteString("<html><head><title>")
builder.WriteString(title)
builder.WriteString("</title></head><body>")
builder.WriteString(body)
builder.WriteString("</body></html>")
return builder.String()
}
4. Повторное использование Builder
var builder strings.Builder
func logMessage(msg string) string {
builder.Reset() // Очищаем для повторного использования
builder.WriteString("[LOG] ")
builder.WriteString(time.Now().Format("2006-01-02 15:04:05"))
builder.WriteString(": ")
builder.WriteString(msg)
return builder.String()
}
Почему Builder лучше обычной конкатенации?
При обычной конкатенации строк с помощью +:
s := "a" + "b" + "c" + "d"
Каждая операция создает новую строку, что приводит к лишним аллокациям памяти.
Builder же:
- Накапливает данные в буфере
- Увеличивает буфер только когда нужно
- Копирует данные минимальное количество раз
Когда использовать strings.Builder?
- Когда нужно собрать строку из многих частей
- Когда важна производительность (особенно в циклах)
- Когда заранее известен приблизительный размер результата (можно использовать Grow)
- Когда нужно минимизировать аллокации памяти
Builder особенно полезен в высоконагруженных участках кода, где важна эффективность работы со строками.
func (*Builder) Cap
func (b *Builder) Cap() int
Cap возвращает емкость базового байтового слайса builder. Это общее пространство, выделенное для строчки, которая создается, и включает в себя все уже записанные байты.
func (*Builder) Grow
func (b *Builder) Grow(n int)
Grow увеличивает емкость b, если это необходимо, чтобы гарантировать пространство для еще n байтов. После Grow(n) в b можно записать как минимум n байтов без дополнительного выделения памяти. Если n отрицательно, Grow вызывает панику.
func (*Builder) Len
func (b *Builder) Len() int
Len возвращает количество накопленных байтов; b.Len() == len(b.String()).
func (*Builder) Reset
func (b *Builder) Reset()
Reset сбрасывает Builder в пустое состояние.
func (*Builder) String
func (b *Builder) String() string
String возвращает накопленную строку.
func (*Builder) Write
func (b *Builder) Write(p []byte) (int, error)
Write добавляет содержимое p в буфер b. Write всегда возвращает len(p), nil.
func (*Builder) WriteByte
func (b *Builder) WriteByte(c byte) error
WriteByte добавляет байт c в буфер b. Возвращаемая ошибка всегда равна nil.
func (*Builder) WriteRune
func (b *Builder) WriteRune(r rune) (int, error)
WriteRune добавляет UTF-8-кодировку кодовой точки Unicode r в буфер b. Возвращает длину r и ошибку nil.
func (*Builder) WriteString
func (b *Builder) WriteString(s string) (int, error)
WriteString добавляет содержимое s в буфер b. Возвращает длину s и ошибку nil.
type Reader
type Reader struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Reader реализует интерфейсы io.Reader, io.ReaderAt, io.ByteReader, io.ByteScanner, io.RuneReader, io.RuneScanner, io.Seeker и io.WriterTo, читая из строки. Нулевое значение для Reader работает как Reader пустой строки.
Объяснение Reader
Reader из пакета strings - это тип, который реализует множество интерфейсов для чтения из строки как из потока данных. Это мощный инструмент, когда вам нужно работать со строкой как с последовательностью байт или рун, поддерживающий различные операции чтения и навигации.
Основные особенности Reader:
- Чтение строки как потока - позволяет обрабатывать строку постепенно
- Реализация множества интерфейсов - совместим со стандартными интерфейсами ввода-вывода Go
- Поддержка позиционирования - можно перемещаться по строке
- Нулевое значение готово к работе -
var r strings.Reader работает с пустой строкой
Реализуемые интерфейсы:
io.Reader - базовое чтение байтов
io.ReaderAt - чтение с определенной позиции
io.ByteReader - чтение отдельных байтов
io.ByteScanner - чтение и возврат байтов
io.RuneReader - чтение рун (Unicode символов)
io.RuneScanner - чтение и возврат рун
io.Seeker - перемещение по строке
io.WriterTo - запись содержимого в io.Writer
Основные методы:
Len() int - количество непрочитанных байтов
Read(b []byte) (n int, err error) - чтение байтов в срез
ReadAt(b []byte, off int64) (n int, err error) - чтение с определенной позиции
ReadByte() (byte, error) - чтение одного байта
UnreadByte() error - возврат последнего прочитанного байта
ReadRune() (ch rune, size int, err error) - чтение одной руны
UnreadRune() error - возврат последней прочитанной руны
Seek(offset int64, whence int) (int64, error) - перемещение по строке
WriteTo(w io.Writer) (n int64, err error) - запись всего содержимого в Writer
Reset(s string) - сброс Reader для чтения новой строки
Примеры использования:
1. Базовое чтение
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReader("Пример строки")
buf := make([]byte, 7)
n, err := r.Read(buf)
fmt.Printf("Прочитано %d байт: %q\n", n, buf[:n])
// Прочитано 7 байт: "Пример"
}
2. Чтение рун
func printRunes(s string) {
r := strings.NewReader(s)
for {
ch, size, err := r.ReadRune()
if err != nil {
break
}
fmt.Printf("%c (%d байт)\n", ch, size)
}
}
// Для строки "Привет" выведет:
// П (2 байт)
// р (2 байт)
// и (2 байт)
// в (2 байт)
// е (2 байт)
// т (2 байт)
3. Использование Seek для навигации
func seekExample() {
r := strings.NewReader("Hello, World!")
// Пропускаем "Hello, "
r.Seek(7, io.SeekStart)
b, _ := r.ReadByte()
fmt.Printf("Первый байт после Seek: %c\n", b) // W
}
4. Чтение с определенной позиции (ReadAt)
func readAtExample() {
r := strings.NewReader("abcdefghij")
buf := make([]byte, 3)
r.ReadAt(buf, 4)
fmt.Println(string(buf)) // efg
}
5. Запись содержимого в другой Writer
func writeToExample(w io.Writer) {
r := strings.NewReader("Данные для записи")
r.WriteTo(w) // Запишет всю строку в w
}
6. Комбинирование методов
func processString(s string) {
r := strings.NewReader(s)
// Читаем первые 5 байт
prefix := make([]byte, 5)
r.Read(prefix)
// Читаем оставшуюся часть по рунам
for {
ch, _, err := r.ReadRune()
if err != nil {
break
}
// Обработка руны...
}
}
Когда использовать strings.Reader?
- Когда нужно читать строку по частям - как из потока
- Для совместимости с API, ожидающими io.Reader - многие функции принимают io.Reader
- Когда нужен произвольный доступ к частям строки - через ReadAt/Seek
- Для обработки Unicode текста - через ReadRune
- Когда нужно повторно “проиграть” часть данных - через UnreadByte/UnreadRune
Преимущества перед работой со строкой напрямую:
- Постепенная обработка - не нужно загружать всю строку в память
- Стандартные интерфейсы - совместимость с другими пакетами
- Гибкость навигации - возможность перемещаться по строке
- Безопасность - Reader неизменяем, исходная строка защищена от модификаций
Reader особенно полезен при парсинге, обработке больших строк или когда вам нужно интегрировать строку в систему, работающую с потоками данных.
func NewReader
func NewReader(s string) *Reader
NewReader возвращает новый Reader, читающий из s. Он похож на bytes.NewBufferString, но более эффективен и не поддается записи.
func (*Reader) Len
func (r *Reader) Len() int
Len возвращает количество байтов непрочитанной части строки.
func (*Reader) Read
func (r *Reader) Read(b []byte) (n int, err error)
Read реализует интерфейс io.Reader.
func (*Reader) ReadAt
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error)
ReadAt реализует интерфейс io.ReaderAt.
func (*Reader) ReadByte
func (r *Reader) ReadByte() (byte, error)
ReadByte реализует интерфейс io.ByteReader.
func (*Reader) ReadRune
func (r *Reader) ReadRune() (ch rune, size int, err error)
ReadRune реализует интерфейс io.RuneReader.
func (*Reader) Reset
func (r *Reader) Reset(s string)
Reset сбрасывает Reader для чтения из s.
func (*Reader) Seek
func (r *Reader) Seek(offset int64, whence int) (int64, error)
Seek реализует интерфейс io.Seeker.
func (*Reader) Size
func (r *Reader) Size() int64
Size возвращает исходную длину базовой строки. Size — это количество байтов, доступных для чтения с помощью Reader.ReadAt. Возвращаемое значение всегда одинаково и не зависит от вызовов других методов.
func (*Reader) UnreadByte
func (r *Reader) UnreadByte() error
UnreadByte реализует интерфейс io.ByteScanner.
func (*Reader) UnreadRune
func (r *Reader) UnreadRune() error
UnreadRune реализует интерфейс io.RuneScanner.
func (*Reader) WriteTo
func (r *Reader) WriteTo(w io.Writer) (n int64, err error)
WriteTo реализует интерфейс io.WriterTo.
type Replacer
type Replacer struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Replacer заменяет список строк на замены. Он безопасен для одновременного использования несколькими goroutines.
Объяснение Replacer
Replacer - это мощный инструмент для замены подстрок в Go, который оптимизирован для многократного использования и безопасен для конкурентного использования в горутинах.
Основные характеристики
- Горутин-безопасность: Можно использовать из нескольких горутин одновременно
- Оптимизированная производительность: Быстрее, чем последовательные вызовы
strings.Replace()
- Поддержка множественных замен: Может обрабатывать множество замен за один проход по строке
- Кэширование: Сохраняет оптимизированное состояние для повторного использования
Создание Replacer
Создается с помощью strings.NewReplacer(), который принимает пары “старая строка - новая строка”:
replacer := strings.NewReplacer(
"старое1", "новое1",
"старое2", "новое2",
// ...
)
Основные методы
Заменяет все вхождения заданных подстрок в входной строке:
func main() {
r := strings.NewReplacer("мир", "Go", "Привет", "Здравствуй")
result := r.Replace("Привет, мир!")
fmt.Println(result) // "Здравствуй, Go!"
}
WriteString(w io.Writer, s string) (n int, err error)
Записывает строку с выполненными заменами непосредственно в io.Writer:
func writeReplaced(w io.Writer) {
r := strings.NewReplacer("\n", "\\n", "\t", "\\t")
r.WriteString(w, "Строка с\nпереносами\tи табуляцией")
// Запишет: "Строка с\nпереносами\tи табуляцией"
}
Примеры использования
1. Экранирование специальных символов
func escapeHTML(s string) string {
replacer := strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
"\"", """,
)
return replacer.Replace(s)
}
2. Транслитерация
func transliterate(s string) string {
return strings.NewReplacer(
"щ", "shh", "ш", "sh", "ч", "ch",
"я", "ya", "ю", "yu", "ж", "zh",
).Replace(s)
}
3. Множественные замены в большом тексте
func processText(text string) string {
replacer := strings.NewReplacer(
"красный", "синий",
"яблоко", "апельсин",
"собака", "кошка",
)
return replacer.Replace(text)
}
4. Обработка логов
func cleanLog(log string) string {
return strings.NewReplacer(
"\n", " | ",
"\t", " ",
"\r", "",
).Replace(log)
}
Особенности производительности
- Однопроходная обработка: Все замены выполняются за один проход по строке
- Оптимизация для повторного использования: Созданный
Replacer кэширует свои внутренние структуры
- Эффективность для большого числа замен: Лучше использовать один
Replacer со многими заменами, чем несколько вызовов Replace
Рекомендации по использованию
- Для многократного использования: Создавайте
Replacer один раз и используйте многократно
- Для большого числа замен: Когда нужно сделать более 2-3 замен в одной строке
- В конкурентной среде: Когда замены могут выполняться из нескольких горутин
- Для записи в Writer: Когда нужно сразу записывать результат в выходной поток
Сравнение с другими методами замены
| Метод |
Многопоточность |
Множественные замены |
Производительность |
Повторное использование |
strings.Replace() |
Нет |
Нет (одна замена) |
Средняя |
Нет |
regexp.Regexp.ReplaceAllString() |
Да (с sync) |
Да |
Низкая (для простых замен) |
Да |
strings.Replacer |
Да |
Да |
Высокая |
Да |
Replacer особенно полезен в веб-серверах, обработчиках запросов и других высоконагруженных приложениях, где требуется выполнять одни и те же замены многократно.
func NewReplacer
func NewReplacer(oldnew ...string) *Replacer
NewReplacer возвращает новый Replacer из списка пар старых и новых строк. Замены выполняются в том порядке, в котором они появляются в целевой строке, без перекрывающихся совпадений. Сравнение старых строк выполняется в порядке аргументов.
NewReplacer вызывает панику, если ему передано нечетное количество аргументов.
Пример
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReplacer("<", "<", ">", ">")
fmt.Println(r.Replace("This is <b>HTML</b>!"))
}
Output:
This is <b>HTML</b>!
func (*Replacer) Replace
func (r *Replacer) Replace(s string) string
Replace возвращает копию s со всеми выполненными заменами.
func (*Replacer) WriteString
func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error)
WriteString записывает s в w со всеми выполненными заменами.
4.18 - Полное описание пакета log в Go
Полное описание пакета log в Go с примерами применения и разными потоками вывода
Пакет log в Go предоставляет простые и эффективные инструменты для логирования. Он поддерживает:
- Вывод логов в стандартный вывод (
stdout/stderr).
- Настройку формата вывода (префиксы, дата/время, уровень логирования).
- Запись логов в файлы.
- Гибкую настройку через
log.Logger.
1. Основные функции пакета log
1.1. Простое логирование
Пакет log предоставляет стандартные функции:
log.Print("Обычное сообщение") // Вывод без формата
log.Printf("Формат: %d", 123) // Форматированный вывод
log.Println("Сообщение с новой строкой") // Вывод с \n
1.2. Логирование с уровнем FATAL (завершение программы)
log.Fatal("Сообщение и выход с os.Exit(1)") // Вывод + exit(1)
log.Fatalf("Формат: %s", "ошибка") // Форматированный вывод + exit(1)
log.Fatalln("Фатальная ошибка") // Вывод + exit(1)
1.3. Логирование с уровнем PANIC (вызов паники)
log.Panic("Паника с выводом стека") // Вывод + panic()
log.Panicf("Ошибка: %v", err) // Форматированный вывод + panic()
log.Panicln("Критическая ошибка") // Вывод + panic()
2. Настройка формата вывода
2.1. Добавление префикса (дата, время, уровень)
log.SetPrefix("ERROR: ") // Установка префикса
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) // Формат
log.Println("Сообщение с префиксом")
Вывод:
ERROR: 2024/03/15 14:20:10 main.go:10: Сообщение с префиксом
2.2. Доступные флаги для log.SetFlags
| Флаг |
Описание |
log.Ldate |
Дата (2006/01/02) |
log.Ltime |
Время (15:04:05) |
log.Lmicroseconds |
Микросекунды (15:04:05.000000) |
log.Llongfile |
Полный путь к файлу (/a/b/c.go:10) |
log.Lshortfile |
Короткое имя файла (c.go:10) |
log.LUTC |
Вывод времени в UTC |
log.Lmsgprefix |
Префикс перед сообщением (Go 1.14+) |
Пример комбинации флагов:
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile | log.Lmsgprefix)
3. Запись логов в файл
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal("Ошибка открытия файла:", err)
}
defer file.Close()
log.SetOutput(file) // Перенаправляем вывод в файл
log.Println("Сообщение записано в app.log")
4. Создание нескольких логгеров
Можно создавать отдельные логгеры для разных целей:
logger := log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime)
errorLogger := log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
logger.Println("Информационное сообщение")
errorLogger.Println("Ошибка в программе")
Вывод:
INFO: 2024/03/15 14:20:10 Информационное сообщение
ERROR: 2024/03/15 14:20:10 main.go:15: Ошибка в программе
5. Продвинутое использование (JSON-логирование, уровни)
Стандартный log не поддерживает уровни (DEBUG, INFO, WARN, ERROR), но их можно эмулировать:
5.1. Логирование в JSON
type LogEntry struct {
Level string `json:"level"`
Message string `json:"message"`
Time string `json:"time"`
}
func JSONLog(level, message string) {
entry := LogEntry{
Level: level,
Message: message,
Time: time.Now().Format(time.RFC3339),
}
data, _ := json.Marshal(entry)
log.Println(string(data))
}
JSONLog("INFO", "Запуск сервера")
Вывод:
{"level":"INFO","message":"Запуск сервера","time":"2024-03-15T14:20:10Z"}
5.2. Уровни логирования
const (
LevelDebug = "DEBUG"
LevelInfo = "INFO"
LevelWarn = "WARN"
LevelError = "ERROR"
)
func Log(level, message string) {
log.Printf("[%s] %s", level, message)
}
Log(LevelError, "Сервер не отвечает")
Вывод:
[ERROR] Сервер не отвечает
6. Когда использовать log, а когда другие пакеты?
log — для простых задач (CLI-утилиты, маленькие сервисы).
slog (Go 1.21+) — структурированное логирование с уровнями.
zap/zerolog — для высоконагруженных приложений (минимальные аллокации).
Вывод
- Пакет
log прост, но гибок (префиксы, файлы, форматирование).
- Подходит для базового логирования.
- Для сложных сценариев лучше использовать
slog, zap или zerolog.
- Можно комбинировать с
io.MultiWriter для вывода в несколько мест.
Пример MultiWriter:
file, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
multiWriter := io.MultiWriter(os.Stdout, file)
log.SetOutput(multiWriter)
log.Println("Сообщение в консоль и файл")
4.19 - Описание пакета unsafe языка программирования Go
Пакет unsafe содержит операции, которые обходят типовую безопасность программ Go.
Пакеты, которые импортируют unsafe, могут быть непереносимыми и не защищены правилами совместимости Go 1.
Функции
func Alignof
func Alignof(x ArbitraryType) uintptr
Alignof принимает выражение x любого типа и возвращает требуемое выравнивание гипотетической переменной v, как если бы v была объявлена с помощью var v = x. Это наибольшее значение m, такое что адрес v всегда равен нулю по модулю m. Оно совпадает со значением, возвращаемым reflect.TypeOf(x).Align(). В качестве особого случая, если переменная s имеет тип struct, а f является полем в этой структуре, то Alignof(s.f) вернет требуемое выравнивание поля этого типа в структуре. Этот случай аналогичен значению, возвращаемому reflect.TypeOf(s.f).FieldAlign(). Возвращаемое значение Alignof является константой Go, если тип аргумента не имеет переменного размера. (Определение типов переменного размера см. в описании Sizeof.)
func Offsetof
func Offsetof(x ArbitraryType) uintptr
Offsetof возвращает смещение в структуре поля, представленного x, которое должно иметь форму structValue.field. Другими словами, она возвращает количество байтов между началом структуры и началом поля. Возвращаемое значение Offsetof является константой Go, если тип аргумента x не имеет переменного размера. (Определение типов переменного размера см. в описании Sizeof.)
func Sizeof
func Sizeof(x ArbitraryType) uintptr
Sizeof принимает выражение x любого типа и возвращает размер в байтах гипотетической переменной v, как если бы v была объявлена с помощью var v = x. Размер не включает в себя память, на которую может ссылаться x. Например, если x является срезом, Sizeof возвращает размер дескриптора среза, а не размер памяти, на которую ссылается срез; если x является интерфейсом, Sizeof возвращает размер самого значения интерфейса, а не размер значения, хранящегося в интерфейсе. Для структуры размер включает в себя любую заполняющую пробел, введенную выравниванием полей. Возвращаемое значение Sizeof является константой Go, если тип аргумента x не имеет переменного размера. (Тип имеет переменный размер, если он является параметром типа или если он является типом массива или структуры с элементами переменного размера).
func String
func String(ptr *byte, len IntegerType) string
String возвращает строковое значение, байты которого начинаются с ptr и длина которого равна len.
Аргумент len должен быть целочисленного типа или нетипизированной константой. Константа len должна быть неотрицательной и представляться значением типа int; если это константа без типа, ей присваивается тип int. Во время выполнения, если len отрицательна или если ptr равна nil, а len не равна нулю, возникает паника во время выполнения.
Поскольку строки Go являются неизменяемыми, байты, переданные в String, не должны изменяться, пока существует возвращаемое строковое значение.
6
func StringData
func StringData(str string) *byte
StringData возвращает указатель на базовые байты str. Для пустой строки возвращаемое значение не определено и может быть nil.
Поскольку строки Go являются неизменяемыми, байты, возвращаемые StringData, не должны изменяться.
Объяснение unsafe
Пакет unsafe в Go
Пакет unsafe предоставляет низкоуровневые операции, которые обходят безопасность типов Go. Он используется для взаимодействия с операционной системой, оптимизации производительности или работы с особыми структурами данных.
Основные функции пакета unsafe:
1. Alignof(x ArbitraryType) uintptr
Что делает: Возвращает требуемое выравнивание для типа переменной x.
Простыми словами: Компьютер оптимизирует доступ к памяти, размещая данные по определенным адресам (обычно кратным 2, 4, 8 и т.д.). Эта функция показывает, какое выравнивание требуется для типа.
Пример:
type Sample struct {
a bool // 1 байт
b int32 // 4 байта
}
fmt.Println(unsafe.Alignof(Sample{}.b)) // 4 (выравнивание для int32)
2. Offsetof(x ArbitraryType) uintptr
Что делает: Возвращает смещение (в байтах) поля в структуре.
Простыми словами: Показывает, на сколько байтов от начала структуры находится конкретное поле.
Пример:
type Sample struct {
a bool // 1 байт
b int32 // 4 байта
}
fmt.Println(unsafe.Offsetof(Sample{}.b)) // 4 (может быть 4 из-за выравнивания)
3. Sizeof(x ArbitraryType) uintptr
Что делает: Возвращает размер переменной в байтах.
Простыми словами: Показывает, сколько места в памяти занимает переменная.
Пример:
type Sample struct {
a bool // 1 байт
b int32 // 4 байта
}
fmt.Println(unsafe.Sizeof(Sample{})) // 8 (может быть 8 из-за выравнивания)
4. String(ptr *byte, len IntegerType) string
Что делает: Создает строку из указателя на байты и длины.
Простыми словами: Позволяет создать строку напрямую из участка памяти.
Пример:
bytes := []byte{'h', 'e', 'l', 'l', 'o'}
str := unsafe.String(&bytes[0], len(bytes))
fmt.Println(str) // "hello"
5. StringData(str string) *byte
Что делает: Возвращает указатель на базовые байты строки.
Простыми словами: Позволяет получить доступ к внутреннему представлению строки.
Пример:
str := "hello"
ptr := unsafe.StringData(str)
fmt.Println(*ptr) // 104 (байт 'h')
Когда использовать unsafe?
- Для оптимизации критического по производительности кода
- При работе с системными вызовами или аппаратным обеспечением
- Для сериализации/десериализации данных
- При реализации специальных структур данных
Важно: Использование unsafe может привести к нестабильности программы и проблемам с безопасностью. Применяйте только когда действительно необходимо и полностью понимаете последствия.
type ArbitraryType
ArbitraryType используется здесь только в целях документирования и на самом деле не входит в состав пакета unsafe. Он представляет тип произвольного выражения Go.
func Slice
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
Функция Slice возвращает срез, базовый массив которого начинается с ptr, а длина и емкость — len. Slice(ptr, len) эквивалентно
(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]
за исключением того, что в особом случае, если ptr равно nil, а len равно нулю, Slice возвращает nil.
Аргумент len должен быть целочисленного типа или нетипизированной константой. Константа len должна быть неотрицательной и представляться значением типа int; если это нетипизированная константа, ей присваивается тип int. Во время выполнения, если len отрицательно, или если ptr равно nil, а len не равно нулю, возникает паника во время выполнения.
func SliceData
func SliceData(slice []ArbitraryType) *ArbitraryType
SliceData возвращает указатель на базовый массив аргумента slice.
Если cap(slice) > 0, SliceData возвращает &slice[:1][0].
Если slice == nil, SliceData возвращает nil.
В противном случае SliceData возвращает не нулевой указатель на неуказанный адрес памяти.
type IntegerType
IntegerType используется здесь только для целей документирования и на самом деле не является частью пакета unsafe. Он представляет любой произвольный целочисленный тип.
type Pointer
type Pointer *ArbitraryType
Pointer представляет указатель на произвольный тип. Для типа Pointer доступны четыре специальные операции, которые недоступны для других типов:
- Значение указателя любого типа может быть преобразовано в Pointer.
- Pointer может быть преобразован в значение указателя любого типа.
- uintptr может быть преобразован в Pointer.
- Pointer может быть преобразован в uintptr.
Таким образом, Pointer позволяет программе обойти систему типов и читать и записывать произвольную память. Его следует использовать с особой осторожностью.
Следующие шаблоны, связанные с Pointer, являются действительными. Код, не использующий эти шаблоны, вероятно, является недействительным сегодня или станет недействительным в будущем. Даже действительные шаблоны, приведенные ниже, сопровождаются важными предостережениями.
Запуск «go vet» может помочь найти использования Pointer, которые не соответствуют этим шаблонам, но отсутствие сообщений от «go vet» не является гарантией того, что код является действительным.
Объяснение Pointer
Тип unsafe.Pointer в Go - простое объяснение
unsafe.Pointer - это специальный тип указателя в Go, который позволяет обходить систему типов и работать с памятью напрямую.
Простыми словами
Представьте, что обычные указатели в Go - это строгие охранники: они следят, чтобы вы обращались только к тем данным, тип которых точно соответствует. unsafe.Pointer - это универсальный пропуск, который позволяет вам обойти эти ограничения и работать с памятью напрямую.
Основные возможности
- Преобразование между разными типами указателей
- Преобразование указателя в
uintptr и обратно
- Работа с памятью на низком уровне
Примеры использования
1. Преобразование типов
package main
import (
"fmt"
"unsafe"
)
func main() {
var x int64 = 42
// Обычный указатель на int64
ptrInt := &x
// Преобразуем в unsafe.Pointer (универсальный указатель)
ptrUnsafe := unsafe.Pointer(ptrInt)
// Теперь можем преобразовать в указатель на другой тип
ptrFloat := (*float64)(ptrUnsafe)
fmt.Printf("Исходное значение: %d\n", x)
fmt.Printf("Как float64: %f\n", *ptrFloat)
}
2. Доступ к структурам
type SecretStruct struct {
a int
b string
c bool
}
func main() {
s := SecretStruct{a: 10, b: "secret", c: true}
// Получаем указатель на структуру
ptr := unsafe.Pointer(&s)
// Получаем доступ к полю 'b' (пропуская проверки типов)
bPtr := (*string)(unsafe.Add(ptr, unsafe.Offsetof(s.b)))
fmt.Println("Секретное поле b:", *bPtr) // Выведет: secret
}
3. Работа с массивами
func main() {
arr := [3]int{1, 2, 3}
// Получаем указатель на первый элемент
ptr := unsafe.Pointer(&arr[0])
// Преобразуем в указатель на байты
bytePtr := (*byte)(ptr)
// Теперь можем работать с памятью как с байтами
fmt.Printf("Первый байт: %x\n", *bytePtr)
}
Важные правила
- Безопасность:
unsafe.Pointer обходит проверки типов - вы полностью отвечаете за корректность операций.
- Совместимость: Гарантии работы есть только при преобразованиях:
- Указатель ↔ unsafe.Pointer ↔ uintptr
- Ограничения: Нельзя арифметику указателей делать напрямую - используйте
unsafe.Add и unsafe.Slice.
Когда это полезно?
- Взаимодействие с системными вызовами
- Высокопроизводительные операции
- Специальные структуры данных
- Работа с foreign function interface (FFI)
Предупреждение
unsafe называется “небезопасным” не просто так. Используйте только когда действительно необходимо и полностью понимаете последствия. Неправильное использование может привести к:
- Падениям программы
- Повреждению данных
- Проблемам с безопасностью
Лучшая практика - изолировать unsafe-код в отдельных пакетах с четкими контрактами.
1. Преобразование *T1 в Pointer в *T2.
При условии, что T2 не больше T1 и что оба имеют одинаковую структуру памяти, это преобразование позволяет переосмыслить данные одного типа как данные другого типа. Примером является реализация math.Float64bits:
func Float64bits(f float64) uint64
return *(*uint64)(unsafe.Pointer(&f))
}
2. Преобразование указателя в uintptr (но не обратно в указатель).
Преобразование указателя в uintptr дает адрес памяти указанного значения в виде целого числа. Обычно uintptr используется для вывода на печать.
Преобразование uintptr обратно в указатель в целом не допускается.
uintptr — это целое число, а не ссылка. Преобразование указателя в uintptr создает целое значение без семантики указателя. Даже если uintptr содержит адрес какого-либо объекта, сборщик мусора не обновит значение uintptr, если объект переместится, и uintptr не помешает объекту быть восстановленным.
Остальные шаблоны перечисляют единственные допустимые преобразования из uintptr в Pointer.
3. Преобразование Pointer в uintptr и обратно с помощью арифметики.
Если p указывает на выделенный объект, его можно продвинуть по объекту путем преобразования в uintptr, добавления смещения и преобразования обратно в Pointer.
p = unsafe.Pointer(uintptr(p) + offset)
Наиболее распространенное использование этого шаблона — доступ к полям в структуре или элементам массива:
// эквивалентно f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))
// эквивалентно e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))
Таким образом можно как добавлять, так и вычитать смещения из указателя. Также допустимо использовать &^ для округления указателей, обычно для выравнивания. Во всех случаях результат должен продолжать указывать на исходный выделенный объект.
В отличие от C, не допускается перемещение указателя за пределы его исходного выделения:
// НЕПРАВИЛЬНО: конечная точка находится за пределами выделенного пространства.
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))
// НЕПРАВИЛЬНО: конец указывает за пределы выделенного пространства.
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))
Обратите внимание, что оба преобразования должны появляться в одном выражении, с арифметическими операциями между ними:
// НЕПРАВИЛЬНО: uintptr не может быть сохранен в переменной
// до преобразования обратно в Pointer.
u := uintptr(p)
p = unsafe.Pointer(u + offset)
Обратите внимание, что указатель должен указывать на выделенный объект, поэтому он не может быть nil.
// НЕПРАВИЛЬНО: преобразование указателя nil
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)
4. Преобразование указателя в uintptr при вызове функций типа syscall.Syscall.
Функции Syscall в пакете syscall передают свои аргументы uintptr непосредственно операционной системе, которая затем, в зависимости от деталей вызова, может переинтерпретировать некоторые из них как указатели. То есть реализация системного вызова неявно преобразует определенные аргументы обратно из uintptr в указатель.
Если аргумент-указатель должен быть преобразован в uintptr для использования в качестве аргумента, это преобразование должно появиться в самом выражении вызова:
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
Компилятор обрабатывает указатель, преобразованный в uintptr в списке аргументов вызова функции, реализованной на ассемблере, путем обеспечения того, что ссылка на выделенный объект, если таковой имеется, сохраняется и не перемещается до завершения вызова, даже если по типам кажется, что объект больше не нужен во время вызова.
Чтобы компилятор распознал этот паттерн, преобразование должно появиться в списке аргументов:
// НЕПРАВИЛЬНО: uintptr не может быть сохранен в переменной
// до неявного преобразования обратно в Pointer во время системного вызова.
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))
5. Преобразование результата reflect.Value.Pointer или reflect.Value.UnsafeAddr из uintptr в Pointer.
Методы Value пакета reflect с именами Pointer и UnsafeAddr возвращают тип uintptr вместо unsafe.Pointer, чтобы вызывающие функции не могли изменить результат на произвольный тип без предварительного импорта «unsafe». Однако это означает, что результат является неустойчивым и должен быть преобразован в Pointer сразу после вызова, в том же выражении:
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
Как и в приведенных выше случаях, хранение результата до преобразования является недопустимым:
// НЕПРАВИЛЬНО: uintptr не может быть сохранен в переменной
// до преобразования обратно в Pointer.
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))
Как и в предыдущем случае, структуры данных reflect SliceHeader и StringHeader объявляют поле Data как uintptr, чтобы вызывающие функции не могли изменить результат на произвольный тип без предварительного импорта «unsafe». Однако это означает, что SliceHeader и StringHeader действительны только при интерпретации содержимого фактического значения slice или string.
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // случай 1
hdr.Data = uintptr(unsafe.Pointer(p)) // случай 6 (этот случай)
hdr.Len = n
В этом случае hdr.Data на самом деле является альтернативным способом ссылки на базовый указатель в заголовке строки, а не самой переменной uintptr.
В общем случае reflect.SliceHeader и reflect.StringHeader следует использовать только как *reflect.SliceHeader и *reflect.StringHeader, указывающие на фактические срезы или строки, но никогда как простые структуры. Программа не должна объявлять или выделять переменные этих типов структур.
// НЕПРАВИЛЬНО: непосредственно объявленный заголовок не будет содержать Data в качестве ссылки.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p, возможно, уже утрачен
func Add
func Add(ptr Pointer, len IntegerType) Pointer
Функция Add добавляет len к ptr и возвращает обновленный указатель Pointer(uintptr(ptr) + uintptr(len)). Аргумент len должен быть целочисленного типа или нетипизированной константой. Константный аргумент len должен быть представлен значением типа int; если он является нетипизированной константой, ему присваивается тип int. Правила допустимого использования Pointer по-прежнему применяются.
4.20 - Пакет time языка программирования Go
Пакет time предоставляет функции для измерения и отображения времени.
Календарные вычисления всегда основаны на григорианском календаре без учета високосных секунд.
Монотонные часы
Операционные системы предоставляют как “системные часы” (которые могут корректироваться для синхронизации времени), так и “монотонные часы” (которые не подвержены таким изменениям). Общее правило гласит: системные часы используются для определения текущего времени, а монотонные часы - для измерения временных интервалов. Вместо разделения API, в данном пакете Time, возвращаемый функцией time.Now, содержит показания как системных, так и монотонных часов. Последующие операции определения времени используют показания системных часов, тогда как операции измерения времени (в частности, сравнения и вычитания) используют показания монотонных часов.
Например, следующий код всегда вычисляет положительный интервал времени около 20 миллисекунд, даже если системные часы были изменены во время выполнения измеряемой операции:
start := time.Now()
... operation that takes 20 milliseconds ...
t := time.Now()
elapsed := t.Sub(start)
Другие идиомы, такие как time.Since(start), time.Until(deadline) и time.Now().Before(deadline), также устойчивы к сбросам системных часов.
Остальная часть этого раздела содержит точные детали использования монотонных часов в операциях, но для работы с пакетом понимание этих деталей не требуется.
Особенности работы с монотонными часами
-
Функция time.Now()
Возвращаемое значение Time содержит показания монотонных часов. Если время t включает монотонные часы:
t.Add добавляет длительность как к системным, так и к монотонным часам
t.AddDate, t.Round и t.Truncate работают только с системным временем и удаляют монотонные показания
t.In, t.Local и t.UTC также удаляют монотонные показания
- Стандартный способ удалить монотонные показания:
t = t.Round(0)
-
Сравнение времен
Если оба значения t и u содержат монотонные показания:
t.After(u), t.Before(u), t.Equal(u), t.Compare(u) и t.Sub(u) используют только монотонные часы
- Если одно из значений не имеет монотонных показаний, используются системные часы
-
Особые случаи
- На некоторых системах монотонные часы останавливаются при переходе в спящий режим
- В таких случаях
t.Sub(u) и аналогичные операции могут давать неточные результаты
- Иногда требуется удалить монотонные показания для получения точных данных
-
Сериализация и форматирование
Монотонные показания:
- Не включаются при сериализации (
GobEncode, MarshalJSON и др.)
- Не имеют формата вывода в
t.Format
- Не создаются конструкторами (
Date, Parse, Unix и др.)
-
Особенности реализации
- Монотонные показания существуют только в значениях
Time
- Не входят в
Duration или Unix-время (t.Unix)
- Оператор
== сравнивает Location и монотонные показания
-
Отладка
t.String() показывает монотонные показания при их наличии
- Различия в монотонных показаниях видны при выводе
Разрешение таймеров ¶
Разрешение таймеров зависит от:
- Версии Go
- Операционной системы
- Аппаратного обеспечения
Типичные значения:
- Unix: ~1 мс
- Windows 1803+: ~0.5 мс
- Старые версии Windows: ~16 мс (можно улучшить через
windows.TimeBeginPeriod)
Константы
const (
Layout = «01/02 03:04:05PM '06 -0700» // Время отсчета, в числовом порядке.
ANSIC = «Mon Jan _2 15:04:05 2006»
UnixDate = «Mon Jan _2 15:04:05 MST 2006»
RubyDate = «Mon Jan 02 15:04:05 -0700 2006»
RFC822 = «02 Jan 06 15:04 MST»
RFC822Z = «02 Jan 06 15:04 -0700» // RFC822 с числовой зоной
RFC850 = «Monday, 02-Jan-06 15:04:05 MST»
RFC1123 = «Mon, 02 Jan 2006 15:04:05 MST»
RFC1123Z = «Mon, 02 Jan 2006 15:04:05 -0700» // RFC1123 с числовой зоной
RFC3339 = «2006-01-02T15:04:05Z07:00»
RFC3339Nano = «2006-01-02T15:04:05.9999999Z07:00»
Kitchen = «3:04PM»
// Удобные временные метки.
Stamp = «Jan _2 15:04:05»
StampMilli = «Jan _2 15:04:05.000»
StampMicro = «Jan _2 15:04:05.000000»
StampNano = «Jan _2 15:04:05.000000000»
DateTime = «2006-01-02 15:04:05»
DateOnly = «2006-01-02»
TimeOnly = «15:04:05»
)
Это предопределенные макеты для использования в Time.Format и time.Parse. В качестве опорного времени в этих макетах используется конкретная временная метка:
01/02 03:04:05PM '06 -0700
(2 января, 15:04:05, 2006, в часовом поясе на семь часов западнее GMT). Это значение записывается как константа с именем Layout, приведенным ниже. В качестве времени Unix это 1136239445. Поскольку MST - это GMT-0700, команда Unix date выведет ссылку в виде:
Mon Jan 2 15:04:05 MST 2006.
Досадная историческая ошибка заключается в том, что в дате используется американская конвенция, согласно которой числовой месяц ставится перед днем.
Пример Time.Format подробно демонстрирует работу со строкой компоновки и является хорошей ссылкой.
Обратите внимание, что форматы RFC822, RFC850 и RFC1123 следует применять только к местному времени. При их использовании для времени UTC в качестве сокращения часового пояса будет использоваться «UTC», в то время как, строго говоря, эти RFC требуют использовать «GMT» в этом случае. При использовании форматов RFC1123 или RFC1123Z для разбора, обратите внимание, что эти форматы определяют ведущий ноль для части «день-месяц», что строго не разрешено RFC 1123. Это приведет к ошибке при разборе строк дат, которые приходятся на первые 9 дней данного месяца. В общем случае RFC1123Z следует использовать вместо RFC1123 для серверов, которые настаивают на этом формате, а RFC3339 следует предпочесть для новых протоколов. RFC3339, RFC822, RFC822Z, RFC1123 и RFC1123Z полезны для форматирования; при использовании с time.Parse они принимают не все форматы времени, разрешенные RFC, и принимают форматы времени, не определенные формально. Формат RFC3339Nano удаляет нули в конце поля секунд, поэтому после форматирования он может сортироваться некорректно.
Большинство программ могут использовать одну из определенных констант в качестве макета, передаваемого в Format или Parse. Остальную часть этого комментария можно игнорировать, если только вы не создаете собственную строку макета.
Чтобы определить свой собственный формат, запишите, как будет выглядеть эталонное время, отформатированное вашим способом; в качестве примера можно посмотреть значения таких констант, как ANSIC, StampMicro или Kitchen. Модель призвана продемонстрировать, как выглядит эталонное время, чтобы методы Format и Parse могли применить то же преобразование к общему значению времени.
Ниже приводится краткое описание компонентов строки макета. Каждый элемент показывает на примере форматирование элемента эталонного времени. Распознаются только эти значения. Текст в компоновочной строке, который не распознается как часть эталонного времени, дословно повторяется во время Format и, как ожидается, дословно отображается на входе в Parse.
Year: "2006" "06"
Month: "Jan" "January" "01" "1"
Day of the week: "Mon" "Monday"
Day of the month: "2" "_2" "02"
Day of the year: "__2" "002"
Hour: "15" "3" "03" (PM or AM)
Minute: "4" "04"
Second: "5" "05"
AM/PM mark: "PM"
Числовые смещения часовых поясов форматируются следующим образом:
"-0700" ±hhmm
"-07:00" ±hh:mm
"-07" ±hh
"-070000" ±hhmmss
"-07:00:00" ±hh:mm:ss
Если в формате заменить знак на Z, это включит режим ISO 8601, где для UTC отображается Z вместо указания смещения. Например:
"Z0700" Z or ±hhmm
"Z07:00" Z or ±hh:mm
"Z07" Z or ±hh
"Z070000" Z or ±hhmmss
"Z07:00:00" Z or ±hh:mm:ss
В строке формата подчёркивания в "_2" и "__2" обозначают пробелы, которые могут заменяться цифрами, если следующее число состоит из нескольких цифр, для совместимости с фиксированными форматами времени Unix. Ноль в начале означает дополнение нулями.
Форматы __2 и 002 обозначают день года, дополненный пробелами или нулями до трёх символов; формата без дополнения для дня года не существует.
Запятая или точка, за которыми следуют один или несколько нулей, обозначают долю секунды, выводимую с указанным количеством знаков после запятой. Запятая или точка, за которыми следуют одна или несколько девяток, обозначают долю секунды, выводимую с указанным количеством знаков после запятой, но с удалением завершающих нулей. Например, форматы "15:04:05,000" или "15:04:05.000" форматируют или парсят время с миллисекундной точностью.
Некоторые допустимые форматы являются недопустимыми значениями времени для time.Parse, из-за элементов вроде _ (пробельного дополнения) или Z (информации о временной зоне).
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
Стандартные длительности (Common durations)
Не существует определения для единиц измерения “День” или больше, чтобы избежать путаницы из-за перехода на летнее/зимнее время в разных часовых поясах.
Чтобы подсчитать количество единиц в Duration (длительности), выполните деление:
second := time.Second
fmt.Print(int64(second/time.Millisecond)) // prints 1000
Чтобы преобразовать целое количество единиц в Duration (длительность), выполните умножение:
seconds := 10
fmt.Print(time.Duration(seconds)*time.Second) // prints 10s
func After
func After(d Duration) <-chan Time
After ожидает истечения указанной длительности d, после чего отправляет текущее время в возвращаемом канале. Эквивалентно NewTimer(d).C.
Изменения в Go 1.23:
В ранних версиях документация предупреждала, что таймер не освобождался сборщиком мусора до срабатывания, и рекомендовала использовать NewTimer с ручным вызовом Timer.Stop для оптимизации. Начиная с Go 1.23, сборщик мусора может освобождать неиспользуемые таймеры, даже если они не остановлены. Нет необходимости использовать NewTimer вместо After.
Пример использования:
package main
import (
"fmt"
"time"
)
var c chan int
func handle(int) {}
func main() {
select {
case m := <-c:
handle(m)
case <-time.After(10 * time.Second):
fmt.Println("timed out")
}
}
func Sleep
Sleep приостанавливает выполнение текущей горутины как минимум на указанную длительность d. Отрицательное или нулевое значение приводит к немедленному возврату.
Пример:
package main
import (
"time"
)
func main() {
time.Sleep(100 * time.Millisecond) // Пауза на 100 мс
}
func Tick
func Tick(d Duration) <-chan Time
Tick — это удобная обёртка над NewTicker, предоставляющая только канал с тиками. В отличие от NewTicker, возвращает nil при d <= 0.
Изменения в Go 1.23:
Ранее документация указывала, что сборщик мусора не освобождает неостановленные тикеры, и рекомендовала использовать NewTicker с Ticker.Stop. В Go 1.23 сборщик может освобождать неиспользуемые тикеры, даже без вызова Stop. Нет причин предпочитать NewTicker, если достаточно Tick.
Пример использования:
package main
import (
"fmt"
"time"
)
func statusUpdate() string { return "" }
func main() {
c := time.Tick(5 * time.Second) // Тикер каждые 5 секунд
for next := range c {
fmt.Printf("%v %s\n", next, statusUpdate())
}
}
Вот перевод технической документации пакета time на русский язык:
Типы
type Duration
Тип Duration представляет промежуток времени между двумя моментами в виде количества наносекунд int64. Представление ограничивает максимальную продолжительность примерно 290 годами.
Пример
package main
import (
"fmt"
"time"
)
func expensiveCall() {}
func main() {
t0 := time.Now()
expensiveCall()
t1 := time.Now()
fmt.Printf("The call took %v to run.\n", t1.Sub(t0))
}
func ParseDuration
func ParseDuration(s string) (Duration, error)
ParseDuration разбирает строку продолжительности. Строка продолжительности - это возможно знаковая последовательность десятичных чисел, каждое с необязательной дробной частью и суффиксом единицы, например “300ms”, “-1.5h” или “2h45m”. Допустимые единицы времени: “ns”, “us” (или “µs”), “ms”, “s”, “m”, “h”.
Пример
import (
"fmt"
"time"
)
func main() {
hours, _ := time.ParseDuration("10h")
complex, _ := time.ParseDuration("1h10m10s")
micro, _ := time.ParseDuration("1µs")
// The package also accepts the incorrect but common prefix u for micro.
micro2, _ := time.ParseDuration("1us")
fmt.Println(hours)
fmt.Println(complex)
fmt.Printf("There are %.0f seconds in %v.\n", complex.Seconds(), complex)
fmt.Printf("There are %d nanoseconds in %v.\n", micro.Nanoseconds(), micro)
fmt.Printf("There are %6.2e seconds in %v.\n", micro2.Seconds(), micro2)
}
Output:
10h0m0s
1h10m10s
There are 4210 seconds in 1h10m10s.
There are 1000 nanoseconds in 1µs.
There are 1.00e-06 seconds in 1µs.
func Since
func Since(t Time) Duration
Since возвращает время, прошедшее с момента t. Это сокращение для time.Now().Sub(t).
Пример
package main
import (
"fmt"
"time"
)
func expensiveCall() {}
func main() {
start := time.Now()
expensiveCall()
elapsed := time.Since(start)
fmt.Printf("The call took %v to run.\n", elapsed)
}
func Until
func Until(t Time) Duration
Until возвращает продолжительность до момента t. Это сокращение для t.Sub(time.Now()).
Пример
package main
import (
"fmt"
"math"
"time"
)
func main() {
futureTime := time.Now().Add(5 * time.Second)
durationUntil := time.Until(futureTime)
fmt.Printf("Duration until future time: %.0f seconds", math.Ceil(durationUntil.Seconds()))
}
Output:
Duration until future time: 5 seconds
func (Duration) Abs
func (d Duration) Abs() Duration
Abs возвращает абсолютное значение d. В особом случае Duration(math.MinInt64) преобразуется в Duration(math.MaxInt64), уменьшая его величину на 1 наносекунду.
Пример
package main
import (
"fmt"
"math"
"time"
)
func main() {
positiveDuration := 5 * time.Second
negativeDuration := -3 * time.Second
minInt64CaseDuration := time.Duration(math.MinInt64)
absPositive := positiveDuration.Abs()
absNegative := negativeDuration.Abs()
absSpecial := minInt64CaseDuration.Abs() == time.Duration(math.MaxInt64)
fmt.Printf("Absolute value of positive duration: %v\n", absPositive)
fmt.Printf("Absolute value of negative duration: %v\n", absNegative)
fmt.Printf("Absolute value of MinInt64 equal to MaxInt64: %t\n", absSpecial)
}
Output:
Absolute value of positive duration: 5s
Absolute value of negative duration: 3s
Absolute value of MinInt64 equal to MaxInt64: true
func (Duration) Hours
func (d Duration) Hours() float64
Hours возвращает продолжительность как число часов с плавающей точкой.
Пример
package main
import (
"fmt"
"time"
)
func main() {
h, _ := time.ParseDuration("4h30m")
fmt.Printf("I've got %.1f hours of work left.", h.Hours())
}
Output:
I've got 4.5 hours of work left.
func (Duration) Microseconds
func (d Duration) Microseconds() int64
Microseconds возвращает продолжительность как целое количество микросекунд.
Пример
package main
import (
"fmt"
"time"
)
func main() {
u, _ := time.ParseDuration("1s")
fmt.Printf("One second is %d microseconds.\n", u.Microseconds())
}
Output:
One second is 1000000 microseconds.
func (Duration) Milliseconds
func (d Duration) Milliseconds() int64
Milliseconds возвращает продолжительность как целое количество миллисекунд.
func (Duration) Minutes
func (d Duration) Minutes() float64
Minutes возвращает продолжительность как число минут с плавающей точкой.
func (Duration) Nanoseconds
func (d Duration) Nanoseconds() int64
Nanoseconds возвращает продолжительность как целое количество наносекунд.
func (Duration) Round
func (d Duration) Round(m Duration) Duration
Round возвращает результат округления d до ближайшего кратного m. При округлении половинных значений округление выполняется от нуля. Если результат превышает максимальное (или минимальное) значение, которое может храниться в Duration, возвращается максимальная (или минимальная) продолжительность. Если m <= 0, Round возвращает d без изменений.
Пример
package main
import (
"fmt"
"time"
)
func main() {
d, err := time.ParseDuration("1h15m30.918273645s")
if err != nil {
panic(err)
}
round := []time.Duration{
time.Nanosecond,
time.Microsecond,
time.Millisecond,
time.Second,
2 * time.Second,
time.Minute,
10 * time.Minute,
time.Hour,
}
for _, r := range round {
fmt.Printf("d.Round(%6s) = %s\n", r, d.Round(r).String())
}
}
d.Round( 1ns) = 1h15m30.918273645s
d.Round( 1µs) = 1h15m30.918274s
d.Round( 1ms) = 1h15m30.918s
d.Round( 1s) = 1h15m31s
d.Round( 2s) = 1h15m30s
d.Round( 1m0s) = 1h16m0s
d.Round( 10m0s) = 1h20m0s
d.Round(1h0m0s) = 1h0m0s
func (Duration) Seconds
func (d Duration) Seconds() float64
Seconds возвращает продолжительность как число секунд с плавающей точкой.
func (Duration) String
func (d Duration) String() string
String возвращает строковое представление продолжительности в виде “72h3m0.5s”. Ведущие нулевые единицы опускаются. В особом случае продолжительности менее одной секунды используют меньшие единицы (милли-, микро- или наносекунды), чтобы ведущая цифра была не нулевой. Нулевая продолжительность форматируется как “0s”.
Пример
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(1*time.Hour + 2*time.Minute + 300*time.Millisecond)
fmt.Println(300 * time.Millisecond)
}
func (Duration) Truncate
func (d Duration) Truncate(m Duration) Duration
Truncate возвращает результат округления d к нулю до кратного m. Если m <= 0, Truncate возвращает d без изменений.
Пример
package main
import (
"fmt"
"time"
)
func main() {
d, err := time.ParseDuration("1h15m30.918273645s")
if err != nil {
panic(err)
}
trunc := []time.Duration{
time.Nanosecond,
time.Microsecond,
time.Millisecond,
time.Second,
2 * time.Second,
time.Minute,
10 * time.Minute,
time.Hour,
}
for _, t := range trunc {
fmt.Printf("d.Truncate(%6s) = %s\n", t, d.Truncate(t).String())
}
}
Output:
d.Truncate( 1ns) = 1h15m30.918273645s
d.Truncate( 1µs) = 1h15m30.918273s
d.Truncate( 1ms) = 1h15m30.918s
d.Truncate( 1s) = 1h15m30s
d.Truncate( 2s) = 1h15m30s
d.Truncate( 1m0s) = 1h15m0s
d.Truncate( 10m0s) = 1h10m0s
d.Truncate(1h0m0s) = 1h0m0s
type Location
type Location struct {
// содержит неэкспортируемые поля
}
Тип Location отображает моменты времени в часовом поясе, используемом в этот момент. Обычно Location представляет набор смещений времени, используемых в географическом регионе. Для многих Location смещение времени варьируется в зависимости от того, действует ли летнее время в данный момент.
Location используется для предоставления часового пояса в печатном значении Time и для вычислений, включающих интервалы, которые могут пересекать границы перехода на летнее время.
Пример
package main
import (
"fmt"
"time"
)
func main() {
// China doesn't have daylight saving. It uses a fixed 8 hour offset from UTC.
secondsEastOfUTC := int((8 * time.Hour).Seconds())
beijing := time.FixedZone("Beijing Time", secondsEastOfUTC)
// If the system has a timezone database present, it's possible to load a location
// from that, e.g.:
// newYork, err := time.LoadLocation("America/New_York")
// Creating a time requires a location. Common locations are time.Local and time.UTC.
timeInUTC := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC)
sameTimeInBeijing := time.Date(2009, 1, 1, 20, 0, 0, 0, beijing)
// Although the UTC clock time is 1200 and the Beijing clock time is 2000, Beijing is
// 8 hours ahead so the two dates actually represent the same instant.
timesAreEqual := timeInUTC.Equal(sameTimeInBeijing)
fmt.Println(timesAreEqual)
}
var Local *Location = &localLoc
Local представляет локальный часовой пояс системы. В Unix-системах Local обращается к переменной окружения TZ для определения используемого часового пояса. Отсутствие TZ означает использование системного значения по умолчанию /etc/localtime. TZ="" означает использование UTC. TZ=“foo” означает использование файла foo в системном каталоге часовых поясов.
var UTC *Location = &utcLoc
UTC представляет Универсальное Координированное Время (UTC).
func FixedZone
func FixedZone(name string, offset int) *Location
FixedZone возвращает Location, который всегда использует заданное имя пояса и смещение (в секундах к востоку от UTC).
Пример
package main
import (
"fmt"
"time"
)
func main() {
loc := time.FixedZone("UTC-8", -8*60*60)
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, loc)
fmt.Println("The time is:", t.Format(time.RFC822))
}
Output:
The time is: 10 Nov 09 23:00 UTC-8
func LoadLocation
func LoadLocation(name string) (*Location, error)
LoadLocation возвращает Location с заданным именем.
Если имя "" или “UTC”, LoadLocation возвращает UTC. Если имя “Local”, LoadLocation возвращает Local.
В противном случае имя считается именем местоположения, соответствующим файлу в базе данных часовых поясов IANA, например “America/New_York”.
LoadLocation ищет базу данных часовых поясов IANA в следующих местах по порядку:
- Каталог или распакованный zip-файл, указанный переменной окружения ZONEINFO
- В Unix-системах - стандартное системное местоположение установки
- $GOROOT/lib/time/zoneinfo.zip
- Пакет time/tzdata, если он был импортирован
Пример
package main
import (
"fmt"
"time"
)
func main() {
location, err := time.LoadLocation("America/Los_Angeles")
if err != nil {
panic(err)
}
timeInUTC := time.Date(2018, 8, 30, 12, 0, 0, 0, time.UTC)
fmt.Println(timeInUTC.In(location))
}
Output:
2018-08-30 05:00:00 -0700 PDT
func LoadLocationFromTZData
func LoadLocationFromTZData(name string, data []byte) (*Location, error)
LoadLocationFromTZData возвращает Location с заданным именем, инициализированный из данных в формате базы данных часовых поясов IANA. Данные должны быть в формате стандартного файла часового пояса IANA (например, содержимое /etc/localtime в Unix-системах).
func (*Location) String
func (l *Location) String() string
String возвращает описательное имя для информации о часовом поясе, соответствующее аргументу name для LoadLocation или FixedZone.
type Month
Month определяет месяц года (January = 1, …).
Пример
package main
import (
"fmt"
"time"
)
func main() {
_, month, day := time.Now().Date()
if month == time.November && day == 10 {
fmt.Println("Happy Go day!")
}
}
Константы:
const (
January Month = 1 + iota
February
March
April
May
June
July
August
September
October
November
December
)
func (Month) String
func (m Month) String() string
String возвращает английское название месяца (“January”, “February”, …).
type ParseError
type ParseError struct {
Layout string
Value string
LayoutElem string
ValueElem string
Message string
}
ParseError описывает проблему при разборе строки времени.
func (*ParseError) Error
func (e *ParseError) Error() string
Error возвращает строковое представление ParseError.
type Ticker
type Ticker struct {
C <-chan Time // Канал, по которому доставляются "тики"
// содержит неэкспортируемые поля
}
Ticker содержит канал, который доставляет “тики” часов с заданными интервалами.
Объяснение Ticker
Тип Ticker в Go
Тип Ticker в пакете time представляет собой механизм для получения повторяющихся сигналов (“тиков”) через регулярные промежутки времени. Это полезный инструмент для выполнения периодических операций.
Основные характеристики:
-
Структура:
type Ticker struct {
C <-chan Time // Канал для получения тиков
// скрытые поля
}
- Поле
C - это канал только для чтения (<-chan), по которому будут приходить значения времени при каждом тике
-
Назначение:
- Регулярное выполнение кода (например, каждые 5 секунд)
- Создание таймеров с повторяющимся срабатыванием
- Периодический опрос или проверка состояния
- Реализация heartbeat-механизмов
-
Типичное использование:
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
fmt.Println("Tick at", t)
// выполнить периодическую операцию
}
}
-
Особенности:
- Тикер продолжает работать, пока не будет явно остановлен
- Интервал между тиками остается постоянным (в отличие от
time.Sleep в цикле)
- Если получатель не успевает обрабатывать тики, они могут быть пропущены
-
Важные методы:
NewTicker(d Duration) - создает новый тикер
Stop() - останавливает тикер
Reset(d Duration) - изменяет интервал (добавлен в Go 1.15)
Когда использовать:
- Для регулярных фоновых задач (например, обновление кэша)
- Для реализации таймаутов с повторением
- Для периодического сбора метрик или логирования
- В долгоживущих горутинах для поддержания активности
Пример реального использования:
func startMonitoring() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
err := checkSystemHealth()
if err != nil {
log.Printf("Health check failed: %v", err)
}
case <-ctx.Done():
return // завершение при отмене контекста
}
}
}
Важно всегда вызывать Stop() для тикера, когда он больше не нужен, чтобы избежать утечек ресурсов. В современных версиях Go (1.23+) сборщик мусора может автоматически очищать неиспользуемые тикеры, но явная остановка остается хорошей практикой.
func NewTicker
func NewTicker(d Duration) *Ticker
NewTicker создает новый Ticker, содержащий канал, который будет отправлять текущее время на канал после каждого тика. Период тиков задается аргументом duration. Ticker будет корректировать интервал времени или пропускать тики для компенсации медленных получателей. Длительность d должна быть больше нуля; в противном случае NewTicker вызовет панику.
Пример
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
done := make(chan bool)
go func() {
time.Sleep(10 * time.Second)
done <- true
}()
for {
select {
case <-done:
fmt.Println("Done!")
return
case t := <-ticker.C:
fmt.Println("Current time: ", t)
}
}
}
func (*Ticker) Reset
func (t *Ticker) Reset(d Duration)
Reset останавливает тикер и устанавливает его период в заданную продолжительность. Следующий тик придет после истечения нового периода. Длительность d должна быть больше нуля; в противном случае Reset вызовет панику.
func (*Ticker) Stop
Stop выключает тикер. После Stop больше не будет отправляться тиков. Stop не закрывает канал, чтобы предотвратить ошибочное чтение “тика” параллельной горутиной.
type Time
type Time struct {
// содержит неэкспортируемые поля
}
Time представляет момент времени с наносекундной точностью.
Программы, использующие время, обычно должны хранить и передавать его как значения, а не указатели. То есть переменные времени и поля структур должны быть типа time.Time, а не *time.Time.
Значение Time может использоваться несколькими горутинами одновременно, за исключением того, что методы Time.GobDecode, Time.UnmarshalBinary, Time.UnmarshalJSON и Time.UnmarshalText не являются безопасными для конкурентного использования.
Моменты времени можно сравнивать с помощью методов Time.Before, Time.After и Time.Equal. Метод Time.Sub вычитает два момента, производя Duration. Метод Time.Add добавляет Time и Duration, производя Time.
Нулевое значение типа Time - January 1, year 1, 00:00:00.000000000 UTC. Поскольку это время вряд ли встретится на практике, метод Time.IsZero дает простой способ обнаружения времени, которое не было инициализировано явно.
Каждое время имеет связанный Location. Методы Time.Local, Time.UTC и Time.In возвращают Time с конкретным Location. Изменение Location значения Time с помощью этих методов не изменяет фактический момент, который оно представляет, только часовой пояс, в котором оно интерпретируется.
func Date
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time
Date возвращает Time, соответствующее
yyyy-mm-dd hh:mm:ss + nsec наносекунд
в соответствующем часовом поясе для этого времени в заданном местоположении.
Значения month, day, hour, min, sec и nsec могут быть вне их обычных диапазонов и будут нормализованы во время преобразования. Например, 32 октября преобразуется в 1 ноября.
Переход на летнее время пропускает или повторяет моменты времени. Например, в США March 13, 2011 2:15am никогда не происходило, а November 6, 2011 1:15am произошло дважды. В таких случаях выбор часового пояса, а следовательно и времени, не определен однозначно. Date возвращает время, которое корректно в одном из двух участвующих в переходе часовых поясов, но не гарантирует, в каком именно.
Date вызывает панику, если loc равен nil.
Пример
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Printf("Go launched at %s\n", t.Local())
}
Output:
Go launched at 2009-11-10 15:00:00 -0800 PST
func Now
Now возвращает текущее локальное время.
func Parse
func Parse(layout, value string) (Time, error)
Parse разбирает форматированную строку и возвращает значение времени, которое она представляет. Смотрите документацию для константы Layout, чтобы понять, как представить формат. Второй аргумент должен быть разбираемым с использованием строки формата (layout), предоставленной в качестве первого аргумента.
Пример
package main
import (
"fmt"
"time"
)
func main() {
// Пример с Time.Format содержит подробное описание того, как
// определять строку формата для разбора значения time.Time;
// Parse и Format используют одну и ту же модель для описания
// входных и выходных данных.
// longForm демонстрирует на примере, как эталонное время будет представлено
// в желаемом формате.
const longForm = "Jan 2, 2006 at 3:04pm (MST)"
t, _ := time.Parse(longForm, "Feb 3, 2013 at 7:54pm (PST)")
fmt.Println(t)
// shortForm - это альтернативный способ представления эталонного времени
// в желаемом формате; он не содержит информации о часовом поясе.
// Примечание: без явного указания пояса возвращает время в UTC.
const shortForm = "2006-Jan-02"
t, _ = time.Parse(shortForm, "2013-Feb-03")
fmt.Println(t)
// Некоторые допустимые форматы могут быть недопустимыми значениями времени,
// из-за спецификаторов формата, таких как _ для заполнения пробелами
// и Z для информации о часовом поясе.
// Например, формат RFC3339 2006-01-02T15:04:05Z07:00
// содержит как Z, так и смещение часового пояса, чтобы обрабатывать оба варианта:
// 2006-01-02T15:04:05Z
// 2006-01-02T15:04:05+07:00
t, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
fmt.Println(t)
t, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
fmt.Println(t)
_, err := time.Parse(time.RFC3339, time.RFC3339)
fmt.Println("ошибка", err) // Возвращает ошибку, так как формат не является допустимым значением времени
}
Output:
2013-02-03 19:54:00 -0800 PST
2013-02-03 00:00:00 +0000 UTC
2006-01-02 15:04:05 +0000 UTC
2006-01-02 15:04:05 +0700 +0700
error parsing time "2006-01-02T15:04:05Z07:00": extra text: "07:00"
func ParseInLocation
func ParseInLocation(layout, value string, loc *Location) (Time, error)
ParseInLocation похож на Parse, но отличается в двух важных аспектах. Во-первых, при отсутствии информации о часовом поясе Parse интерпретирует время как UTC; ParseInLocation интерпретирует время как в заданном местоположении. Во-вторых, при заданном смещении или аббревиатуре часового пояса Parse пытается сопоставить его с местоположением Local; ParseInLocation использует заданное местоположение.
Пример
package main
import (
"fmt"
"time"
)
func main() {
loc, _ := time.LoadLocation("Europe/Berlin")
// This will look for the name CEST in the Europe/Berlin time zone.
const longForm = "Jan 2, 2006 at 3:04pm (MST)"
t, _ := time.ParseInLocation(longForm, "Jul 9, 2012 at 5:02am (CEST)", loc)
fmt.Println(t)
// Note: without explicit zone, returns time in given location.
const shortForm = "2006-Jan-02"
t, _ = time.ParseInLocation(shortForm, "2012-Jul-09", loc)
fmt.Println(t)
}
Output:
2012-07-09 05:02:00 +0200 CEST
2012-07-09 00:00:00 +0200 CEST
func Unix
func Unix(sec int64, nsec int64) Time
Unix возвращает локальное Time, соответствующее заданному Unix-времени, sec секунд и nsec наносекунд с 1 января 1970 UTC. Допустимо передавать nsec вне диапазона [0, 999999999]. Не все значения sec имеют соответствующее значение времени. Одним из таких значений является 1«63-1 (максимальное значение int64).
Пример
package main
import (
"fmt"
"time"
)
func main() {
unixTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Println(unixTime.Unix())
t := time.Unix(unixTime.Unix(), 0).UTC()
fmt.Println(t)
}
Output:
1257894000
2009-11-10 23:00:00 +0000 UTC
func UnixMicro
func UnixMicro(usec int64) Time
UnixMicro возвращает локальное Time, соответствующее заданному Unix-времени, usec микросекунд с 1 января 1970 UTC.
Пример
package main
import (
"fmt"
"time"
)
func main() {
umt := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Println(umt.UnixMicro())
t := time.UnixMicro(umt.UnixMicro()).UTC()
fmt.Println(t)
}
Output:
1257894000000000
2009-11-10 23:00:00 +0000 UTC
func UnixMilli
func UnixMilli(msec int64) Time
UnixMilli возвращает локальное Time, соответствующее заданному Unix-времени, msec миллисекунд с 1 января 1970 UTC.
func (Time) Add
func (t Time) Add(d Duration) Time
Add возвращает время t+d.
Пример
import (
"fmt"
"time"
)
func main() {
start := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC)
afterTenSeconds := start.Add(time.Second * 10)
afterTenMinutes := start.Add(time.Minute * 10)
afterTenHours := start.Add(time.Hour * 10)
afterTenDays := start.Add(time.Hour * 24 * 10)
fmt.Printf("start = %v\n", start)
fmt.Printf("start.Add(time.Second * 10) = %v\n", afterTenSeconds)
fmt.Printf("start.Add(time.Minute * 10) = %v\n", afterTenMinutes)
fmt.Printf("start.Add(time.Hour * 10) = %v\n", afterTenHours)
fmt.Printf("start.Add(time.Hour * 24 * 10) = %v\n", afterTenDays)
}
Output:
start = 2009-01-01 12:00:00 +0000 UTC
start.Add(time.Second * 10) = 2009-01-01 12:00:10 +0000 UTC
start.Add(time.Minute * 10) = 2009-01-01 12:10:00 +0000 UTC
start.Add(time.Hour * 10) = 2009-01-01 22:00:00 +0000 UTC
start.Add(time.Hour * 24 * 10) = 2009-01-11 12:00:00 +0000 UTC
func (Time) AddDate
func (t Time) AddDate(years int, months int, days int) Time
AddDate возвращает время, соответствующее добавлению заданного количества лет, месяцев и дней к t. Например, AddDate(-1, 2, 3), примененное к 1 января 2011, возвращает 4 марта 2010.
Пример
package main
import (
"fmt"
"time"
)
func main() {
start := time.Date(2023, 03, 25, 12, 0, 0, 0, time.UTC)
oneDayLater := start.AddDate(0, 0, 1)
dayDuration := oneDayLater.Sub(start)
oneMonthLater := start.AddDate(0, 1, 0)
oneYearLater := start.AddDate(1, 0, 0)
zurich, err := time.LoadLocation("Europe/Zurich")
if err != nil {
panic(err)
}
// This was the day before a daylight saving time transition in Zürich.
startZurich := time.Date(2023, 03, 25, 12, 0, 0, 0, zurich)
oneDayLaterZurich := startZurich.AddDate(0, 0, 1)
dayDurationZurich := oneDayLaterZurich.Sub(startZurich)
fmt.Printf("oneDayLater: start.AddDate(0, 0, 1) = %v\n", oneDayLater)
fmt.Printf("oneMonthLater: start.AddDate(0, 1, 0) = %v\n", oneMonthLater)
fmt.Printf("oneYearLater: start.AddDate(1, 0, 0) = %v\n", oneYearLater)
fmt.Printf("oneDayLaterZurich: startZurich.AddDate(0, 0, 1) = %v\n", oneDayLaterZurich)
fmt.Printf("Day duration in UTC: %v | Day duration in Zürich: %v\n", dayDuration, dayDurationZurich)
}
Output:
oneDayLater: start.AddDate(0, 0, 1) = 2023-03-26 12:00:00 +0000 UTC
oneMonthLater: start.AddDate(0, 1, 0) = 2023-04-25 12:00:00 +0000 UTC
oneYearLater: start.AddDate(1, 0, 0) = 2024-03-25 12:00:00 +0000 UTC
oneDayLaterZurich: startZurich.AddDate(0, 0, 1) = 2023-03-26 12:00:00 +0200 CEST
Day duration in UTC: 24h0m0s | Day duration in Zürich: 23h0m0s
func (Time) After
func (t Time) After(u Time) bool
After сообщает, является ли момент времени t после u.
Пример
package main
import (
"fmt"
"time"
)
func main() {
year2000 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
year3000 := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)
isYear3000AfterYear2000 := year3000.After(year2000) // True
isYear2000AfterYear3000 := year2000.After(year3000) // False
fmt.Printf("year3000.After(year2000) = %v\n", isYear3000AfterYear2000)
fmt.Printf("year2000.After(year3000) = %v\n", isYear2000AfterYear3000)
}
Output:
year3000.After(year2000) = true
year2000.After(year3000) = false
func (Time) AppendBinary
func (t Time) AppendBinary(b []byte) ([]byte, error)
AppendBinary реализует интерфейс encoding.BinaryAppender. Добавляет бинарное представление времени в срез байт b и возвращает расширенный буфер.
func (t Time) AppendFormat(b []byte, layout string) []byte
AppendFormat аналогичен Time.Format, но добавляет текстовое представление времени в буфер b и возвращает расширенный буфер. Формат определяется параметром layout.
Пример
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2017, time.November, 4, 11, 0, 0, 0, time.UTC)
text := []byte("Time: ")
text = t.AppendFormat(text, time.Kitchen)
fmt.Println(string(text))
}
func (Time) AppendText
func (t Time) AppendText(b []byte) ([]byte, error)
AppendText реализует интерфейс encoding.TextAppender. Форматирует время в формате RFC 3339 с субсекундной точностью и добавляет в буфер b. Возвращает ошибку, если время невозможно представить в корректном формате RFC 3339 (например, при выходе года за допустимый диапазон).
func (Time) Before
func (t Time) Before(u Time) bool
Before сообщает, является ли момент времени t до u.
Пример
ackage main
import (
"fmt"
"time"
)
func main() {
year2000 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
year3000 := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)
isYear2000BeforeYear3000 := year2000.Before(year3000) // True
isYear3000BeforeYear2000 := year3000.Before(year2000) // False
fmt.Printf("year2000.Before(year3000) = %v\n", isYear2000BeforeYear3000)
fmt.Printf("year3000.Before(year2000) = %v\n", isYear3000BeforeYear2000)
}
Output:
year2000.Before(year3000) = true
year3000.Before(year2000) = false
func (Time) Clock
func (t Time) Clock() (hour, min, sec int)
Clock возвращает час, минуту и секунду в пределах дня, указанного t.
func (Time) Compare
func (t Time) Compare(u Time) int
Compare сравнивает момент времени t с u. Если t до u, возвращает -1; если t после u, возвращает +1; если они одинаковы, возвращает 0.
func (Time) Date
func (t Time) Date() (year int, month Month, day int)
Date возвращает год, месяц и день, в которые происходит t.
Пример
package main
import (
"fmt"
"time"
)
func main() {
d := time.Date(2000, 2, 1, 12, 30, 0, 0, time.UTC)
year, month, day := d.Date()
fmt.Printf("year = %v\n", year)
fmt.Printf("month = %v\n", month)
fmt.Printf("day = %v\n", day)
}
Output:
year = 2000
month = February
day = 1
func (Time) Day
Day возвращает день месяца, указанный t.
Пример
package main
import (
"fmt"
"time"
)
func main() {
d := time.Date(2000, 2, 1, 12, 30, 0, 0, time.UTC)
day := d.Day()
fmt.Printf("day = %v\n", day)
}
func (Time) Equal
func (t Time) Equal(u Time) bool
Equal сообщает, представляют ли t и u один и тот же момент времени. Два времени могут быть равны, даже если они находятся в разных местоположениях. Например, 6:00 +0200 и 4:00 UTC равны.
Пример
package main
import (
"fmt"
"time"
)
func main() {
secondsEastOfUTC := int((8 * time.Hour).Seconds())
beijing := time.FixedZone("Beijing Time", secondsEastOfUTC)
// Unlike the equal operator, Equal is aware that d1 and d2 are the
// same instant but in different time zones.
d1 := time.Date(2000, 2, 1, 12, 30, 0, 0, time.UTC)
d2 := time.Date(2000, 2, 1, 20, 30, 0, 0, beijing)
datesEqualUsingEqualOperator := d1 == d2
datesEqualUsingFunction := d1.Equal(d2)
fmt.Printf("datesEqualUsingEqualOperator = %v\n", datesEqualUsingEqualOperator)
fmt.Printf("datesEqualUsingFunction = %v\n", datesEqualUsingFunction)
}
Output:
datesEqualUsingEqualOperator = false
datesEqualUsingFunction = true
func (t Time) Format(layout string) string
Format возвращает текстовое представление значения времени, отформатированное согласно layout. Смотрите документацию для константы Layout, чтобы понять, как представить формат.
Пример
package main
import (
"fmt"
"time"
)
func main() {
// Разбираем строку времени в стандартном Unix формате
t, err := time.Parse(time.UnixDate, "Wed Feb 25 11:06:39 PST 2015")
if err != nil { // Всегда проверяем ошибки, даже если они не должны возникать
panic(err)
}
tz, err := time.LoadLocation("Asia/Shanghai")
if err != nil { // Всегда проверяем ошибки
panic(err)
}
// Метод Stringer типа time.Time удобен для вывода без указания формата
fmt.Println("формат по умолчанию:", t)
// Предопределенные константы пакета реализуют распространенные форматы
fmt.Println("Unix формат:", t.Format(time.UnixDate))
// Часовой пояс, связанный со значением времени, влияет на его вывод
fmt.Println("То же время в UTC:", t.UTC().Format(time.UnixDate))
fmt.Println("в Шанхае с секундами:", t.In(tz).Format("2006-01-02T15:04:05 -070000"))
fmt.Println("в Шанхае с секундами и двоеточиями:", t.In(tz).Format("2006-01-02T15:04:05 -07:00:00"))
// Оставшаяся часть функции демонстрирует свойства
// строки формата, используемой в Format
// Строка формата, используемая функциями Parse и Format,
// на примере показывает, как должно быть представлено эталонное время.
// Важно подчеркнуть, что нужно показывать, как форматируется именно эталонное время,
// а не произвольное время пользователя. Таким образом, каждая строка формата -
// это представление временной метки:
// Jan 2 15:04:05 2006 MST
// Простой способ запомнить это значение - заметить, что при таком порядке
// оно содержит следующие значения (соответствующие элементам выше):
// 1 2 3 4 5 6 -7
// Ниже показаны некоторые особенности.
// Большинство использований Format и Parse применяют константные строки формата,
// такие как определенные в этом пакете, но интерфейс гибкий,
// как показывают следующие примеры.
// Вспомогательная функция для красивого вывода примеров
do := func(name, layout, want string) {
got := t.Format(layout)
if want != got {
fmt.Printf("ошибка: для %q получено %q; ожидалось %q\n", layout, got, want)
return
}
fmt.Printf("%-16s %q дает %q\n", name, layout, got)
}
// Заголовок для вывода
fmt.Printf("\nФорматы:\n\n")
// Простые начальные примеры
do("Полная дата", "Mon Jan 2 15:04:05 MST 2006", "Wed Feb 25 11:06:39 PST 2015")
do("Краткая дата", "2006/01/02", "2015/02/25")
// Час эталонного времени - 15 (3PM). Формат может выражать это
// любым способом, и так как наше значение утреннее, мы должны видеть его
// в AM-формате. Покажем оба варианта в одной строке формата. И в нижнем регистре.
do("AM/PM", "3PM==3pm==15h", "11AM==11am==11h")
// При разборе, если за секундами следует точка
// и цифры, это воспринимается как доля секунды, даже если
// строка формата не включает представление долей секунды.
// Добавим доли секунды к нашему значению времени
t, err = time.Parse(time.UnixDate, "Wed Feb 25 11:06:39.1234 PST 2015")
if err != nil {
panic(err)
}
// Доли секунды не отображаются, если строка формата не содержит
// их представления
do("Без долей секунды", time.UnixDate, "Wed Feb 25 11:06:39 PST 2015")
// Доли секунды можно вывести, добавив последовательность 0 или 9
// после точки в значении секунд в строке формата.
// Если используются 0, выводится указанное количество знаков.
// Обратите внимание, что вывод содержит завершающий ноль.
do("0 для долей секунды", "15:04:05.00000", "11:06:39.12340")
// Если используются 9, завершающие нули отбрасываются.
do("9 для долей секунды", "15:04:05.99999999", "11:06:39.1234")
}
формат по умолчанию: 2015-02-25 11:06:39 -0800 PST
Unix формат: Wed Feb 25 11:06:39 PST 2015
То же время в UTC: Wed Feb 25 19:06:39 UTC 2015
в Шанхае с секундами: 2015-02-26T03:06:39 +080000
в Шанхае с секундами и двоеточиями: 2015-02-26T03:06:39 +08:00:00
Форматы:
Полная дата "Mon Jan 2 15:04:05 MST 2006" дает "Wed Feb 25 11:06:39 PST 2015"
Краткая дата "2006/01/02" дает "2015/02/25"
AM/PM "3PM==3pm==15h" дает "11AM==11am==11h"
Без долей секунды "Mon Jan _2 15:04:05 MST 2006" дает "Wed Feb 25 11:06:39 PST 2015"
0 для долей секунды "15:04:05.00000" дает "11:06:39.12340"
9 для долей секунды "15:04:05.99999999" дает "11:06:39.1234"
Пример (Pad)
package main
import (
"fmt"
"time"
)
func main() {
// Разбор строки времени в стандартном Unix формате
t, err := time.Parse(time.UnixDate, "Sat Mar 7 11:06:39 PST 2015")
if err != nil { // Всегда проверяем ошибки, даже если они маловероятны
panic(err)
}
// Вспомогательная функция для форматированного вывода примеров
do := func(name, layout, want string) {
got := t.Format(layout)
if want != got {
fmt.Printf("ошибка: для формата %q получено %q; ожидалось %q\n", layout, got, want)
return
}
fmt.Printf("%-16s %q → %q\n", name, layout, got)
}
// Предопределенная константа Unix использует подчеркивание для выравнивания дня
do("Unix формат", time.UnixDate, "Sat Mar 7 11:06:39 PST 2015")
// Для выравнивания значений переменной ширины (например, дня месяца)
// используйте _ вместо пробела в строке формата
do("Без выравнивания", "<2>", "<7>")
// Подчеркивание добавляет пробел для однозначных чисел
do("С пробелом", "<_2>", "< 7>")
// "0" добавляет нулевое заполнение для однозначных чисел
do("С нулями", "<02>", "<07>")
// Если значение уже имеет нужную ширину, заполнение не применяется
// Например, секунды (39) не требуют заполнения, а минуты (06) - требуют
do("Пропуск заполнения", "04:05", "06:39")
}
Unix формат "Mon Jan _2 15:04:05 MST 2006" → "Sat Mar 7 11:06:39 PST 2015"
Без выравнивания "<2>" → "<7>"
С пробелом "<_2>" → "< 7>"
С нулями "<02>" → "<07>"
Пропуск заполнения "04:05" → "06:39"
func (Time) GoString
func (t Time) GoString() string
GoString реализует fmt.GoStringer и форматирует t для вывода в исходном коде Go.
Пример
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Println(t.GoString())
t = t.Add(1 * time.Minute)
fmt.Println(t.GoString())
t = t.AddDate(0, 1, 0)
fmt.Println(t.GoString())
t, _ = time.Parse("Jan 2, 2006 at 3:04pm (MST)", "Feb 3, 2013 at 7:54pm (UTC)")
fmt.Println(t.GoString())
}
Output:
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
time.Date(2009, time.November, 10, 23, 1, 0, 0, time.UTC)
time.Date(2009, time.December, 10, 23, 1, 0, 0, time.UTC)
time.Date(2013, time.February, 3, 19, 54, 0, 0, time.UTC)
func (*Time) GobDecode
func (t *Time) GobDecode(data []byte) error
GobDecode реализует интерфейс gob.GobDecoder.
func (Time) GobEncode
func (t Time) GobEncode() ([]byte, error)
GobEncode реализует интерфейс gob.GobEncoder.
func (Time) Hour
Hour возвращает час в пределах дня, указанного t, в диапазоне [0, 23].
func (Time) ISOWeek
func (t Time) ISOWeek() (year, week int)
ISOWeek возвращает год и номер недели ISO 8601, в которые происходит t. Неделя находится в диапазоне от 1 до 53. 1-3 января года n могут принадлежать 52 или 53 неделе года n-1, а 29-31 декабря могут принадлежать 1 неделе года n+1.
func (Time) In
func (t Time) In(loc *Location) Time
In возвращает копию t, представляющую тот же момент времени, но с информацией о местоположении, установленной в loc для целей отображения.
In вызывает панику, если loc равен nil.
func (Time) IsDST
func (t Time) IsDST() bool
IsDST сообщает, находится ли время в настроенном местоположении в летнем времени.
func (Time) IsZero
func (t Time) IsZero() bool
IsZero сообщает, представляет ли t нулевой момент времени, 1 января, год 1, 00:00:00 UTC.
func (Time) Local
func (t Time) Local() Time
Local возвращает t с местоположением, установленным в локальное время.
func (Time) Location
func (t Time) Location() *Location
Location возвращает информацию о часовом поясе, связанную с t.
func (Time) MarshalBinary
func (t Time) MarshalBinary() ([]byte, error)
MarshalBinary реализует интерфейс encoding.BinaryMarshaler.
func (Time) MarshalJSON
func (t Time) MarshalJSON() ([]byte, error)
MarshalJSON реализует интерфейс encoding/json.Marshaler. Время — это строка в формате RFC 3339 с точностью до секунды. Если временная метка не может быть представлена как допустимая RFC 3339 (например, год выходит за пределы диапазона), то выдается сообщение об ошибке.
func (Time) MarshalText
func (t Time) MarshalText() ([]byte, error)
MarshalText реализует интерфейс encoding.TextMarshaler. Вывод соответствует вызову метода Time.AppendText.
См. Time.AppendText для получения дополнительной информации.
func (Time) Minute
func (t Time) Minute() int
Minute возвращает минуту в пределах часа, указанного t, в диапазоне [0, 59].
func (Time) Month
func (t Time) Month() Month
Month возвращает месяц года, указанный t.
func (Time) Nanosecond
func (t Time) Nanosecond() int
Nanosecond возвращает наносекунду в пределах секунды, указанной t, в диапазоне [0, 999999999].
func (Time) Round
func (t Time) Round(d Duration) Time
Round возвращает результат округления t до ближайшего кратного d (с нулевого времени). При округлении половинных значений округление выполняется от нуля. Если d <= 0, Round возвращает t, лишенное любого монотонного показания часов, но в остальном неизменное.
Пример
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(0, 0, 0, 12, 15, 30, 918273645, time.UTC)
round := []time.Duration{
time.Nanosecond,
time.Microsecond,
time.Millisecond,
time.Second,
2 * time.Second,
time.Minute,
10 * time.Minute,
time.Hour,
}
for _, d := range round {
fmt.Printf("t.Round(%6s) = %s\n", d, t.Round(d).Format("15:04:05.999999999"))
}
}
Output:
t.Round( 1ns) = 12:15:30.918273645
t.Round( 1µs) = 12:15:30.918274
t.Round( 1ms) = 12:15:30.918
t.Round( 1s) = 12:15:31
t.Round( 2s) = 12:15:30
t.Round( 1m0s) = 12:16:00
t.Round( 10m0s) = 12:20:00
t.Round(1h0m0s) = 12:00:00
func (Time) Second
func (t Time) Second() int
Second возвращает секунду в пределах минуты, указанной t, в диапазоне [0, 59].
func (Time) String
func (t Time) String() string
String возвращает время, отформатированное с использованием строки формата
"2006-01-02 15:04:05.999999999 -0700 MST"
Если время имеет монотонное показание часов, возвращаемая строка включает конечное поле “m=±”, где value - монотонное показание часов, отформатированное как десятичное число секунд.
Пример
import (
"fmt"
"time"
)
func main() {
timeWithNanoseconds := time.Date(2000, 2, 1, 12, 13, 14, 15, time.UTC)
withNanoseconds := timeWithNanoseconds.String()
timeWithoutNanoseconds := time.Date(2000, 2, 1, 12, 13, 14, 0, time.UTC)
withoutNanoseconds := timeWithoutNanoseconds.String()
fmt.Printf("withNanoseconds = %v\n", string(withNanoseconds))
fmt.Printf("withoutNanoseconds = %v\n", string(withoutNanoseconds))
}
Output:
withNanoseconds = 2000-02-01 12:13:14.000000015 +0000 UTC
withoutNanoseconds = 2000-02-01 12:13:14 +0000 UTC
func (Time) Sub
func (t Time) Sub(u Time) Duration
Sub возвращает продолжительность t-u. Если результат превышает максимальное (или минимальное) значение, которое может храниться в Duration, будет возвращена максимальная (или минимальная) продолжительность.
Пример
package main
import (
"fmt"
"time"
)
func main() {
start := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC)
difference := end.Sub(start)
fmt.Printf("difference = %v\n", difference)
}
Output:
difference = 12h0m0s
func (Time) Truncate
func (t Time) Truncate(d Duration) Time
Truncate возвращает результат округления t вниз до кратного d (с нулевого времени). Если d <= 0, Truncate возвращает t, лишенное любого монотонного показания часов, но в остальном неизменное.
Пример
import (
"fmt"
"time"
)
func main() {
t, _ := time.Parse("2006 Jan 02 15:04:05", "2012 Dec 07 12:15:30.918273645")
trunc := []time.Duration{
time.Nanosecond,
time.Microsecond,
time.Millisecond,
time.Second,
2 * time.Second,
time.Minute,
10 * time.Minute,
}
for _, d := range trunc {
fmt.Printf("t.Truncate(%5s) = %s\n", d, t.Truncate(d).Format("15:04:05.999999999"))
}
// To round to the last midnight in the local timezone, create a new Date.
midnight := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.Local)
_ = midnight
}
Output:
t.Truncate( 1ns) = 12:15:30.918273645
t.Truncate( 1µs) = 12:15:30.918273
t.Truncate( 1ms) = 12:15:30.918
t.Truncate( 1s) = 12:15:30
t.Truncate( 2s) = 12:15:30
t.Truncate( 1m0s) = 12:15:00
t.Truncate(10m0s) = 12:10:00
func (Time) UTC
UTC возвращает t с местоположением, установленным в UTC.
func (Time) Unix
func (t Time) Unix() int64
Unix возвращает t как Unix-время, количество секунд, прошедших с 1 января 1970 UTC. Результат не зависит от местоположения, связанного с t.
Пример
package main
import (
"fmt"
"time"
)
func main() {
// 1 billion seconds of Unix, three ways.
fmt.Println(time.Unix(1e9, 0).UTC()) // 1e9 seconds
fmt.Println(time.Unix(0, 1e18).UTC()) // 1e18 nanoseconds
fmt.Println(time.Unix(2e9, -1e18).UTC()) // 2e9 seconds - 1e18 nanoseconds
t := time.Date(2001, time.September, 9, 1, 46, 40, 0, time.UTC)
fmt.Println(t.Unix()) // seconds since 1970
fmt.Println(t.UnixNano()) // nanoseconds since 1970
}
Output:
2001-09-09 01:46:40 +0000 UTC
2001-09-09 01:46:40 +0000 UTC
2001-09-09 01:46:40 +0000 UTC
1000000000
1000000000000000000
func (Time) UnixMicro
func (t Time) UnixMicro() int64
UnixMicro возвращает t как Unix-время, количество микросекунд, прошедших с 1 января 1970 UTC.
func (Time) UnixMilli
func (t Time) UnixMilli() int64
UnixMilli возвращает t как Unix-время, количество миллисекунд, прошедших с 1 января 1970 UTC.
func (Time) UnixNano
func (t Time) UnixNano() int64
UnixNano возвращает t как Unix-время, количество наносекунд, прошедших с 1 января 1970 UTC.
func (*Time) UnmarshalBinary
func (t *Time) UnmarshalBinary(data []byte) error
UnmarshalBinary реализует интерфейс encoding.BinaryUnmarshaler.
func (*Time) UnmarshalJSON
func (t *Time) UnmarshalJSON(data []byte) error
UnmarshalJSON реализует интерфейс encoding/json.Unmarshaler. Время должно быть строкой в кавычках в формате RFC 3339.
func (*Time) UnmarshalText
func (t *Time) UnmarshalText(data []byte) error
UnmarshalText реализует интерфейс encoding.TextUnmarshaler. Время должно быть в формате RFC 3339.
func (Time) Weekday
func (t Time) Weekday() Weekday
Weekday возвращает день недели, указанный t.
func (Time) Year
Year возвращает год, в который происходит t.
func (Time) YearDay
func (t Time) YearDay() int
YearDay возвращает день года, указанный t, в диапазоне [1,365] для невисокосных лет и [1,366] в високосных годах.
func (Time) Zone
func (t Time) Zone() (name string, offset int)
Zone вычисляет часовой пояс, действующий в момент времени t, возвращая сокращенное название пояса (например, “CET”) и его смещение в секундах к востоку от UTC.
func (Time) ZoneBounds
func (t Time) ZoneBounds() (start, end Time)
ZoneBounds возвращает границы часового пояса, действующего в момент времени t. Пояс начинается в start, а следующий пояс начинается в end. Если пояс начинается в начале времен, start будет возвращен как нулевое Time. Если пояс продолжается вечно, end будет возвращен как нулевое Time. Местоположение возвращаемых времен будет таким же, как у t.
type Timer
type Timer struct {
C <-chan Time
// содержит неэкспортируемые поля
}
Timer представляет единичное событие. Когда Timer истекает, текущее время будет отправлено на C, если только Timer не был создан с помощью AfterFunc. Timer должен быть создан с помощью NewTimer или AfterFunc.
func AfterFunc
func AfterFunc(d Duration, f func()) *Timer
AfterFunc ждет истечения продолжительности, а затем вызывает f в своей собственной горутине. Возвращает Timer, который можно использовать для отмены вызова с помощью его метода Stop. Поле C возвращаемого Timer не используется и будет nil.
func NewTimer
func NewTimer(d Duration) *Timer
NewTimer создает новый Timer, который отправит текущее время на свой канал после как минимум продолжительности d.
func (*Timer) Reset
func (t *Timer) Reset(d Duration) bool
Reset изменяет таймер на истечение после продолжительности d. Возвращает true, если таймер был активен, false, если таймер уже истек или был остановлен.
func (t *Timer) Reset(d Duration) bool
Reset изменяет таймер для срабатывания через интервал d. Возвращает true, если таймер был активен, и false, если таймер уже сработал или был остановлен.
Для функционального таймера, созданного через AfterFunc(d, f):
Reset либо переносит время выполнения f (возвращает true)
- либо планирует новое выполнение
f (возвращает false)
При возврате false:
Reset не ожидает завершения предыдущего выполнения f
- Не гарантируется отсутствие конкурентного выполнения предыдущей и новой версий
f
- Для контроля завершения нужно реализовать собственную синхронизацию
Для канального таймера (создан через NewTimer), начиная с Go 1.23:
- Гарантируется, что после
Reset канал t.C не получит значений от предыдущих настроек
- Если программа еще не читала из
t.C и таймер активен, Reset гарантированно вернет true
До Go 1.23 безопасное использование требовало:
- Вызова
Timer.Stop()
- Явного чтения из канала (если
Stop вернула false)
func (*Timer) Stop
func (t *Timer) Stop() bool
Stop предотвращает срабатывание таймера. Возвращает:
true - если таймер был успешно остановлен
false - если таймер уже сработал или был остановлен
Для функционального таймера (AfterFunc):
- При
false функция f уже запущена в отдельной горутине
Stop не ожидает завершения f
- Для контроля завершения нужна дополнительная синхронизация
Для канального таймера (Go 1.23+):
- После
Stop чтение из t.C гарантированно блокируется
- Не будет получено “устаревших” значений времени
- Если таймер активен и канал не читался,
Stop гарантированно вернет true
До Go 1.23 требовалось:
- При
false выполнять дополнительное чтение <-t.C для очистки канала
type Weekday
Weekday определяет день недели (Sunday = 0, …).
Константы:
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
func (Weekday) String
func (d Weekday) String() string
String возвращает английское название дня (“Sunday”, “Monday”, …).
Пакет time/tzdata
Пакет tzdata предоставляет встроенную копию базы данных часовых поясов. Если этот пакет импортируется в любое место программы, то, если пакет времени не может найти файлы tzdata в системе, он будет использовать эту встроенную информацию.
Импорт этого пакета увеличит размер программы примерно на 450 КБ.
Обычно этот пакет должен импортироваться основным пакетом программы, а не библиотекой. Библиотеки обычно не должны решать, включать ли базу данных часовых поясов в программу.
Этот пакет будет автоматически импортирован, если вы построите с -tags timetzdata.
4.21 - SCS: Управление HTTP-сессиями для Go
Менеджер сессий для GO
Возможности
-
Автоматическая загрузка и сохранение данных сессии через middleware
-
Выбор из 19 серверных хранилищ для сессий, включая:
- Базы данных: PostgreSQL, MySQL, MSSQL, SQLite
- Кэш-системы: Redis
- Поддержка пользовательских хранилищ
-
Расширенные функции:
- Несколько сессий на один запрос
- “Flash”-сообщения (одноразовые уведомления)
- Регенерация токенов сессии
- Таймауты: по бездействию и абсолютные
- Функция “запомнить меня”
-
Гибкость:
- Простота расширения и кастомизации
- Передача токенов сессии через HTTP-заголовки или тела запросов/ответов
-
Производительность:
- Оптимизированная архитектура
- Меньший размер, выше скорость и меньше потребление памяти по сравнению с gorilla/sessions
Установка
Этот пакет требует Go версии 1.12 или новее.
go get github.com/alexedwards/scs/v2
Примечание: Если вы используете традиционный механизм GOPATH для управления зависимостями (вместо модулей), вам нужно использовать go get и импортировать github.com/alexedwards/scs без суффикса v2.
Рекомендуется использовать версионные релизы. Код в ветке tip может содержать экспериментальные функции, которые могут измениться.
Базовое использование
SCS реализует механизм управления сессиями в соответствии с рекомендациями по безопасности OWASP. Данные сессии хранятся на сервере, а случайно сгенерированный уникальный токен сессии (или ID сессии) передается клиенту и обратно через cookie сессии.
package main
import (
"io"
"net/http"
"time"
"github.com/alexedwards/scs/v2"
)
var sessionManager *scs.SessionManager
func main() {
// Инициализация нового менеджера сессий и настройка времени жизни сессии
sessionManager = scs.New()
sessionManager.Lifetime = 24 * time.Hour
mux := http.NewServeMux()
mux.HandleFunc("/put", putHandler)
mux.HandleFunc("/get", getHandler)
// Оберните ваши обработчики middleware LoadAndSave()
http.ListenAndServe(":4000", sessionManager.LoadAndSave(mux))
}
func putHandler(w http.ResponseWriter, r *http.Request) {
// Сохранение нового ключа и значения в данных сессии
sessionManager.Put(r.Context(), "message", "Hello from a session!")
}
func getHandler(w http.ResponseWriter, r *http.Request) {
// Использование вспомогательной функции GetString для получения строкового значения
// по ключу. Возвращается нулевое значение, если ключ не существует.
msg := sessionManager.GetString(r.Context(), "message")
io.WriteString(w, msg)
}
Пример работы с curl:
$ curl -i --cookie-jar cj --cookie cj localhost:4000/put
HTTP/1.1 200 OK
Cache-Control: no-cache="Set-Cookie"
Set-Cookie: session=lHqcPNiQp_5diPxumzOklsSdE-MJ7zyU6kjch1Ee0UM; Path=/; Expires=Sat, 27 Apr 2019 10:28:20 GMT; Max-Age=86400; HttpOnly; SameSite=Lax
Vary: Cookie
Date: Fri, 26 Apr 2019 10:28:19 GMT
Content-Length: 0
$ curl -i --cookie-jar cj --cookie cj localhost:4000/get
HTTP/1.1 200 OK
Date: Fri, 26 Apr 2019 10:28:24 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8
Hello from a session!
Настройка поведения сессии
Поведение сессии можно настроить через поля SessionManager:
sessionManager = scs.New()
sessionManager.Lifetime = 3 * time.Hour // Общее время жизни сессии
sessionManager.IdleTimeout = 20 * time.Minute // Таймаут бездействия
sessionManager.Cookie.Name = "session_id" // Имя cookie
sessionManager.Cookie.Domain = "example.com" // Домен cookie
sessionManager.Cookie.HttpOnly = true // Доступ только через HTTP
sessionManager.Cookie.Path = "/example/" // Путь cookie
sessionManager.Cookie.Persist = true // Сохранение после закрытия браузера
sessionManager.Cookie.SameSite = http.SameSiteStrictMode // Политика SameSite
sessionManager.Cookie.Secure = true // Только HTTPS
sessionManager.Cookie.Partitioned = true // Partitioned cookies
Документация по всем доступным настройкам и их значениям по умолчанию доступна [здесь](ссылка на документацию).
Работа с данными сессии
Установка и получение данных
Данные можно устанавливать с помощью метода Put() и получать с помощью Get(). Для распространённых типов данных предусмотрены вспомогательные методы, такие как GetString(), GetInt() и GetBytes(). Полный список вспомогательных методов доступен в документации.
Одноразовое получение данных
Метод Pop() (и соответствующие вспомогательные методы для распространённых типов данных) работает как одноразовый Get() - извлекает данные и сразу удаляет их из сессии. Это полезно для реализации функционала “flash”-сообщений, которые показываются пользователю только один раз.
Дополнительные функции
Exists() - возвращает bool, указывающий на наличие ключа в данных сессии
Keys() - возвращает отсортированный срез всех ключей в данных сессии
Удаление данных
Отдельные элементы можно удалить с помощью метода Remove(). Для полной очистки данных сессии используйте метод Destroy(). После вызова Destroy() любые последующие операции в рамках того же цикла запроса приведут к созданию новой сессии - с новым токеном и временем жизни.
Хранение пользовательских типов
SCS использует кодировку gob для хранения данных сессии. Пользовательские типы должны быть зарегистрированы в пакете encoding/gob. Поля структур пользовательских типов должны быть экспортируемыми (начинаться с заглавной буквы), чтобы быть видимыми для пакета encoding/gob. Пример работы можно посмотреть [здесь](ссылка на пример).
Загрузка и сохранение сессий
Стандартное использование
Большинство приложений используют middleware LoadAndSave(). Этот middleware:
- Загружает и сохраняет данные сессии в хранилище
- Управляет передачей токена сессии клиенту через cookie
Кастомизация поведения
Если требуется изменить стандартное поведение (например, передавать токен сессии через HTTP-заголовки или устанавливать распределённую блокировку токена сессии на время запроса), можно создать собственный middleware, используя код LoadAndSave() в качестве шаблона. Пример доступен [здесь](ссылка на пример).
Точный контроль
Для более точного управления можно загружать и сохранять сессии непосредственно в обработчиках запросов или в любом другом месте приложения. Пример такого подхода приведён [здесь](ссылка на пример).
Настройка хранилища сессий
Хранилище по умолчанию
По умолчанию SCS использует in-memory хранилище для данных сессии. Это:
- Удобно (не требует настройки)
- Очень быстро
- Но все данные теряются при остановке или перезапуске приложения
Это решение подходит для:
- Приложений, где допустима потеря данных в обмен на высокую производительность
- Прототипирования
- Тестирования
Продукционные решения
Для большинства рабочих приложений рекомендуется использовать постоянные хранилища, такие как:
- PostgreSQL
- MySQL
- Другие поддерживаемые системы (список ниже)
Полный список поддерживаемых хранилищ сессий с инструкциями по использованию доступен в таблице (ссылки в оригинальной документации).
Поддерживаемые хранилища сессий
Вот полный список доступных хранилищ сессий в SCS с их характеристиками:
| Пакет |
Бэкенд |
Встроенный |
In-Memory |
Мультипроцессорный |
badgerstore |
BadgerDB |
Да |
Нет |
Нет |
boltstore |
BBolt |
Да |
Нет |
Нет |
bunstore |
Bun ORM (PostgreSQL/MySQL/MSSQL/SQLite) |
Нет |
Нет |
Да |
buntdbstore |
BuntDB |
Да |
Да |
Нет |
cockroachdbstore |
CockroachDB |
Нет |
Нет |
Да |
consulstore |
Consul |
Нет |
Да |
Да |
etcdstore |
Etcd |
Нет |
Нет |
Да |
firestore |
Google Cloud Firestore |
Нет |
? |
Да |
gormstore |
GORM ORM (PostgreSQL/MySQL/SQLite/MSSQL/TiDB) |
Нет |
Нет |
Да |
leveldbstore |
LevelDB |
Да |
Нет |
Нет |
memstore |
In-memory (по умолчанию) |
Да |
Да |
Нет |
mongodbstore |
MongoDB |
Нет |
Нет |
Да |
mssqlstore |
Microsoft SQL Server |
Нет |
Нет |
Да |
mysqlstore |
MySQL |
Нет |
Нет |
Да |
pgxstore |
PostgreSQL (драйвер pgx) |
Нет |
Нет |
Да |
postgresstore |
PostgreSQL (драйвер pq) |
Нет |
Нет |
Да |
redisstore |
Redis |
Нет |
Да |
Да |
sqlite3store |
SQLite3 (CGO-драйвер mattn/go-sqlite3) |
Да |
Нет |
Да |
Ключевые характеристики:
- Встроенный - Не требует отдельного сервера/процесса
- In-Memory - Хранит данные в оперативной памяти
- Мультипроцессорный - Поддерживает работу в распределённой среде
Пользовательские хранилища
SCS также поддерживает создание собственных хранилищ сессий. Подробная информация доступна [здесь](ссылка на документацию).
Выбор хранилища зависит от требований вашего приложения:
- Для тестирования/разработки -
memstore или buntdbstore
- Для продакшена -
postgresstore, mysqlstore или redisstore
- Для встраиваемых решений -
badgerstore или boltstore
Использование пользовательских хранилищ сессий
Базовый интерфейс хранилища
Интерфейс scs.Store определяет контракт для пользовательских хранилищ сессий. Любой объект, реализующий этот интерфейс, может быть установлен как хранилище при настройке сессии.
type Store interface {
// Delete удаляет токен сессии и соответствующие данные из хранилища.
// Если токен не существует, Delete должен завершиться без ошибки.
Delete(token string) (err error)
// Find возвращает данные для указанного токена сессии.
// Если токен не найден или истек, возвращает found=false.
// Для повреждённых токенов также возвращает found=false.
// Ошибка возвращается только для системных сбоев.
Find(token string) (b []byte, found bool, err error)
// Commit добавляет или обновляет данные сессии с указанным сроком действия.
Commit(token string, b []byte, expiry time.Time) (err error)
}
type IterableStore interface {
// All возвращает map со всеми активными (не истекшими) сессиями.
// Ключ - токен сессии, значение - данные сессии.
// Если активных сессий нет, возвращает пустую (не nil) map.
All() (map[string][]byte, error)
}
Интерфейс с поддержкой контекста
scs.CtxStore расширяет базовый интерфейс, добавляя методы с поддержкой context.Context:
type CtxStore interface {
Store
// Версии методов с контекстом
DeleteCtx(ctx context.Context, token string) (err error)
FindCtx(ctx context.Context, token string) (b []byte, found bool, err error)
CommitCtx(ctx context.Context, token string, b []byte, expiry time.Time) (err error)
}
type IterableCtxStore interface {
// AllCtx - версия All с поддержкой контекста
AllCtx(ctx context.Context) (map[string][]byte, error)
}
Защита от фиксации сессии
Для предотвращения атак фиксации сессии следует обновлять токен сессии при любом изменении уровня привилегий. Обычно это делается при входе/выходе пользователя:
func loginHandler(w http.ResponseWriter, r *http.Request) {
userID := 123
// 1. Сначала обновляем токен сессии
err := sessionManager.RenewToken(r.Context())
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// 2. Затем изменяем уровень привилегий
sessionManager.Put(r.Context(), "userID", userID)
}
Важные моменты:
RenewToken() должен вызываться ДО изменения данных сессии
- При ошибке обновления токена следует прервать операцию
- Такой же подход применяется при выходе пользователя
Этот механизм гарантирует, что после аутентификации сессия получает новый токен, делая недействительным любой ранее перехваченный злоумышленником токен.
Расширенные возможности SCS
Несколько сессий в одном запросе
SCS позволяет использовать несколько параллельных сессий в рамках одного запроса с разными:
- Временем жизни
- Хранилищами данных
Пример реализации доступен [здесь](ссылка на пример).
Перебор всех сессий
Для работы со всеми активными сессиями SCS предоставляет метод Iterate(), который принимает функцию-обработчик:
err := sessionManager.Iterate(r.Context(), func(ctx context.Context) error {
userID := sessionManager.GetInt(ctx, "userID")
if userID == 4 {
// Удаляем сессию для пользователя с ID=4
return sessionManager.Destroy(ctx)
}
return nil
})
if err != nil {
log.Fatal(err)
}
Ключевые особенности:
- Обработчик получает контекст каждой сессии
- Можно выполнять любые операции с сессией
- Возврат ошибки прерывает процесс итерации
Потоковая передача ответов (Flushing)
Поддержка потоковой передачи реализована через http.NewResponseController (доступно в Go ≥1.20):
func flushingHandler(w http.ResponseWriter, r *http.Request) {
sessionManager.Put(r.Context(), "message", "Hello from flushing handler!")
rc := http.NewResponseController(w)
for i := 0; i < 5; i++ {
fmt.Fprintf(w, "Write %d\n", i)
err := rc.Flush()
if err != nil {
log.Println(err)
return
}
time.Sleep(time.Second)
}
}
Важно:
- Стандартная реализация middleware
LoadAndSave() не поддерживает интерфейс http.Flusher
- Потоковая передача работает только в Go версии 1.20 и выше
Полный рабочий пример можно найти [здесь](ссылка на пример).
Совместимость с фреймворками
Возможны проблемы при интеграции с фреймворками, которые не передают контекст запроса через стандартные middleware:
- Для Echo рекомендуется использовать пакет echo-scs-session
- Fiber и другие нестандартные фреймворки могут требовать дополнительной адаптации
Вклад в разработку
Приветствуются:
- Исправления ошибок
- Улучшения документации
Для новых функций или изменений поведения:
- Создайте issue для обсуждения
- После согласования - отправляйте PR
Для новых реализаций хранилищ:
- Как правило, не добавляются в основной репозиторий
- Можно создать отдельный репозиторий и добавить ссылку в README через PR
4.21.1 - Официальное API SCS: Управление HTTP-сессиями для Go
Менеджер сессий для GO
Использование пользовательских хранилищ сеансов scs.Store определяет интерфейс для пользовательских хранилищ сеансов. Любой объект, реализующий этот интерфейс, может быть установлен как хранилище при настройке сеанса.
Работа с пользовательскими хранилищами сессий
Интерфейсы хранилищ
Базовый интерфейс Store
Определяет контракт для реализации пользовательских хранилищ:
type Store interface {
// Delete удаляет токен и данные сессии из хранилища
// Если токен не существует - операция должна завершиться успешно
Delete(token string) error
// Find ищет данные по токену сессии
// Возвращает:
// - данные (если найдены)
// - флаг found (найдено/не найдено)
// - ошибку (только для системных сбоев)
Find(token string) ([]byte, bool, error)
// Commit сохраняет или обновляет данные сессии
Commit(token string, data []byte, expiry time.Time) error
}
Интерфейс IterableStore
Добавляет возможность перебора всех сессий:
type IterableStore interface {
// All возвращает все активные (не истекшие) сессии
// В формате map[токен]данные
All() (map[string][]byte, error)
}
Версии с поддержкой контекста
CtxStore
Расширяет базовый интерфейс с добавлением context.Context:
type CtxStore interface {
Store
// Версии методов с поддержкой контекста
DeleteCtx(ctx context.Context, token string) error
FindCtx(ctx context.Context, token string) ([]byte, bool, error)
CommitCtx(ctx context.Context, token string, data []byte, expiry time.Time) error
}
IterableCtxStore
Аналогично для перебора сессий:
type IterableCtxStore interface {
AllCtx(ctx context.Context) (map[string][]byte, error)
}
Защита от фиксации сессии
Для предотвращения атак фиксации сессии необходимо:
func loginHandler(w http.ResponseWriter, r *http.Request) {
userID := 123
// 1. Сначала обновляем токен сессии
if err := sessionManager.RenewToken(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 2. Затем сохраняем данные пользователя
sessionManager.Put(r.Context(), "userID", userID)
}
Ключевые моменты:
- Всегда обновляйте токен при изменении прав доступа
- Порядок операций критически важен
- Такой же подход применяйте при выходе пользователя
Это гарантирует, что после аутентификации будет создан новый токен сессии, делая недействительными любые ранее перехваченные токены.
Типы данных в SCS
Интерфейс Codec
Добавлен в v2.2.0
type Codec interface {
// Encode преобразует срок действия и значения сессии в байтовый срез
Encode(deadline time.Time, values map[string]interface{}) ([]byte, error)
// Decode восстанавливает срок действия и значения сессии из байтового среза
Decode([]byte) (deadline time.Time, values map[string]interface{}, err error)
}
Интерфейс для кодирования/декодирования данных сессии при работе с хранилищем.
Интерфейс CtxStore
Добавлен в v2.5.0
type CtxStore interface {
Store
// Версии методов с поддержкой контекста:
DeleteCtx(ctx context.Context, token string) error
FindCtx(ctx context.Context, token string) ([]byte, bool, error)
CommitCtx(ctx context.Context, token string, b []byte, expiry time.Time) error
}
Расширенный интерфейс хранилища с поддержкой context.Context.
Структура GobCodec
Добавлена в v2.3.1
Реализация Codec с использованием encoding/gob для сериализации данных.
Методы GobCodec:
// Decode преобразует байты в срок действия и данные сессии
func (GobCodec) Decode(b []byte) (time.Time, map[string]interface{}, error)
// Encode сериализует данные сессии в байтовый срез
func (GobCodec) Encode(deadline time.Time, values map[string]interface{}) ([]byte, error)
Интерфейс IterableCtxStore
Добавлен в v2.5.0
type IterableCtxStore interface {
// AllCtx возвращает все активные сессии (с поддержкой контекста)
AllCtx(ctx context.Context) (map[string][]byte, error)
}
Интерфейс для перечислимых хранилищ с поддержкой context.Context.
Интерфейс IterableStore
Добавлен в v2.5.0
type IterableStore interface {
// All возвращает все активные (не истекшие) сессии
All() (map[string][]byte, error)
}
Базовый интерфейс для хранилищ с возможностью перебора сессий.
Ключевые особенности:
Codec - абстракция для сериализации данных
GobCodec - стандартная реализация через encoding/gob
CtxStore добавляет поддержку контекста к базовому хранилищу
- Интерфейсы
Iterable позволяют работать со всеми сессиями
Конфигурация сессий в SCS
Структура SessionCookie
type SessionCookie struct {
// Имя cookie сессии (не должно содержать спецсимволы)
// По умолчанию: "session"
Name string
// Домен cookie (по умолчанию - текущий домен)
Domain string
// HttpOnly флаг (по умолчанию true)
HttpOnly bool
// Путь cookie (по умолчанию "/")
Path string
// Сохранять cookie после закрытия браузера (по умолчанию true)
Persist bool
// Политика SameSite (по умолчанию Lax)
SameSite http.SameSite
// Secure флаг (только HTTPS, по умолчанию false)
Secure bool
}
Настройки cookie для управления сессиями.
Структура SessionManager
Добавлена в v2.1.0
type SessionManager struct {
// Таймаут бездействия (опционально)
IdleTimeout time.Duration
// Максимальное время жизни сессии (по умолчанию 24 часа)
Lifetime time.Duration
// Хранилище сессий
Store Store
// Настройки cookie
Cookie SessionCookie
// Кодек для сериализации данных (по умолчанию GobCodec)
Codec Codec
// Обработчик ошибок (по умолчанию HTTP 500)
ErrorFunc func(http.ResponseWriter, *http.Request, error)
// Хэшировать токен в хранилище
HashTokenInStore bool
// содержит скрытые поля
}
Основные настройки:
- IdleTimeout: автоматическое завершение неактивных сессий
- Lifetime: абсолютное время жизни сессии
- Store: подключение к Redis, PostgreSQL и другим хранилищам
- Cookie: тонкая настройка параметров безопасности cookie
- Codec: кастомная сериализация данных
- ErrorFunc: обработка ошибок на уровне middleware
Рекомендации по безопасности:
- Всегда устанавливайте
Secure=true в production
- Используйте
SameSite=Strict для критичных операций
- Для API можно отключить
HttpOnly и работать через заголовки
- Регулируйте
Lifetime в соответствии с требованиями приложения
Пример инициализации:
manager := scs.New()
manager.Lifetime = 12 * time.Hour
manager.Cookie.Secure = true
manager.Cookie.SameSite = http.SameSiteStrictMode
manager.Store = redisstore.New(redisPool)
Методы SessionManager
Основные функции
New
Добавлено в v2.1.0
func New() *SessionManager
Создает новый менеджер сессий с настройками по умолчанию. Потокобезопасен.
Clear (устаревший)
func (s *SessionManager) Clear(ctx context.Context) error
Очищает все данные текущей сессии, не затрагивая токен и время жизни. Если данных нет - операция не выполняется.
Управление сессиями
Commit
Добавлено в v2.1.0
func (s *SessionManager) Commit(ctx context.Context) (string, time.Time, error)
Сохраняет данные сессии в хранилище и возвращает:
- Токен сессии
- Время истечения
- Ошибку (если есть)
Обычно используется автоматически middleware LoadAndSave().
Deadline
Добавлено в v2.5.0
func (s *SessionManager) Deadline(ctx context.Context) time.Time
Возвращает абсолютное время истечения сессии. При использовании IdleTimeout сессия может завершиться раньше из-за неактивности.
Destroy
Добавлено в v2.1.0
func (s *SessionManager) Destroy(ctx context.Context) error
Полностью удаляет сессию:
- Удаляет данные из хранилища
- Помечает сессию как уничтоженную
- Новые операции создадут свежую сессию
Работа с данными
Exists
Добавлено в v2.1.0
func (s *SessionManager) Exists(ctx context.Context, key string) bool
Проверяет наличие ключа в данных сессии.
Get
Добавлено в v2.1.0
func (s *SessionManager) Get(ctx context.Context, key string) interface{}
Возвращает значение по ключу как interface{}, требующее приведения типа:
foo, ok := session.Get(ctx, "foo").(string)
if !ok {
return errors.New("ошибка приведения типа")
}
GetBool
Добавлено в v2.1.0
func (s *SessionManager) GetBool(ctx context.Context, key string) bool
Возвращает значение как bool (по умолчанию false если ключ отсутствует или тип не совпадает).
GetBytes
Добавлено в v2.1.0
func (s *SessionManager) GetBytes(ctx context.Context, key string) []byte
Возвращает значение как []byte (по умолчанию nil).
Методы работы с данными сессии
Получение типизированных значений
GetFloat
func (s *SessionManager) GetFloat(ctx context.Context, key string) float64
Возвращает значение как float64 (0 если ключ отсутствует или тип не совпадает)
GetInt
func (s *SessionManager) GetInt(ctx context.Context, key string) int
Возвращает значение как int (0 если ключ отсутствует)
GetInt32
func (s *SessionManager) GetInt32(ctx context.Context, key string) int32
Возвращает значение как int32 (0 по умолчанию)
GetInt64
func (s *SessionManager) GetInt64(ctx context.Context, key string) int64
Возвращает значение как int64 (0 по умолчанию)
GetString
func (s *SessionManager) GetString(ctx context.Context, key string) string
Возвращает строковое значение (пустая строка "" если ключ отсутствует)
GetTime
func (s *SessionManager) GetTime(ctx context.Context, key string) time.Time
Возвращает значение времени (time.IsZero() == true если ключ отсутствует)
Управление сессиями
Iterate
func (s *SessionManager) Iterate(ctx context.Context, fn func(context.Context) error) error
Итерируется по всем активным сессиям и выполняет функцию fn для каждой. Требует поддержки итерации в хранилище.
Keys
func (s *SessionManager) Keys(ctx context.Context) []string
Возвращает отсортированный список всех ключей сессии (пустой слайс если данных нет)
Middleware и загрузка данных
Load
func (s *SessionManager) Load(ctx context.Context, token string) (context.Context, error)
Загружает данные сессии по токену и возвращает новый контекст. Создает новую сессию если токен не найден.
LoadAndSave
func (s *SessionManager) LoadAndSave(next http.Handler) http.Handler
Middleware для автоматической загрузки/сохранения сессии и управления cookie.
Пример:
router := http.NewServeMux()
router.HandleFunc("/", handler)
// Обертывание роутера middleware
app := sessionManager.LoadAndSave(router)
http.ListenAndServe(":4000", app)
Методы управления сессиями в SCS
Работа с данными сессии
MergeSession (v2.5.0+)
func (s *SessionManager) MergeSession(ctx context.Context, token string) error
Объединяет данные из другой сессии (полезно при OAuth-редиректах). Для полной замены данных используйте Clear().
Pop-методы
Одноразовое получение данных с удалением ключа:
func (s *SessionManager) Pop(ctx context.Context, key string) interface{}
func (s *SessionManager) PopBool(ctx context.Context, key string) bool
func (s *SessionManager) PopBytes(ctx context.Context, key string) []byte
func (s *SessionManager) PopFloat(ctx context.Context, key string) float64
func (s *SessionManager) PopInt(ctx context.Context, key string) int
func (s *SessionManager) PopString(ctx context.Context, key string) string
func (s *SessionManager) PopTime(ctx context.Context, key string) time.Time
Пример для flash-сообщений:
// Установка
sessionManager.Put(ctx, "flash", "Сообщение")
// Получение с удалением
msg := sessionManager.PopString(ctx, "flash")
Основные операции
Put (v2.1.0+)
func (s *SessionManager) Put(ctx context.Context, key string, val interface{})
Добавляет или обновляет значение в сессии. Автоматически помечает сессию как измененную.
Remove (v2.1.0+)
func (s *SessionManager) Remove(ctx context.Context, key string)
Удаляет ключ из сессии. Если ключ не существует - операция игнорируется.
Управление токенами и временем жизни
RenewToken (v2.1.0+)
func (s *SessionManager) RenewToken(ctx context.Context) error
Генерирует новый токен сессии, сохраняя данные. Критично для:
- Входа/выхода пользователей
- Изменения прав доступа
- Защиты от фиксации сессии
RememberMe (v2.5.0+)
func (s *SessionManager) RememberMe(ctx context.Context, val bool)
Управляет постоянством сессии (работает только при Cookie.Persist = false).
SetDeadline (v2.7.0+)
func (s *SessionManager) SetDeadline(ctx context.Context, expire time.Time)
Устанавливает абсолютное время истечения сессии (может быть перекрыто IdleTimeout).
Информационные методы
Status (v2.1.0+)
func (s *SessionManager) Status(ctx context.Context) Status
Возвращает текущее состояние сессии (новые/измененные/уничтоженные).
Token (v2.5.0+)
func (s *SessionManager) Token(ctx context.Context) string
Возвращает токен сессии (пустую строку до коммита).
WriteSessionCookie (v2.3.0+)
func (s *SessionManager) WriteSessionCookie(ctx context.Context, w http.ResponseWriter, token string, expiry time.Time)
Низкоуровневый метод записи cookie (обычно используется через middleware).
Типы данных и интерфейсы SCS
Тип Status
Статус представляет состояние данных сессии в течение цикла запроса.
Константы статусов:
const (
// Unmodified - данные сессии не изменялись
Unmodified Status = iota
// Modified - данные были изменены
Modified
// Destroyed - сессия была уничтожена
Destroyed
)
Использование:
status := sessionManager.Status(ctx)
switch status {
case scs.Unmodified:
// Сессия не изменялась
case scs.Modified:
// Требуется сохранение изменений
case scs.Destroyed:
// Сессия закрыта
}
Интерфейс Store
type Store interface {
// Delete удаляет токен и данные сессии
// Если токен не существует - операция игнорируется
Delete(token string) error
// Find ищет данные по токену сессии
// Возвращает:
// - данные (если найдены)
// - флаг существования
// - ошибку (только для системных сбоев)
Find(token string) ([]byte, bool, error)
// Commit сохраняет/обновляет данные сессии
Commit(token string, data []byte, expiry time.Time) error
}
Базовый интерфейс для реализации хранилищ сессий.
Особенности реализации:
- Delete должен быть идемпотентным
- Find возвращает
found=false для:
- Несуществующих токенов
- Истекших сессий
- Поврежденных данных
- Commit должен перезаписывать существующие данные
Пример проверки сессии:
data, found, err := store.Find(token)
if err != nil {
// Обработка системной ошибки
}
if !found {
// Сессия не существует/истекла
}
Этот интерфейс позволяет интегрировать SCS с любыми системами хранения данных.
5 - Горутина (goroutine) в Go, Каналы (channels) в Go и Конвейеры (Pipelines) в Go.
Горутина (goroutine) в Go — это легковесный “поток”, который позволяет выполнять код конкурентно (почти одновременно) без создания полноценных потоков ОС. Каналы (channels) в Go — это специальный тип данных, который позволяет безопасно передавать данные между горутинами
goroutine
Горутина (goroutine) в Go — это легковесный “поток”, который позволяет выполнять код конкурентно (почти одновременно) без создания полноценных потоков ОС.
Простыми словами о Goroutine:
- Это как виртуальный работник, который выполняет задачи параллельно с другими горутинами.
- Горутины дешевле настоящих потоков (их можно создавать тысячи без нагрузки на систему).
- Управляются средой Go (runtime), а не операционной системой.
Пример:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Привет!")
}
func main() {
// Запуск горутины (выполняется параллельно с main)
go sayHello()
// Главная функция продолжает работу
fmt.Println("Главная функция...")
time.Sleep(1 * time.Second) // Ждём, чтобы горутина успела выполниться
}
Вывод:
Главная функция...
Привет!
Особенности:
- Легковесные — занимают всего ~2 КБ памяти (потоки ОС — от 1 МБ).
- Быстрый старт — создаются и запускаются почти мгновенно.
- Автомасштабирование — Go сам распределяет их по доступным ядрам CPU.
- Не блокируют main — если главная функция завершится, все горутины умрут.
Важно:
- Горутины не гарантируют порядок выполнения.
- Для обмена данными между горутинами используют каналы (channels).
Аналог из жизни:
Представьте, что вы (главная функция) даёте задание коллеге (горутине):
- Вы можете продолжать работать, пока коллега выполняет свою часть.
- Вам не нужно ждать его — всё происходит конкурентно.
Горутины — одна из главных “фишек” Go, делающая его мощным для многозадачных программ!
channels
Каналы (channels) в Go — это специальный тип данных, который позволяет безопасно передавать данные между горутинами.
Представьте их как трубы, по которым одна горутина отправляет данные, а другая — принимает. Это помогает избежать гонок данных (data races) и синхронизировать работу горутин.
Базовые операции с каналами
1. Создание канала
Каналы бывают:
- Буферизированные (с фиксированной ёмкостью)
- Небуферизированные (ждёт, пока получатель заберёт данные)
ch := make(chan int) // Небуферизированный канал для int
chBuf := make(chan int, 3) // Буферизированный (ёмкость = 3)
2. Отправка данных (<-)
ch <- 42 // Отправить число 42 в канал
3. Получение данных (<-)
value := <-ch // Получить данные из канала
fmt.Println(value) // 42
4. Закрытие канала (close)
Закрытый канал нельзя использовать для отправки, но можно читать оставшиеся данные.
Пример: Простая передача данных
package main
import "fmt"
func sendData(ch chan string) {
ch <- "Привет из горутины!" // Отправляем данные
}
func main() {
ch := make(chan string) // Создаём канал
go sendData(ch) // Запускаем горутину
msg := <-ch // Получаем данные
fmt.Println(msg) // "Привет из горутины!"
close(ch) // Закрываем канал
}
Вывод:
Важные особенности каналов
1. Блокировка операций
2. Проверка на закрытие
value, ok := <-ch
if !ok {
fmt.Println("Канал закрыт!")
}
3. range по каналу
Можно перебирать данные, пока канал не закроется:
for msg := range ch {
fmt.Println(msg)
}
Пример: Синхронизация горутин
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Println("Работаю...")
time.Sleep(2 * time.Second)
fmt.Println("Готово!")
done <- true // Сигнализируем о завершении
}
func main() {
done := make(chan bool)
go worker(done)
<-done // Ждём сигнал от worker
fmt.Println("Рабочая горутина завершилась.")
}
Вывод:
Работаю...
Готово!
Рабочая горутина завершилась.
Когда использовать каналы?
- Для синхронизации (ожидание завершения горутин).
- Для передачи данных между горутинами.
- Для организации конвейеров (pipeline) обработки данных.
Ошибки при работе с каналами
- Deadlock — если нет получателя/отправителя.
ch := make(chan int)
ch <- 42 // Ошибка: никто не читает из канала!
- Паника при отправке в закрытый канал.
close(ch)
ch <- 10 // panic: send on closed channel
- Чтение из пустого закрытого канала вернёт нулевое значение.
Аналог из жизни
Канал — как почтовый ящик:
- Вы (горутина) кладёте письмо (
ch <- data).
- Другой человек (горутина) забирает его (
<-ch).
- Если ящик переполнен (буфер заполнен), вы ждёте, пока освободится место.
Каналы — это безопасный и удобный способ общения между горутинами в Go!
Pipelines
Конвейеры (Pipelines) в Go: примеры и особенности
Конвейер (pipeline) — это цепочка горутин, где каждая выполняет свою часть работы, передавая данные следующей через каналы. Это мощный паттерн для:
- Обработки данных по этапам (фильтрация, преобразование, агрегация).
- Параллельного выполнения задач с контролируемым потоком данных.
- Управления ресурсами (ограничение числа одновременно работающих горутин).
Базовый конвейер (3 этапа)
1. Генерация данных → Обработка → Вывод
package main
import "fmt"
// Этап 1: Генерация чисел (producer)
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Этап 2: Умножение (processor)
func multiply(in <-chan int, factor int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * factor
}
close(out)
}()
return out
}
// Этап 3: Вывод (consumer)
func print(in <-chan int) {
for n := range in {
fmt.Println(n)
}
}
func main() {
// Конвейер: generate → multiply → print
data := generate(1, 2, 3, 4)
processed := multiply(data, 10)
print(processed)
}
Вывод:
Особенности конвейеров
1. Каждый этап — отдельная горутина
- Этапы работают параллельно (если CPU позволяет).
- Данные передаются последовательно через каналы.
2. Закрытие каналов
- Когда producer закрывает канал, все processors и consumer получают сигнал завершения.
- Использование
range для чтения из канала автоматически завершается при close().
3. Буферизация для ускорения
Буферизированные каналы уменьшают блокировки:
out := make(chan int, 100) // Буфер на 100 элементов
Фильтрация в конвейере
Добавим этап, который оставляет только чётные числа:
// Этап 2.5: Фильтрация чётных чисел
func filterEven(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if n%2 == 0 {
out <- n
}
}
close(out)
}()
return out
}
func main() {
data := generate(1, 2, 3, 4, 5, 6)
processed := multiply(data, 3) // Умножаем на 3
evenOnly := filterEven(processed) // Оставляем чётные
print(evenOnly)
}
Вывод:
Fan-Out/Fan-In: Параллельная обработка
- Fan-Out: Разделение работы между несколькими горутинами.
- Fan-In: Объединение результатов в один канал.
Пример:
// Fan-In: объединяет несколько каналов в один
func merge(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
// Запускаем для каждого канала горутину, которая перенаправляет данные в out
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}(ch)
}
// Закрываем out после завершения всех горутин
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
data := generate(1, 2, 3, 4, 5, 6, 7, 8)
// Fan-Out: Запускаем 3 обработчика
proc1 := multiply(data, 10)
proc2 := multiply(data, 20)
proc3 := multiply(data, 30)
// Fan-In: Объединяем результаты
merged := merge(proc1, proc2, proc3)
print(merged)
}
Вывод (порядок может меняться):
10
20
30
40
50
60
70
80
20
40
60
80
100
120
140
160
30
60
90
120
150
180
210
240
Ошибки и подводные камни
-
Утечки горутин
- Если канал не закрыт, горутина может зависнуть в ожидании.
- Всегда закрывайте каналы на стороне отправителя.
-
Deadlock
- Если канал не читается, отправляющая горутина заблокируется навсегда.
-
Ограничение параллелизма
- Если запустить 1000 горутин, они могут перегрузить систему.
- Решение: пул воркеров с использованием
semaphore или buffered channels.
Когда использовать конвейеры?
- ETL-процессы (извлечение, преобразование, загрузка данных).
- Параллельная обработка большого количества задач (например, скачивание файлов).
- Построение многопоточных приложений с чёткой структурой.
Аналог из жизни
Конвейер — как заводская линия сборки:
- Этап 1: Детали поступают на ленту (генерация данных).
- Этап 2: Робот красит детали (обработка).
- Этап 3: Другой робот упаковывает (фильтрация).
- Этап 4: Готовые изделия отправляются на склад (вывод).
Каждый этап работает независимо, но согласованно через общий механизм (каналы).
Итог
Конвейеры в Go — это:
✅ Гибкость (можно добавлять/удалять этапы).
✅ Параллелизм (эффективное использование CPU).
✅ Безопасность (данные передаются через каналы, нет гонок).
Используйте их для сложных многопоточных задач!
6 - Подключение к SQLite в Go: Полное руководство
Основные команды по управлению базой данных SQLite на языке программирования Go.
Установка драйвера SQLite для Go
Перед началом работы установите драйвер SQLite:
go get github.com/mattn/go-sqlite3
Базовое подключение к базе данных
Простое подключение
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// Открытие соединения с базой данных
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверка соединения
err = db.Ping()
if err != nil {
log.Fatal(err)
}
fmt.Println("Successfully connected to SQLite database")
}
Подключение с параметрами
db, err := sql.Open("sqlite3", "file:test.db?cache=shared&mode=memory")
if err != nil {
log.Fatal(err)
}
Выполнение запросов
Создание таблицы
func createTable(db *sql.DB) error {
query := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
_, err := db.Exec(query)
return err
}
Вставка данных
func insertUser(db *sql.DB, name, email string) (int64, error) {
res, err := db.Exec("INSERT INTO users(name, email) VALUES(?, ?)", name, email)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
Выборка данных
type User struct {
ID int
Name string
Email string
CreatedAt string
}
func getUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name, email, created_at FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt)
if err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
Транзакции
func transferMoney(db *sql.DB, from, to int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
// Откат транзакции при ошибке
defer func() {
if err != nil {
tx.Rollback()
}
}()
// Снимаем деньги с первого аккаунта
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
// Добавляем деньги на второй аккаунт
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
if err != nil {
return err
}
// Фиксируем транзакцию
return tx.Commit()
}
Подготовленные выражения (Prepared Statements)
func getUserByEmail(db *sql.DB, email string) (*User, error) {
stmt, err := db.Prepare("SELECT id, name, email FROM users WHERE email = ?")
if err != nil {
return nil, err
}
defer stmt.Close()
var u User
err = stmt.QueryRow(email).Scan(&u.ID, &u.Name, &u.Email)
if err != nil {
return nil, err
}
return &u, nil
}
Практические советы
- Пулинг соединений: SQLite не поддерживает множественные соединения на запись, но вы можете использовать пулинг для чтения:
db.SetMaxOpenConns(1) // Важно для SQLite
- Режим WAL: Для лучшей производительности:
_, err = db.Exec("PRAGMA journal_mode=WAL")
if err != nil {
log.Fatal(err)
}
- Обработка ошибок: Всегда проверяйте ошибки при закрытии ресурсов:
defer func() {
if err := rows.Close(); err != nil {
log.Printf("failed to close rows: %v", err)
}
}()
Полный пример приложения
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int
Name string
Email string
}
func main() {
// Подключение к базе данных
db, err := sql.Open("sqlite3", "./app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Настройка базы данных
if err := initDB(db); err != nil {
log.Fatal(err)
}
// Добавление пользователя
id, err := addUser(db, "John Doe", "john@example.com")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Added user with ID: %d\n", id)
// Получение списка пользователей
users, err := getUsers(db)
if err != nil {
log.Fatal(err)
}
fmt.Println("Users:")
for _, u := range users {
fmt.Printf("%d: %s (%s)\n", u.ID, u.Name, u.Email)
}
}
func initDB(db *sql.DB) error {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
`)
return err
}
func addUser(db *sql.DB, name, email string) (int64, error) {
res, err := db.Exec("INSERT INTO users(name, email) VALUES(?, ?)", name, email)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
func getUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name, email FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
Заключение
Работа с SQLite в Go проста и эффективна благодаря пакету database/sql и драйверу go-sqlite3. Основные принципы:
- Всегда закрывайте ресурсы (деферы для
Close())
- Используйте подготовленные выражения для защиты от SQL-инъекций
- Для операций записи используйте транзакции
- Настраивайте параметры SQLite для оптимальной производительности
7 - Спецификация языка программирования Go
Спецификация языка программирования Go на русском языке
“The Go Programming Language Specification”
Introduction (Введение)
Это справочное руководство по языку программирования Go. Версию до Go1.18, без дженериков, можно найти здесь. Дополнительную информацию и другие документы можно найти на сайте go.dev
Go - это язык общего назначения, разработанный для системного программирования. Он сильно типизирован, имеет сборщик мусора, а также имеет явную поддержку параллельного программирования. Программы строятся из пакетов, свойства которых позволяют эффективно управлять зависимостями.
Синтаксис компактен и прост в разборе, что позволяет легко анализировать его с помощью автоматических инструментов, таких как интегрированные среды разработки.
Notation (Нотация)
Синтаксис задается с помощью варианта расширенной формы Бэкуса-Наура (EBNF):
Production = production_name "=" [ Expression ] "." .
Expression = Alternative { "|" Alternative } .
Alternative = Term { Term } .
Term = production_name | token [ "…" token ] | Group | Option | Repetition .
Group = "(" Expression ")" .
Option = "[" Expression "]" .
Repetition = "{" Expression "}" .
“Productions” (продукции) — это правила в формальной грамматике, которые описывают синтаксис языка Go.
Productions - это выражения, построенные из терминов и следующих операторов в порядке возрастания старшинства:
| alternation
() grouping
[] option (0 or 1 times)
{} repetition (0 to n times)
Для обозначения лексических (терминальных) лексем используются строчные имена, написанные строчными буквами. Нетерминалы обозначаются в CamelCase. Лексические лексемы заключаются в двойные кавычки "" или обратные кавычки ``.
Форма a ... b представляет собой набор символов от a до b в качестве альтернатив. Горизонтальное многоточие ... также используется в других местах спецификации для неформального обозначения различных перечислений или фрагментов кода, которые не уточняются далее. Символ ... (в отличие от трех символов …) не является лексемой языка Go.
Ссылка вида [Go 1.xx] указывает на то, что описанная функция языка (или какой-то ее аспект) была изменена или добавлена в версии языка 1.xx и поэтому для сборки требуется как минимум эта версия языка. Подробнее см. раздел со ссылками в приложении.
Source code representation (Представление исходного кода)
Исходный код - это текст Юникода, закодированный в UTF-8. Текст не канонизирован, поэтому одна точка кода с ударением отличается от того же символа, построенного из сочетания ударения и буквы; они рассматриваются как две точки кода. Для простоты в этом документе будет использоваться неквалифицированный термин “символ” для обозначения кодовой точки Юникода в исходном тексте.
Каждая кодовая точка является отдельной; например, прописные и строчные буквы - это разные символы.
Ограничение на реализацию: Для совместимости с другими инструментами компилятор может запретить использование символа NUL (U+0000) в исходном тексте.
Ограничение на реализацию: Для совместимости с другими инструментами компилятор может игнорировать метку порядка байтов в кодировке UTF-8 (U+FEFF), если она является первой точкой кода Юникода в исходном тексте. В любом другом месте исходного текста знак порядка байтов может быть запрещен.
Символы
Следующие термины используются для обозначения определенных категорий символов Unicode:
newline = /* кодовая позиция Юникода U+000A */ .
unicode_char = /* произвольная кодовая позиция Юникода, кроме newline */ .
unicode_letter = /* кодовая позиция Юникода, отнесенная к категории "Буква" */ .
unicode_digit = /* кодовая позиция Юникода, отнесенная к категории "Число, десятичная цифра" */ .```
В стандарте Юникод 8.0 в разделе 4.5 “Общая категория” определен набор категорий символов. Go рассматривает все символы в любой из буквенных категорий Lu, Ll, Lt, Lm или Lo как буквы Юникода, а символы в числовой категории Nd - как цифры Юникода.
Символы и числа
Символ подчеркивания _ (U+005F) считается строчной буквой.
letter = unicode_letter | "_" .
decimal_digit = "0" … "9" .
binary_digit = "0" | "1" .
octal_digit = "0" … "7" .
hex_digit = "0" … "9" | "A" … "F" | "a" … "f" .
Лексические элементы
Комментарии
Комментарии служат в качестве программной документации. Существует две формы:
- Строчные комментарии начинаются с последовательности символов
// и останавливаются в конце строки.
- Общие комментарии начинаются с последовательности символов
/* и заканчиваются первой последующей последовательностью символов */.
Комментарий не может начинаться внутри руны или строкового литерала, а также внутри комментария. Общий комментарий, не содержащий новых строк, действует как пробел. Любой другой комментарий действует как новая строка.
Токены
Токены составляют словарный запас языка Go. Существует четыре класса:
- идентификаторы (Identifiers),
- ключевые слова, (Keywords)
- операторы и пунктуация,
- литералы.
Пространство пробелов, образованное символами (U+0020), горизонтальных табуляций (U+0009), возвратов каретки (U+000D) и новых строк (U+000A), игнорируется, за исключением случаев, когда оно разделяет лексемы, которые в противном случае были бы объединены в одну лексему. Кроме того, новая строка или конец файла могут вызвать вставку точки с запятой.
При разбиении вводимого текста на лексемы следующей лексемой является самая длинная последовательность символов, образующая допустимую лексему.
Точки с запятой
Формальный синтаксис использует точки с запятой ";" в качестве терминаторов в ряде последовательности команд. Программы на Go могут опускать большинство этих точек с запятой, используя следующие два правила:
- Когда ввод разбивается на токены, точка с запятой автоматически вставляется в поток токенов сразу после последнего токена строки, если этот токен
- идентификатор
- целое число, плавающая точка, мнимое число, руна или строковый литерал
- одно из ключевых слов break, continue, fallthrough или return
- один из операторов и пунктуации ++, –, ), ], или }.
- Чтобы сложные операторы занимали одну строку, перед закрывающим оператором “)” или “}” можно опустить точку с запятой.
Чтобы отразить идиоматическое использование, в примерах кода в этом документе точки с запятой опускаются в соответствии с этими правилами.
(Идентификаторы) Identifiers
Идентификаторы называют программные объекты, такие как переменные и типы. Идентификатор - это последовательность из одной или нескольких букв и цифр. Первый символ в идентификаторе должен быть буквой.
identifier = letter { letter | unicode_digit } .
a
_x9
ThisVariableIsExported
αβ
Ключевые слова (Keywords)
Следующие ключевые слова зарезервированы и не могут быть использованы в качестве идентификаторов.
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
Операторы и пунктуация
Следующие последовательности символов представляют операторы (включая операторы присваивания) и пунктуацию:
+ & += &= && == != ( )
- | -= |= || < <= [ ]
* ^ *= ^= <- > >= { }
/ << /= <<= ++ = := , ;
% >> %= >>= -- ! ... . :
&^ &^=
Целочисленные литералы
Целочисленный литерал - это последовательность цифр, представляющая целочисленную константу. Необязательный префикс задает недесятичное основание: 0b или 0B для двоичной системы, 0, 0o или 0O для восьмеричной и 0x или 0X для шестнадцатеричной [Go 1.13]. Одиночный 0 считается десятичным нулем. В шестнадцатеричных литералах буквы от a до f и от A до F обозначают значения от 10 до 15.
Для удобства чтения после базового префикса или между последовательными цифрами может стоять символ подчеркивания _; такое подчеркивание не меняет значения литерала.
int_lit = decimal_lit | binary_lit | octal_lit | hex_lit .
decimal_lit = "0" | ( "1" … "9" ) [ [ "_" ] decimal_digits ] .
binary_lit = "0" ( "b" | "B" ) [ "_" ] binary_digits .
octal_lit = "0" [ "o" | "O" ] [ "_" ] octal_digits .
hex_lit = "0" ( "x" | "X" ) [ "_" ] hex_digits .
decimal_digits = decimal_digit { [ "_" ] decimal_digit } .
binary_digits = binary_digit { [ "_" ] binary_digit } .
octal_digits = octal_digit { [ "_" ] octal_digit } .
hex_digits = hex_digit { [ "_" ] hex_digit } .
42
4_2
0600
0_600
0o600
0O600 // second character is capital letter 'O'
0xBadFace
0xBad_Face
0x_67_7a_2f_cc_40_c6
170141183460469231731687303715884105727
170_141183_460469_231731_687303_715884_105727
_42 // an identifier, not an integer literal
42_ // invalid: _ must separate successive digits
4__2 // invalid: only one _ at a time
0_xBadFace // invalid: _ must separate successive digits
Литералы с плавающей точкой
Литерал с плавающей точкой - это десятичное или шестнадцатеричное представление константы с плавающей точкой.
Десятичный литерал с плавающей точкой состоит из целой части (десятичных цифр), десятичной точки, дробной части (десятичных цифр) и экспоненты (e или E, за которой следует необязательный знак и десятичные цифры). Одна из частей целого числа или дробная часть могут быть опущены; одна из десятичных точек или часть экспоненты могут быть опущены. Значение экспоненты exp увеличивает мантиссу (целую и дробную части) на (10^{exp}).
Шестнадцатеричный литерал с плавающей точкой состоит из префикса 0x или 0X, целой части (шестнадцатеричные цифры), точки радикса, дробной части (шестнадцатеричные цифры) и части экспоненты (p или P, за которой следует необязательный знак и десятичные цифры). Одна из частей целого числа или дробная часть могут быть опущены; точка радикса также может быть опущена, но часть экспоненты обязательна. (Этот синтаксис соответствует синтаксису, приведенному в IEEE 754-2008 §5.12.3.) Значение экспоненты exp увеличивает мантиссу (целую и дробную части) на (2^{exp}) [Go 1.13].
Для удобства чтения после базового префикса или между последовательными цифрами может стоять символ подчеркивания _; такое подчеркивание не меняет буквенного значения.
float_lit = decimal_float_lit | hex_float_lit .
decimal_float_lit = decimal_digits "." [ decimal_digits ] [ decimal_exponent ] |
decimal_digits decimal_exponent |
"." decimal_digits [ decimal_exponent ] .
decimal_exponent = ( "e" | "E" ) [ "+" | "-" ] decimal_digits .
hex_float_lit = "0" ( "x" | "X" ) hex_mantissa hex_exponent .
hex_mantissa = [ "_" ] hex_digits "." [ hex_digits ] |
[ "_" ] hex_digits |
"." hex_digits .
hex_exponent = ( "p" | "P" ) [ "+" | "-" ] decimal_digits .
0.
72.40
072.40 // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
1_5. // == 15.0
0.15e+0_2 // == 15.0
0x1p-2 // == 0.25
0x2.p10 // == 2048.0
0x1.Fp+0 // == 1.9375
0X.8p-0 // == 0.5
0X_1FFFP-16 // == 0.1249847412109375
0x15e-2 // == 0x15e - 2 (integer subtraction)
0x.p1 // invalid: mantissa has no digits
1p-2 // invalid: p exponent requires hexadecimal mantissa
0x1.5e-2 // invalid: hexadecimal mantissa requires p exponent
1_.5 // invalid: _ must separate successive digits
1._5 // invalid: _ must separate successive digits
1.5_e1 // invalid: _ must separate successive digits
1.5e_1 // invalid: _ must separate successive digits
1.5e1_ // invalid: _ must separate successive digits
Мнимые литералы (imaging)
Мнимый литерал представляет собой мнимую часть комплексной константы. Он состоит из целого литерала или литерала с плавающей точкой, за которым следует строчная буква i. Значение мнимого литерала - это значение соответствующего целого литерала или литерала с плавающей точкой, умноженное на мнимую единицу i [Go 1.13].
imaginary_lit = (decimal_digits | int_lit | float_lit) "i" .
В целях обратной совместимости целая часть мнимого литерала, полностью состоящая из десятичных цифр (и, возможно, знаков подчеркивания), считается десятичным целым числом, даже если оно начинается с ведущего 0.
0i
0123i // == 123i for backward-compatibility
0o123i // == 0o123 * 1i == 83i
0xabci // == 0xabc * 1i == 2748i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i
0x1p-2i // == 0x1p-2 * 1i == 0.25i
Литералы рун
Литерал руны представляет собой константу руны, целочисленное значение, идентифицирующее код Юникода. Рунический литерал выражается в виде одного или нескольких символов, заключенных в одинарные кавычки, как, например, 'x' или '\n'. Внутри кавычек может находиться любой символ, кроме новой строки и неэкранированной одинарной кавычки. Одиночный символ в кавычках представляет собой значение Unicode самого символа, а многосимвольные последовательности, начинающиеся с обратной косой черты, кодируют значения в различных форматах.
Простейшая форма представляет собой одиночный символ внутри кавычек; поскольку исходный текст Go - это символы Юникода, закодированные в UTF-8, несколько байтов в кодировке UTF-8 могут представлять одно целое значение.
Например, литерал 'a' содержит один байт, представляющий литерал a, Unicode U+0061, значение 0x61, а литерал 'ä' содержит два байта (0xc3 0xa4), представляющих литерал a-dieresis, U+00E4, значение 0xe4.
Несколько обратных косых черточек позволяют кодировать произвольные значения как ASCII-текст. Существует четыре способа представления целочисленного значения в виде числовой константы:
\x с последующими ровно двумя шестнадцатеричными цифрами;
\u с последующими ровно четырьмя шестнадцатеричными цифрами;
\U с последующими ровно восемью шестнадцатеричными цифрами,
- и обычный обратный слеш
\ с последующими ровно тремя восьмеричными цифрами.
В каждом случае значение литерала - это значение, представленное цифрами в соответствующем базисе.
Хотя все эти представления приводят к целому числу, они имеют разные диапазоны допустимых значений.
Октальные эскейпы должны представлять значение от 0 до 255 включительно. Шестнадцатеричные эскейпы удовлетворяют этому условию по своей конструкции. Эскапады \u и \U представляют кодовые точки Юникода, поэтому в них некоторые значения являются недопустимыми, в частности, значения выше 0x10FFFF и суррогатные половинки.
После обратной косой черты некоторые односимвольные эскейпы представляют специальные значения:
\a U+0007 alert or bell
\b U+0008 backspace
\f U+000C form feed
\n U+000A line feed or newline
\r U+000D carriage return
\t U+0009 horizontal tab
\v U+000B vertical tab
\\ U+005C backslash
\' U+0027 single quote (valid escape only within rune literals)
\" U+0022 double quote (valid escape only within string literals)
Нераспознанный символ, следующий за обратной косой чертой в литерале руны, является незаконным.
rune_lit = "'" ( unicode_value | byte_value ) "'" .
unicode_value = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value = octal_byte_value | hex_byte_value .
octal_byte_value = `\` octal_digit octal_digit octal_digit .
hex_byte_value = `\` "x" hex_digit hex_digit .
little_u_value = `\` "u" hex_digit hex_digit hex_digit hex_digit .
big_u_value = `\` "U" hex_digit hex_digit hex_digit hex_digit
hex_digit hex_digit hex_digit hex_digit .
escaped_char = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
'a'
'ä'
'本'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'
'\'' // rune literal containing single quote character
'aa' // illegal: too many characters
'\k' // illegal: k is not recognized after a backslash
'\xa' // illegal: too few hexadecimal digits
'\0' // illegal: too few octal digits
'\400' // illegal: octal value over 255
'\uDFFF' // illegal: surrogate half
'\U00110000' // illegal: invalid Unicode code point
Строковые литералы
Строковый литерал представляет собой строковую константу, полученную в результате конкатенации последовательности символов. Существует две формы: необработанные строковые литералы и интерпретированные строковые литералы.
Необработанные строковые литералы - это последовательности символов между обратными кавычками, как в `foo`.
Внутри кавычек может находиться любой символ, кроме обратной кавычки. Значением сырого строкового литерала является строка, состоящая из неинтерпретированных (неявно UTF-8-кодированных) символов между кавычками; в частности, обратные косые черты не имеют специального значения, и строка может содержать новые строки. Символы возврата каретки ('\r') внутри необработанных строковых литералов отбрасываются из значения необработанной строки.
Интерпретируемые строковые литералы - это последовательности символов между двойными кавычками, как в слове “bar”. Внутри кавычек может появиться любой символ, кроме новой строки и двойной кавычки без сопровождения. Текст между кавычками образует значение литерала, при этом обратные косые черты интерпретируются так же, как и в рунных литералах (за исключением того, что \' является незаконным, а \" - законным), с теми же ограничениями. Трехзначные восьмеричные (\nnn) и двузначные шестнадцатеричные (\xnn) эскапады представляют отдельные байты результирующей строки; все остальные эскапады представляют (возможно, многобайтовую) кодировку UTF-8 отдельных символов. Таким образом, внутри строки литералы \377 и \xFF представляют один байт значения 0xFF=255, а ÿ, \u00FF, \U000000FF и \xc3\xbf представляют два байта 0xc3 0xbf кодировки UTF-8 символа U+00FF.
string_lit = raw_string_lit | interpreted_string_lit .
raw_string_lit = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"` .
`abc` // same as "abc"
`\n
\n` // same as "\\n\n\\n"
"\n"
"\"" // same as `"`
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800" // illegal: surrogate half
"\U00110000" // illegal: invalid Unicode code point
Все эти примеры представляют одну и ту же строку:
"日本語" // UTF-8 input text
`日本語` // UTF-8 input text as a raw literal
"\u65e5\u672c\u8a9e" // the explicit Unicode code points
"\U000065e5\U0000672c\U00008a9e" // the explicit Unicode code points
"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" // the explicit UTF-8 bytes
Если исходный код представляет символ как две кодовые позиции, например, комбинированную форму, включающую ударение и букву, то при помещении в литерал руны результат будет ошибкой (это не одна кодовая позиция), а при помещении в строковый литерал - двумя кодовыми позициями.
Константы
Существуют булевы константы, рунные константы, целочисленные константы, константы с плавающей точкой, комплексные константы и строковые константы.
Руны, целые числа, константы с плавающей точкой и комплексные константы в совокупности называются числовыми константами.
Постоянное значение представлено руной, целым числом, плавающей точкой, мнимым или строковым литералом, идентификатором, обозначающим константу, постоянным выражением, преобразованием с результатом, являющимся константой, или значением результата некоторых встроенных функций, таких как min или max, применяемых к аргументам констант, unsafe.Sizeof, применяемых к определенным значениям, cap или len, применяемых к некоторым выражениям, real и imag, применяемых к комплексной константе, и complex, применяемых к числовым константам. Булевы истинностные значения представлены заранее объявленными константами true и false. Объявленный идентификатор iota обозначает целочисленную константу.
В общем случае комплексные константы являются одной из форм выражения констант и рассматриваются в этом разделе.
Числовые константы представляют собой точные значения произвольной точности и не переполняются. Следовательно, не существует констант, обозначающих отрицательный ноль, бесконечность и не-число по стандарту IEEE 754.
Константы могут быть типизированными или нетипизированными. Буквальные константы, true, false, iota, а также некоторые константные выражения, содержащие только нетипизированные операнды констант, являются нетипизированными.
Тип константы может быть задан явно при объявлении или преобразовании константы, или неявно при использовании в объявлении переменной, операторе присваивания или в качестве операнда в выражении. Если значение константы не может быть представлено как значение соответствующего типа, то это ошибка. Если тип является параметром типа, константа преобразуется в неконстантное значение параметра типа.
Ограничение на реализацию:
Хотя числовые константы имеют произвольную точность в языке, компилятор может реализовать их, используя внутреннее представление с ограниченной точностью. Тем не менее, каждая реализация должна:
- Представлять целочисленные константы не менее чем 256 битами.
- Представлять константы с плавающей точкой, включая части комплексной константы, с мантиссой не менее 256 бит и знаковой двоичной экспонентой не менее 16 бит.
- Указывать ошибку, если не может точно представить целую константу.
- Давать ошибку, если не может представить константу с плавающей точкой или комплексную константу из-за переполнения.
- Округлять до ближайшей представимой константы, если не может представить константу с плавающей точкой или комплексную константу из-за ограничений на точность.
Эти требования относятся как к литеральным константам, так и к результатам вычисления константных выражений.
Переменные
Переменная - это место хранения значения. Набор допустимых значений определяется типом переменной.
Объявление переменной или, для параметров и результатов функций, подпись объявления функции или литерала функции резервирует место для хранения именованной переменной. Вызов встроенной функции new или получение адреса составного литерала выделяет место для хранения переменной во время выполнения. На такую анонимную переменную ссылаются через (возможно, неявный) указатель.
Структурированные переменные типов array, slice и struct имеют элементы и поля, к которым можно обращаться по отдельности. Каждый такой элемент действует как переменная.
Статический тип (или просто тип) переменной - это тип, указанный в ее объявлении, тип, указанный в новом вызове или составном литерале, или тип элемента структурированной переменной. Переменные интерфейсного типа также имеют отдельный динамический тип, который представляет собой (неинтерфейсный) тип значения, присваиваемого переменной во время выполнения (если только это значение не является заранее объявленным идентификатором nil, который не имеет типа). Динамический тип может меняться в процессе выполнения, но значения, хранящиеся в интерфейсных переменных, всегда можно присвоить статическому типу переменной.
var x interface{} // x is nil and has static type interface{}
var v *T // v has value nil, static type *T
x = 42 // x has value 42 and dynamic type int
x = v // x has value (*T)(nil) and dynamic type *T
Значение переменной можно получить, обратившись к ней в выражении; это самое последнее значение, присвоенное переменной. Если переменной еще не присвоено значение, ее значение равно нулю для ее типа.
Типы
Тип определяет набор значений вместе с операциями и методами, характерными для этих значений. Тип может обозначаться именем типа, если оно есть, за которым должны следовать аргументы типа, если тип является общим. Тип также может быть указан с помощью литерала типа, который составляет тип из существующих типов.
Type = TypeName [ TypeArgs ] | TypeLit | "(" Type ")" .
TypeName = identifier | QualifiedIdent .
TypeArgs = "[" TypeList [ "," ] "]" .
TypeList = Type { "," Type } .
TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
SliceType | MapType | ChannelType .
Язык заранее объявляет некоторые имена типов. Другие вводятся с помощью деклараций типов или списков параметров типов. Составные типы - массив, структура, указатель, функция, интерфейс, фрагмент, карта и канальный тип - могут быть построены с помощью литералов типов.
Объявленные типы, определенные типы и параметры типов называются именованными типами. Псевдоним обозначает именованный тип, если тип, указанный в объявлении псевдонима, является именованным типом.
Boolean types (Булевы типы)
Булевый тип представляет собой набор булевых истинностных значений, обозначаемых заранее объявленными константами true и false. Предварительно объявленный булевский тип - bool; это определенный тип.
Numeric types (Числовые типы)
Целый, плавающий или комплексный тип представляет собой набор целых, плавающих или комплексных значений, соответственно. Все они называются числовыми типами. К заранее объявленным независимым от архитектуры числовым типам относятся:
uint8 the set of all unsigned 8-bit integers (0 to 255)
uint16 the set of all unsigned 16-bit integers (0 to 65535)
uint32 the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615)
int8 the set of all signed 8-bit integers (-128 to 127)
int16 the set of all signed 16-bit integers (-32768 to 32767)
int32 the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
float32 the set of all IEEE 754 32-bit floating-point numbers
float64 the set of all IEEE 754 64-bit floating-point numbers
complex64 the set of all complex numbers with float32 real and imaginary parts
complex128 the set of all complex numbers with float64 real and imaginary parts
byte alias for uint8
rune alias for int32
Значение n-битного целого числа имеет ширину n бит и представляется с помощью арифметики дополнения двойки.
Существует также набор предопределенных целочисленных типов с размерами, зависящими от реализации:
uint //32 или 64 бита
int //тот же размер, что и uint
uintptr //целое число без знака, достаточно большое для хранения неинтерпретированных битов значения указателя
Чтобы избежать проблем с переносимостью, все числовые типы являются определенными типами и, следовательно, отдельными, за исключением byte, который является псевдонимом для uint8, и rune, который является псевдонимом для int32. Явные преобразования требуются, когда различные числовые типы смешиваются в выражении или присваивании.
Например, int32 и int не являются одним и тем же типом, даже если они могут иметь одинаковый размер на определенной архитектуре.
String types (Строковые типы)
Строковый тип представляет собой набор строковых значений.
Строковое значение - это последовательность байтов (возможно, пустая). Количество байтов называется длиной строки и никогда не бывает отрицательным. Строки неизменяемы: после создания содержимое строки невозможно изменить. Объявленный тип строки - string; это определенный тип.
Длина строки s может быть определена с помощью встроенной функции len. Длина является константой времени компиляции, если строка является константой. Доступ к байтам строки осуществляется по целочисленным индексам от 0 до len(s)-1. Взятие адреса такого элемента недопустимо; если s[i] - это i-й байт строки, то &s[i] недопустимо.
Array types (Типы массивов)
Массив - это пронумерованная последовательность элементов одного типа, называемого типом элемента. Количество элементов называется длиной массива и никогда не бывает отрицательным.
ArrayType = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .
Длина является частью типа массива; она должна быть оценена неотрицательной константой, представляемой значением типа int. Длину массива a можно узнать с помощью встроенной функции len. Элементы можно адресовать по целочисленным индексам от 0 до len(a)-1. Типы массивов всегда одномерны, но могут образовывать многомерные типы.
[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64 // same as [2]([2]([2]float64))
Массив типа T не может иметь элемент типа T или типа, содержащего T в качестве компонента, прямо или косвенно, если эти содержащие типы являются только типами array или struct.
// недопустимые типы массивов
type (
T1 [10]T1 // тип элемента T1 - T1
T2 [10]struct{ f T2 } // T2 содержит T2 как компонент struct
T3 [10]T4 // T3 содержит T3 как компонент struct в T4
T4 struct{ f T3 } // T4 содержит T4 как компонент массива T3 в struct
)
// допустимые типы массивов
type (
T5 [10]*T5 // T5 содержит T5 как компонент указателя
T6 [10]func() T6 // T6 содержит T6 как компонент функции типа
T7 [10]struct{ f []T7 } // T7 содержит T7 как компонент фрагмента в структуре
)
Slice types (Типы срезов)
Срез - это дескриптор для непрерывного сегмента нижележащего массива, который обеспечивает доступ к пронумерованной последовательности элементов из этого массива. Тип slice обозначает множество всех срезов массивов его типа элемента. Количество элементов называется длиной среза и никогда не бывает отрицательным. Значение неинициализированного среза равно nil.
SliceType = "[" "]" ElementType .
Длину фрагмента s можно определить с помощью встроенной функции len; в отличие от массивов, она может меняться в процессе выполнения. К элементам можно обращаться по целочисленным индексам от 0 до len(s)-1. Индекс фрагмента данного элемента может быть меньше, чем индекс того же элемента в нижележащем массиве.
Слайс, после инициализации, всегда связан с нижележащим массивом, в котором хранятся его элементы. Таким образом, фрагмент разделяет хранилище со своим массивом и с другими фрагментами того же массива; напротив, отдельные массивы всегда представляют собой отдельные хранилища.
Массив, лежащий в основе фрагмента, может простираться за пределы фрагмента. Емкость - это мера этой протяженности: она равна сумме длины среза и длины массива за пределами среза; срез длиной до этой емкости может быть создан путем нарезки нового среза из исходного среза. Вместимость среза a можно узнать с помощью встроенной функции cap(a).
Новое инициализированное значение среза для заданного типа элемента T можно создать с помощью встроенной функции make, которая принимает тип среза и параметры, указывающие длину и, опционально, емкость. При создании среза с помощью make всегда выделяется новый скрытый массив, на который ссылается возвращаемое значение среза. Иными словами, выполнение функции
make([]T, length, capacity)
дает тот же срез, что и выделение массива и его срез, поэтому эти два выражения эквивалентны:
make([]int, 50, 100)
new([100]int)[0:50]
Как и массивы, срезы всегда одномерны, но могут быть скомпонованы для создания объектов более высокой размерности. В массивах массивов внутренние массивы, по конструкции, всегда имеют одинаковую длину; однако в срезах срезов (или массивах срезов) внутренние длины могут динамически изменяться. Более того, внутренние срезы должны инициализироваться по отдельности.
Struct types (Типы структур)
Структура - это последовательность именованных элементов, называемых полями, каждый из которых имеет имя и тип. Имена полей могут быть заданы явно (IdentifierList) или неявно (EmbeddedField). Внутри структуры непустые имена полей должны быть уникальными.
StructType = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag = string_lit .
// Пустая структура.
struct {}
// Структура с 6 полями.
struct {
x, y int
u float32
_ float32 // подложка (пустышка)
A *[]int
F func()
}
Поле, объявленное с типом, но без явного имени поля, называется встроенным полем. Встроенное поле должно быть указано как имя типа T или как указатель на имя неинтерфейсного типа *T, причем сам T не может быть типом-указателем или параметром типа. Неквалифицированное имя типа выступает в качестве имени поля.
// Структура с четырьмя встроенными полями типов T1, *T2, P.T3 и *P.T4
struct {
T1 // имя поля - T1
*T2 // имя поля - T2
P.T3 // имя поля - T3
*P.T4 // имя поля - T4
x, y int // имена полей - x и y
}
Следующее объявление является незаконным, поскольку имена полей должны быть уникальными в типе struct:
struct {
T // конфликтует со встроенным полем *T и *P.T
*T // конфликтует со встроенным полем T и *P.T
*P.T // конфликтует со встроенным полем T и *T
}
Поле или метод f встроенного поля в структуре x называется продвинутым, если x.f является легальным селектором, обозначающим это поле или метод f.
Продвигаемые поля действуют как обычные поля структуры, за исключением того, что они не могут быть использованы в качестве имен полей в составных литералах структуры.
Если задан тип struct S и имя типа T, то продвигаемые методы включаются в набор методов struct следующим образом:
- Если
S содержит встроенное поле T, то наборы методов S и *S оба включают продвигаемые методы с приемником T. Набор методов *S также включает продвигаемые методы с приемником *T.
- Если
S содержит встроенное поле *T, то наборы методов S и *S оба включают продвигаемые методы с приемником T или *T.
За объявлением поля может следовать необязательный строковый литерал tag, который становится атрибутом для всех полей в соответствующем объявлении поля.
Пустая строка тега эквивалентна отсутствию тега. Теги становятся видимыми через интерфейс отражения и участвуют в идентификации типов для структур, но в остальном игнорируются.
struct {
x, y float64 "" // пустая строка тега подобна отсутствующему тегу
name string "в качестве тега допускается любая строка"
_ [4]byte "Это не структурное поле"
}
// Структура, соответствующая буферу протокола TimeStamp.
// Строки тегов определяют номера полей буфера протокола;
// они следуют соглашению, изложенному в пакете reflect.
struct {
microsec uint64 `protobuf: "1"`
serverIP6 uint64 `protobuf: "2"`
}
Тип struct T не может содержать поле типа T или типа, содержащего T в качестве компонента, прямо или косвенно, если эти содержащие типы являются только типами array или struct.
// недопустимые типы структур
type (
T1 struct{ T1 } // T1 содержит поле T1
T2 struct{ f [10]T2 } // T2 содержит T2 как компонент массива
T3 struct{ T4 } // T3 содержит T3 как компонент массива в struct T4
T4 struct{ f [10]T3 } // T4 содержит T4 как компонент struct T3 в массиве
)
// допустимые типы struct
type (
T5 struct{ f *T5 } // T5 содержит T5 как компонент указателя
T6 struct{ f func() T6 } // T6 содержит T6 как компонент функции type
T7 struct{ f [10][]T7 } // T7 содержит T7 как компонент фрагмента массива
)
Pointer types (Типы указателей)
Тип указателя обозначает множество всех указателей на переменные данного типа, называемого базовым типом указателя. Значением неинициализированного указателя является nil.
PointerType = "*" BaseType .
BaseType = Type .
*Point
*[4]int
Function types (Типы функций)
Тип функции обозначает множество всех функций с одинаковыми типами параметров и результатов. Значением неинициализированной переменной типа функции является nil.
FunctionType = "func" Signature .
Signature = Parameters [ Result ] .
Result = Parameters | Type .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
ParameterList = ParameterDecl { "," ParameterDecl } .
ParameterDecl = [ IdentifierList ] [ "..." ] Type .
В списке параметров или результатов имена (IdentifierList) должны либо все присутствовать, либо все отсутствовать. Если они присутствуют, каждое имя обозначает один элемент (параметр или результат) указанного типа, и все непустые имена в сигнатуре должны быть уникальными. Если они отсутствуют, то каждый тип обозначает один элемент этого типа. Списки параметров и результатов всегда заключаются в круглые скобки, за исключением того, что если имеется ровно один безымянный результат, он может быть записан как тип без скобок.
Последний входящий параметр в сигнатуре функции может иметь тип с префиксом .... Функция с таким параметром называется переменной и может быть вызвана с нулем или более аргументов для этого параметра.
func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)
Interface types (Типы интерфейсов)
Тип интерфейса определяет набор типов. Переменная интерфейсного типа может хранить значение любого типа, входящего в набор типов интерфейса. Считается, что такой тип реализует интерфейс. Значение неинициализированной переменной интерфейсного типа равно nil.
InterfaceType = "interface" "{" { InterfaceElem ";" } "}" .
InterfaceElem = MethodElem | TypeElem .
MethodElem = MethodName Signature .
MethodName = identifier .
TypeElem = TypeTerm { "|" TypeTerm } .
TypeTerm = Type | UnderlyingType .
UnderlyingType = "~" Type .
Тип интерфейса задается списком элементов интерфейса. Элемент интерфейса - это либо метод, либо элемент типа, где элемент типа - это объединение одного или нескольких терминов типа. Термин типа - это либо один тип, либо один базовый тип.
Основные интерфейсы
В своей самой простой форме интерфейс задает (возможно, пустой) список методов. Набор типов, определяемый таким интерфейсом, - это набор типов, реализующих все эти методы, а соответствующий набор методов состоит именно из методов, указанных интерфейсом. Интерфейсы, чьи наборы типов могут быть полностью определены списком методов, называются базовыми интерфейсами.
// Простой интерфейс File.
interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
Close() error
}
Имя каждого явно указанного метода должно быть уникальным и не пустым.
interface {
String() string
String() string // ошибка: String не уникальна
_(x int) // ошибка: метод не должен иметь пустого имени
}
Интерфейс может быть реализован более чем одним типом. Например, если два типа S1 и S2 имеют набор методов
func (p T) Read(p []byte) (n int, err error)
func (p T) Write(p []byte) (n int, err error)
func (p T) Close() error
(где T обозначает S1 или S2), то интерфейс File реализуется как S1, так и S2, независимо от того, какие еще методы могут быть у S1 и S2.
Каждый тип, входящий в набор типов интерфейса, реализует этот интерфейс. Любой тип может реализовывать несколько различных интерфейсов. Например, все типы реализуют пустой интерфейс, который обозначает множество всех (неинтерфейсных) типов:
Для удобства предопределенный тип any является псевдонимом для пустого интерфейса. [Go 1.18].
Аналогично, рассмотрим эту спецификацию интерфейса, которая появляется внутри объявления типа для определения интерфейса под названием Locker:
type Locker interface {
Lock()
Unlock()
}
Если S1 и S2 также реализуют
func (p T) Lock() { … }
func (p T) Unlock() { … }
они реализуют интерфейс Locker, а также интерфейс File.
Встроенные интерфейсы
В несколько более общей форме интерфейс T может использовать (возможно, квалифицированное) имя типа интерфейса E в качестве элемента интерфейса. Это называется встраиванием интерфейса E в T [Go 1.14]. Набор типов T - это пересечение наборов типов, определяемых явно объявленными методами T, и наборов типов встроенных интерфейсов T. Другими словами, набор типов T - это множество всех типов, реализующих все явно объявленные методы T, а также все методы E [Go 1.18].
type Reader interface {
Read(p []byte) (n int, err error)
Close() error
}
type Writer interface {
Write(p []byte) (n int, err error)
Close() error
}
// Методы ReadWriter - это Read, Write, и Close.
type ReadWriter interface {
Reader // includes methods of Reader in ReadWriter's method set
Writer // includes methods of Writer in ReadWriter's method set
}
При встраивании интерфейсов методы с одинаковыми именами должны иметь идентичные сигнатуры.
type ReadCloser interface {
Reader // includes methods of Reader in ReadCloser's method set
Close() // ошибка: сигнатуры Reader.Close и Close отличаются друг от друга
}
Общие интерфейсы
В самом общем виде элемент интерфейса может быть также термином произвольного типа T, или термином вида ~T, задающим базовый тип T, или объединением терминов t1|t2|...|tn [Go 1.18]. Вместе со спецификациями методов эти элементы позволяют точно определить набор типов интерфейса следующим образом:
- Набор типов пустого интерфейса - это набор всех типов, не входящих в интерфейс.
- Набор типов непустого интерфейса - это пересечение наборов типов его интерфейсных элементов.
- Набор типов спецификации метода - это набор всех неинтерфейсных типов, чьи наборы методов включают этот метод.
- Набор типов термина типа, не являющегося интерфейсом, - это множество, состоящее только из этого типа.
- Множество типов термина вида
~T - это множество всех типов, базовым типом которых является T.
- Множество типов объединения терминов
t1|t2|...|tn - это объединение множеств типов этих терминов.
Квантификация “множество всех неинтерфейсных типов” относится не только ко всем (неинтерфейсным) типам, объявленным в рассматриваемой программе, но и ко всем возможным типам во всех возможных программах, и, следовательно, является бесконечной. Аналогично, если задано множество всех неинтерфейсных типов, реализующих определенный метод, то пересечение множеств методов этих типов будет содержать именно этот метод, даже если все типы в рассматриваемой программе всегда используют этот метод в паре с другим методом.
По конструкции, набор типов интерфейса никогда не содержит интерфейсного типа.
// Интерфейс, представляющий только тип int.
interface {
int
}
// Интерфейс, представляющий все типы с базовым типом int.
interface {
~int
}
// Интерфейс, представляющий все типы с базовым типом int, реализующие метод String.
interface {
~int
String() string
}
// Интерфейс, представляющий пустой набор типов: не существует типа, который был бы одновременно и int, и string.
interface {
int
string
}
В термине вида ~T базовым типом T должен быть он сам, и T не может быть интерфейсом.
type MyInt int
interface {
~[]byte // базовым типом []byte является он сам
~MyInt // ошибка: базовым типом MyInt не является MyInt
~error // ошибка: ошибка является интерфейсом
}
Элементы Union обозначают объединения наборов типов:
// Интерфейс Float представляет все типы с плавающей точкой
// (включая любые именованные типы, базовыми типами которых являются
// либо float32, либо float64).
type Float interface {
~float32 | ~float64
}
Тип T в термине вида T или ~T не может быть параметром типа, а наборы типов всех неинтерфейсных терминов должны быть попарно расходящимися (попарное пересечение наборов типов должно быть пустым). Если задан параметр типа P:
interface {
P // ошибка: P является параметром типа
int | ~P // ошибка: P является параметром типа
~int | MyInt // ошибка: наборы типов для ~int и MyInt не разделяются (~int включает MyInt)
float32 | Float // пересекающиеся наборы типов, но Float - это интерфейс
}
Ограничение на реализацию: Объединение (с более чем одним членом) не может содержать заранее объявленный идентификатор comparable или интерфейсов, определяющих методы, или встраивать comparable или интерфейсы, определяющие методы.
Интерфейсы, которые не являются базовыми, могут использоваться только как ограничения типов или как элементы других интерфейсов, используемых в качестве ограничений. Они не могут быть типами значений или переменных, или компонентами других, неинтерфейсных типов.
var x Float // illegal: Float is not a basic interface
var x interface{} = Float(nil) // illegal
type Floatish struct {
f Float // illegal
}
Интерфейсный тип T не может встраивать элемент типа, который является, содержит или встраивает T, прямо или косвенно.
// illegal: Bad не может встраивать себя
type Bad interface {
Bad
}
// illegal: Bad1 не может встраивать себя, используя Bad2
type Bad1 interface {
Bad2
}
type Bad2 interface {
Bad1
}
// illegal: Bad3 не может встраивать объединение, содержащее Bad3
type Bad3 interface {
~int | ~string | Bad3
}
// illegal: Bad4 не может встраивать массив, содержащий Bad4 в качестве элемента type
type Bad4 interface {
[10]Bad4
}
Реализация интерфейса
Тип T реализует интерфейс I, если
T не является интерфейсом и является элементом множества типов I; или
T является интерфейсом и множество типов T является подмножеством множества типов I.
Значение типа T реализует интерфейс, если T реализует интерфейс.
Map types (Типы карт)
Карта - это неупорядоченная группа элементов одного типа, называемого типом элемента, индексируемых набором уникальных ключей другого типа, называемого типом ключа. Значением неинициализированной карты является nil.
MapType = "map" "[" KeyType "]" ElementType .
KeyType = Type .
Операторы сравнения == и != должны быть полностью определены для операндов типа ключа; таким образом, тип ключа не должен быть функцией, картой или срезом. Если тип ключа является типом интерфейса, эти операторы сравнения должны быть определены для динамических значений ключа; в противном случае произойдет паника во время выполнения.
map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}
Количество элементов карты называется ее длиной. Для карты m она может быть определена с помощью встроенной функции len и может меняться в процессе выполнения. Элементы могут добавляться в процессе выполнения с помощью присваиваний и извлекаться с помощью индексных выражений; они могут быть удалены с помощью встроенных функций delete и clear.
Новое, пустое значение карты создается с помощью встроенной функции make, которая принимает в качестве аргументов тип карты и необязательную подсказку емкости:
make(map[string]int)
make(map[string]int, 100)
Начальная емкость карты не ограничивает ее размер: карты растут по мере увеличения количества хранимых в них элементов, за исключением карт nil. Карта nil эквивалентна пустой карте, за исключением того, что никакие элементы не могут быть добавлены.
Channel types (Типы каналов)
Канал обеспечивает механизм взаимодействия параллельно выполняющихся функций, посылая и принимая значения определенного типа элементов. Значение неинициализированного канала равно nil.
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
Необязательный оператор <- задает направление канала, отправку или прием. Если указано направление, канал является направленным, в противном случае - двунаправленным. Канал может быть ограничен только на отправку или только на прием путем присваивания или явного преобразования.
chan T // может использоваться для отправки и получения значений типа T
chan <- float64 // может использоваться только для отправки float64
<-chan int // может использоваться только для получения ints
Оператор <- ассоциируется с самым левым возможным chan:
chan<- chan int // то же, что chan <- (chan int)
chan<- <-chan int // то же, что chan<- (<-chan int)
<-chan <-chan int // то же, что <-chan (<-chan int)
chan (<-chan int)
Новое инициализированное значение канала может быть создано с помощью встроенной функции make, которая принимает в качестве аргументов тип канала и необязательную емкость:
Емкость, выраженная в количестве элементов, задает размер буфера в канале. Если емкость равна нулю или отсутствует, канал является небуферизованным, и обмен данными происходит только тогда, когда отправитель и получатель готовы. В противном случае канал является буферизованным, и обмен данными происходит без блокировки, если буфер не заполнен (отправка) или не пуст (прием). Канал с нулевым значением никогда не готов к обмену данными.
Канал может быть закрыт с помощью встроенной функции close. Многозначная форма присваивания оператора receive сообщает, было ли полученное значение отправлено до того, как канал был закрыт.
Один канал может использоваться в операторах send, receive, вызовах встроенных функций cap и len любым количеством горутин без дополнительной синхронизации.
Каналы действуют как очереди “первым пришел - первым ушел”.
Например, если одна горутина отправляет значения по каналу, а вторая получает их, то значения будут получены в порядке отправки.
Свойства типов и значений
Представление значений
Значения предопределенных типов (см. ниже интерфейсы any и error), массивов и структур являются самодостаточными: Каждое такое значение содержит полную копию всех своих данных, а переменные таких типов хранят все значение.
Например, переменная массива обеспечивает хранение (переменных) для всех элементов массива. Соответствующие нулевые значения характерны для типов значений; они никогда не бывают нулевыми.
Ненулевые значения указателей, функций, фрагментов, карт и каналов содержат ссылки на базовые данные, которые могут быть общими для нескольких значений:
- Значение указателя - это ссылка на переменную, содержащую значение базового типа указателя.
- Значение функции содержит ссылки на (возможно, анонимную) функцию и вложенные переменные.
- Значение среза содержит длину среза, емкость и ссылку на его базовый массив.
- Значение карты или канала - это ссылка на специфическую для реализации структуру данных карты или канала.
- Значение интерфейса может быть самостоятельным или содержать ссылки на базовые данные в зависимости от динамического типа интерфейса. Объявленный идентификатор
nil является нулевым значением для типов, значения которых могут содержать ссылки.
Когда несколько значений совместно используют базовые данные, изменение одного значения может изменить другое.
Например, изменение элемента среза приводит к изменению этого элемента в базовом массиве для всех срезов, разделяющих этот массив.
Базовые типы
Каждый тип T имеет базовый тип: Если T - один из предопределенных булевых, числовых или строковых типов или литерал типа, то соответствующий базовый тип - это сам T. В противном случае базовый тип T - это базовый тип типа, на который T ссылается в своем объявлении. Для параметра типа это базовый тип его ограничения типа, которое всегда является интерфейсом.
type (
A1 = string
A2 = A1
)
type (
B1 string
B2 B1
B3 []B1
B4 B3
)
func f[P any](x P) { … }
Базовым типом строк A1, A2, B1 и B2 является string. Базовым типом []B1, B3 и B4 является []B1. Базовым типом P является interface{}.
Основные типы
Каждый неинтерфейсный тип T имеет основной тип, который совпадает с базовым типом T.
Интерфейс T имеет основной тип, если выполняется одно из следующих условий:
- Существует единственный тип
U, который является базовым типом всех типов в наборе типов T; или
- набор типов
T содержит только типы каналов с идентичным типом элемента E, и все направленные каналы имеют одинаковое направление.
Никакие другие интерфейсы не имеют базового типа.
Основным типом интерфейса, в зависимости от выполняемого условия, является либо:
- тип
U; либо
- тип
chan E, если T содержит только двунаправленные каналы, либо тип chan<- E или <-chan E в зависимости от направления присутствующих направленных каналов.
По определению, основной тип никогда не является определенным типом, параметром типа или типом интерфейса.
Примеры интерфейсов с основными типами:
type Celsius float32
type Kelvin float32
interface{ int } // int
interface{ Celsius|Kelvin } // float32
interface{ ~chan int } // chan int
interface{ ~chan int|~chan<- int } // chan<- int
interface{ ~[]*data; String() string } // []*data
Примеры интерфейсов без основных типов:
interface{} // нет ни одного базового типа
interface{ Celsius|float64 } // нет единого базового типа
interface{ chan int | chan<- string } // каналы имеют разные типы элементов
interface{ <-chan int | chan<- int } // направленные каналы имеют разные направления
Некоторые операции (выражения slice, append и copy) опираются на несколько более свободную форму основных типов, которые принимают байтовые фрагменты и строки. В частности, если существует ровно два типа, []byte и string, которые являются базовыми типами всех типов в наборе типов интерфейса T, то основной тип T называется bytestring.
Примеры интерфейсов с базовыми типами bytestring:
interface{ int } // int (то же, что и обычный основной тип)
interface{ []byte | string } // bytestring
interface{ ~[]byte | myString } // bytestring
Обратите внимание, что bytestring не является реальным типом; его нельзя использовать для объявления переменных или составления других типов. Он существует исключительно для описания поведения некоторых операций чтения из последовательности байтов, которая может быть байтовым фрагментом или строкой.
Идентичность типов
Два типа могут быть либо одинаковыми, либо разными.
Именованный тип всегда отличается от любого другого типа. В противном случае два типа идентичны, если их базовые литералы типов структурно эквивалентны; то есть они имеют одинаковую структуру литералов и соответствующие компоненты имеют одинаковые типы. Подробнее:
- Два типа массивов идентичны, если у них одинаковые типы элементов и одинаковая длина массива.
- Два типа
slice идентичны, если у них одинаковые типы элементов.
- Два типа
struct идентичны, если у них одинаковая последовательность полей, и если соответствующие пары полей имеют одинаковые имена, одинаковые типы и одинаковые теги, и либо оба встроены, либо оба не встроены. Имена неэкспортируемых полей из разных пакетов всегда различны.
- Два типа указателей идентичны, если у них одинаковые базовые типы.
- Два типа функций идентичны, если у них одинаковое количество параметров и значений результата, соответствующие типы параметров и результатов идентичны, и либо обе функции являются переменными, либо ни одна из них не является таковой. Имена параметров и результатов не обязаны совпадать.
- Два интерфейсных типа идентичны, если они определяют один и тот же набор типов.
- Два типа карт идентичны, если у них одинаковые типы ключей и элементов.
- Два типа каналов идентичны, если у них одинаковые типы элементов и одинаковое направление.
- Два инстанцированных типа идентичны, если их определенные типы и все аргументы типа идентичны.
Учитывая скаазанное:
type (
A0 = []string
A1 = A0
A2 = struct{ a, b int }
A3 = int
A4 = func(A3, float64) *A0
A5 = func(x int, _ float64) *[]string
B0 A0
B1 []string
B2 struct{ a, b int }
B3 struct{ a, c int }
B4 func(int, float64) *B0
B5 func(x int, y float64) *A1
C0 = B0
D0[P1, P2 any] struct{ x P1; y P2 }
E0 = D0[int, string]
)
эти типы идентичны:
A0, A1, and []string
A2 and struct{ a, b int }
A3 and int
A4, func(int, float64) *[]string, and A5
B0 and C0
D0[int, string] and E0
[]int and []int
struct{ a, b *B5 } and struct{ a, b *B5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), and A5
B0 и B1 отличаются, потому что это новые типы, созданные разными определениями типов; func(int, float64) *B0 и func(x int, y float64) *[]string отличаются, потому что B0 отличается от []string; а P1 и P2 отличаются, потому что это разные параметры типа. D0[int, string] и struct{ x int; y string } отличаются тем, что первый является инстанцированным определенным типом, а второй - литералом типа (но они все равно присваиваемые).
Назначаемость
Значение x типа V является назначаемым переменной типа T ("x является назначаемым T"), если выполняется одно из следующих условий:
V и T идентичны.
V и T имеют одинаковые базовые типы, но не являются параметрами типа, и хотя бы один из V или T не является именованным типом.
V и T - канальные типы с идентичными типами элементов, V - двунаправленный канал, и хотя бы один из V или T не является именованным типом.
T - интерфейсный тип, но не параметр типа, и x реализует T.
x - это объявленный идентификатор nil, а T - указатель, функция, фрагмент, карта, канал или интерфейсный тип, но не параметр типа.
x - это нетипизированная константа, представляемая значением типа T.
Кроме того, если для x тип V или T является параметром типа, x можно присвоить переменной типа T, если выполняется одно из следующих условий:
x - предопределенный идентификатор nil, T - параметр типа, и x может быть присвоен каждому типу в наборе типов T.
V не является именованным типом, T - параметр типа, и x может быть присвоен каждому типу в наборе типов T.
V - параметр типа, T - не именованный тип, и значения каждого типа в наборе типов V могут быть присвоены T.
Представимость
Константа x представима значением типа T, где T не является параметром типа, если выполняется одно из следующих условий:
x находится в множестве значений, определяемых T.
T - тип с плавающей точкой, и x может быть округлена до точности T без переполнения. При округлении используются правила округления IEEE 754, но при этом отрицательный ноль IEEE упрощается до беззнакового нуля. Обратите внимание, что постоянные значения никогда не приводят к отрицательному нулю IEEE, NaN или бесконечности.
T - комплексный тип, и компоненты x - real(x) и imag(x) - могут быть представлены значениями типа компонента T (float32 или float64).
Если T - параметр типа, то x может быть представлен значением типа T, если x может быть представлен значением каждого типа в наборе типов T.
| x |
T |
x может быть представлено значением T, потому что |
| ‘a’ |
bite |
97 находится в наборе значений байтов |
| 97 |
rune |
rune - псевдоним для int32, и 97 находится в наборе 32-битных целых чисел |
| “foo” |
string |
“foo” находится в наборе значений строк |
| 1024 |
int16 |
1024 находится в наборе 16-битных целых чисел |
| 42.0 byte |
42 |
находится в наборе беззнаковых 8-битных целых чисел |
| 1e10 |
uint64 |
10000000000 находится в наборе беззнаковых 64-битных целых чисел |
| 2.718281828459045 |
float32 |
2.718281828459045 округляется до 2.7182817, который находится в наборе значений float32 |
| -1e-1000 |
float64 |
-1e-1000 округляется до IEEE -0.0, которое далее упрощается до 0.0 |
| 0i |
int |
0 - целое значение |
| (42 + 0i) |
float32 |
42.0 (с нулевой мнимой частью) находится в наборе значений float32 |
| x |
T |
x не может быть представлено значением T, потому что |
| 0 |
bool |
0 не входит в набор булевых значений |
| ‘a’ |
string |
‘a’ - это руна, она не входит в набор строковых значений |
| 1024 |
byte |
1024 не входит в набор беззнаковых 8-битных целых чисел |
| -1 |
uint16 |
-1 не входит в набор беззнаковых 16-битных целых чисел |
| 1.1 |
int |
1.1 не является целым числом |
| 42i |
float32 |
(0 + 42i) не входит в набор значений float32 |
| 1e1000 |
float64 |
1e1000 после округления переполняется до IEEE +Inf |
Наборы методов
Набор методов типа определяет методы, которые могут быть вызваны на операнде этого типа. Каждый тип имеет (или пустой) набор методов, связанный с ним:
- Набор методов определенного типа
T состоит из всех методов, объявленных для типа-приемника T.
- Набор методов указателя на определенный тип
T (где T не является ни указателем, ни интерфейсом) - это набор всех методов, объявленных с приемником *T или T.
- Набор методов интерфейсного типа - это пересечение наборов методов каждого типа в наборе типов интерфейса (результирующий набор методов обычно представляет собой просто набор объявленных методов в интерфейсе).
К структурам (и указателям на структуры), содержащим встроенные поля, применяются дополнительные правила, описанные в разделе о типах struct. Любой другой тип имеет пустой набор методов.
В наборе методов каждый метод должен иметь уникальное непустое имя метода.
Blocks (Блоки)
Блок - это возможно пустая последовательность деклараций и утверждений, заключенных в соответствующие скобки.
Block = "{" StatementList "}" .
StatementList = { Statement ";" } .
Помимо явных блоков в исходном коде, существуют и неявные блоки:
- Вселенский блок
universe block включает в себя весь исходный текст Go.
- Каждый пакет имеет
package block блок пакета, содержащий весь исходный текст Go для этого пакета.
- Каждый файл имеет
file block блок файла, содержащий весь исходный текст Go в этом файле.
- Каждый оператор “
if”, “for” и “switch” считается находящимся в своем собственном неявном блоке.
- Каждый пункт в операторе “
switch” или “select” действует как неявный блок.
Блоки вложены друг в друга и влияют на область видимости.
Декларации и области видимости
Декларация связывает непустой идентификатор с константой, типом, параметром типа, переменной, функцией, меткой или пакетом. Каждый идентификатор в программе должен быть объявлен. Ни один идентификатор не может быть объявлен дважды в одном и том же блоке, и ни один идентификатор не может быть объявлен как в блоке файла, так и в блоке пакета.
Пустой идентификатор может использоваться как любой другой идентификатор в объявлении, но он не вводит привязку и поэтому не объявляется. В блоке пакетов идентификатор init может использоваться только для объявления функции init, и, как и пустой идентификатор, он не вводит нового связывания.
Declaration = ConstDecl | TypeDecl | VarDecl .
TopLevelDecl = Declaration | FunctionDecl | MethodDecl .
Область видимости объявленного идентификатора - это часть исходного текста, в которой идентификатор обозначает указанную константу, тип, переменную, функцию, метку или пакет.
В Go лексическая область видимости определяется с помощью блоков:
- Область видимости заранее объявленного идентификатора - это блок
universe (вселенной).
- Областью видимости идентификатора, обозначающего константу, тип, переменную или функцию (но не метод), объявленную на верхнем уровне (вне любой функции), является блок
package.
- Областью видимости имени импортируемого пакета является блок файла, содержащий объявление импорта.
- Областью видимости идентификатора, обозначающего приемник метода, параметр функции или переменную результата, является тело функции.
- Область видимости идентификатора, обозначающего параметр типа функции или объявленный приемником метода, начинается после имени функции и заканчивается в конце тела функции.
- Область видимости идентификатора, обозначающего параметр типа, начинается после имени типа и заканчивается в конце
TypeSpec.
- Область видимости идентификатора константы или переменной, объявленной внутри функции, начинается в конце
ConstSpec или VarSpec (ShortVarDecl для объявлений коротких переменных) и заканчивается в конце самого внутреннего содержащего блока.
- Область видимости идентификатора типа, объявленного внутри функции, начинается с идентификатора в
TypeSpec и заканчивается в конце самого внутреннего содержащего блока.
Идентификатор, объявленный в блоке, может быть повторно объявлен во внутреннем блоке. Пока идентификатор внутреннего объявления находится в области видимости, он обозначает объект, объявленный внутренним объявлением.
Клаузула package не является объявлением; имя пакета не появляется ни в какой области видимости. Его назначение - идентифицировать файлы, принадлежащие одному пакету, и указать имя пакета по умолчанию для объявлений импорта.
Области применения меток
Метки объявляются в операторах с метками и используются в операторах “break”, “continue” и “goto”. Запрещено определять метку, которая никогда не используется. В отличие от других идентификаторов, метки не имеют блочного диапазона и не конфликтуют с идентификаторами, которые не являются метками. Областью действия метки является тело функции, в которой она объявлена, и исключает тело любой вложенной функции.
Пустой идентификатор
Пустой идентификатор обозначается символом подчеркивания _. Он служит анонимным заполнителем вместо обычного (непустого) идентификатора и имеет особое значение в объявлениях, в качестве операнда и в операторах присваивания.
Объявленные идентификаторы
Следующие идентификаторы неявно объявлены в блоке universe [Go 1.18] [Go 1.21]:
Types:
any bool byte comparable
complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr
Constants:
true false iota
Zero value:
nil
Functions:
append cap clear close complex copy delete imag len
make max min new panic print println real recover
Экспортируемые идентификаторы
Идентификатор может быть экспортирован, чтобы разрешить доступ к нему из другого пакета. Идентификатор экспортируется, если:
- первый символ имени идентификатора является заглавной буквой Юникода (категория символов Юникода
Lu); и
- идентификатор объявлен в блоке пакета или является именем поля или метода.
Все остальные идентификаторы не экспортируются.
Уникальность идентификаторов
Если задан набор идентификаторов, то идентификатор называется уникальным, если он отличается от всех остальных в этом наборе. Два идентификатора отличаются друг от друга, если они пишутся по-разному или если они находятся в разных пакетах и не экспортируются. В противном случае они одинаковы.
Объявление констант
Объявление констант связывает список идентификаторов (имена констант) со значениями списка константных выражений. Количество идентификаторов должно быть равно количеству выражений, и n-й идентификатор слева привязывается к значению n-го выражения справа.
ConstDecl = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
IdentifierList = identifier { "," identifier } .
ExpressionList = Expression { "," Expression } .
Если тип присутствует, все константы принимают указанный тип, а выражения должны быть присваиваемыми этому типу, который не должен быть параметром типа.
Если тип опущен, константы принимают индивидуальные типы соответствующих выражений.
Если значения выражений являются нетипизированными константами, объявленные константы остаются нетипизированными, а идентификаторы констант обозначают значения констант.
Например, если выражение представляет собой литерал с плавающей точкой, идентификатор константы обозначает константу с плавающей точкой, даже если дробная часть литерала равна нулю.
const Pi float64 = 3.14159265358979323846
const zero = 0.0 // untyped floating-point constant
const (
size int64 = 1024
eof = -1 // untyped integer constant
)
const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", untyped integer and string constants
const u, v float32 = 0, 3 // u = 0.0, v = 3.0
Внутри списка объявлений const с круглыми скобками список выражений может быть опущен в любом ConstSpec, кроме первого. Такой пустой список эквивалентен текстовой подстановке первого предшествующего непустого списка выражений и его типа, если таковой имеется. Поэтому пропуск списка выражений эквивалентен повторению предыдущего списка. Количество идентификаторов должно быть равно количеству выражений в предыдущем списке. Вместе с генератором констант iota этот механизм позволяет легко объявлять последовательные значения:
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Partyday
numberOfDays // this constant is not exported
)
Iota
В объявлении константы заранее объявленный идентификатор iota представляет последовательные нетипизированные целочисленные константы. Его значение - это индекс соответствующей константы в объявлении константы, начиная с нуля. Его можно использовать для построения набора связанных констант:
const (
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)
const (
a = 1 << iota // a == 1 (iota == 0)
b = 1 << iota // b == 2 (iota == 1)
c = 3 // c == 3 (iota == 2, unused)
d = 1 << iota // d == 8 (iota == 3)
)
const (
u = iota * 42 // u == 0 (untyped integer constant)
v float64 = iota * 42 // v == 42.0 (float64 constant)
w = iota * 42 // w == 84 (untyped integer constant)
)
const x = iota // x == 0
const y = iota // y == 0
По определению, несколько применений iota в одном и том же ConstSpec имеют одно и то же значение:
const (
bit0, mask0 = 1 << iota, 1<<iota - 1 // bit0 == 1, mask0 == 0 (iota == 0)
bit1, mask1 // bit1 == 2, mask1 == 1 (iota == 1)
_, _ // (iota == 2, unused)
bit3, mask3 // bit3 == 8, mask3 == 7 (iota == 3)
)
В последнем примере используется неявное повторение последнего непустого списка выражений.
Декларации типов
Декларация типа связывает идентификатор, имени типа, с типом. Декларации типов бывают двух видов: декларации псевдонимов и определения типов.
TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec = AliasDecl | TypeDef .
Объявление псевдонимов
Объявление псевдонимов связывает идентификатор с заданным типом [Go 1.9].
AliasDecl = identifier [ TypeParameters ] "=" Type .
В пределах области действия идентификатора он служит псевдонимом для данного типа.
type (
nodeList = []*Node // nodeList и []*Node являются идентичными типами
Polar = polar // Polar и polar обозначают идентичные типы
)
Если в объявлении псевдонима указаны параметры типа [Go 1.24], то имя типа обозначает общий псевдоним. Общие псевдонимы должны быть инстанцированы при их использовании.
type set[P comparable] = map[P]bool
В объявлении псевдонима заданный тип не может быть параметром типа.
type A[P any] = P // ошибка: P является параметром типа
Определения типов
Определение типа создает новый, отдельный тип с теми же базовыми типами и операциями, что и данный тип, и привязывает к нему идентификатор, имя типа.
TypeDef = identifier [ TypeParameters ] Type .
Новый тип называется определенным типом. Он отличается от любого другого типа, включая тип, на основе которого он создан.
type (
Point struct{ x, y float64 } // Point and struct{ x, y float64 } are different types
polar Point // polar and Point denote different types
)
type TreeNode struct {
left, right *TreeNode
value any
}
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}
Определенный тип может иметь методы, связанные с ним. Он не наследует никаких методов, связанных с данным типом, но набор методов интерфейсного типа или элементов составного типа остается неизменным:
// Mutex - это тип данных с двумя методами, Lock и Unlock.
type Mutex struct { /* Поля Mutex */ }
func (m *Mutex) Lock() { /* Реализация Lock */ }
func (m *Mutex) Unlock() { /* Реализация Unlock */ }
// NewMutex имеет тот же состав, что и Mutex, но его набор методов пуст.
type NewMutex Mutex
// Набор методов PtrMutex, лежащий в основе типа *Mutex, остается неизменным,
// но набор методов PtrMutex пуст.
type PtrMutex *Mutex
// Набор методов *PrintableMutex содержит методы
// Lock и Unlock, привязанные к его встроенному полю Mutex.
type PrintableMutex struct {
Mutex
}
// MyBlock - интерфейсный тип, имеющий тот же набор методов, что и Block.
type MyBlock Block
Определения типов могут использоваться для определения различных булевых, числовых или строковых типов и связывания с ними методов:
type TimeZone int
const (
EST TimeZone = -(5 + iota)
CST
MST
PST
)
func (tz TimeZone) String() string {
return fmt.Sprintf("GMT%+dh", tz)
}
Если в определении типа указаны параметры типа, то имя типа обозначает общий тип. Общие типы должны быть инстанцированы при их использовании.
type List[T any] struct {
next *List[T]
value T
}
В определении типа заданный тип не может быть параметром типа.
type T[P any] P // ошибка: P - параметр типа
func f[T any]() {
type L T // ошибка: T - параметр типа, объявленный в объемлющей функции
}
Родовой тип может также иметь методы, связанные с ним. В этом случае приемники методов должны объявить то же количество параметров типа, что и в определении родового типа.
// Метод Len возвращает количество элементов в связанном списке l.
func (l *List[T]) Len() int { ... }
Объявление параметров типа
Список параметров типа объявляет параметры типа общей функции или объявления типа. Список параметров типа выглядит как обычный список параметров функции, за исключением того, что имена параметров типа должны присутствовать все, и список заключен в квадратные скобки, а не в круглые [Go 1.18].
TypeParameters = "[" TypeParamList [ "," ] "]" .
TypeParamList = TypeParamDecl { "," TypeParamDecl } .
TypeParamDecl = IdentifierList TypeConstraint .
Все непустые имена в списке должны быть уникальными. Каждое имя объявляет параметр типа, который представляет собой новый и отличный от других именованный тип, выступающий в качестве заполнителя для (пока) неизвестного типа в объявлении. Параметр типа заменяется аргументом типа при инстанцировании родовой функции или типа.
[P any]
[S interface{ ~[]byte|string }]
[S ~[]E, E any]
[P Constraint[int]]
[_ any]
Подобно тому, как каждый параметр обычной функции имеет тип параметра, каждый параметр типа имеет соответствующий (мета-)тип, который называется ограничением типа.
Неоднозначность при разборе возникает, когда в списке параметров типа для общего типа объявляется единственный параметр типа P с ограничением C таким образом, что текст P C образует допустимое выражение:
type T[P *C] …
type T[P (C)] …
type T[P *C|Q] …
…
В этих редких случаях список параметров типа неотличим от выражения, и объявление типа разбирается как объявление типа массива. Чтобы устранить двусмысленность, включите ограничение в интерфейс или используйте запятую в конце:
type T[P interface{*C}] …
type T[P *C,] …
Параметры типа также могут быть объявлены спецификацией приемника объявления метода, связанного с родовым типом.
В списке параметров типа родового типа T ограничение типа не может (прямо или косвенно через список параметров типа другого родового типа) ссылаться на T.
type T1[P T1[P]] ... // illegal: T1 ссылается на себя
type T2[P interface{ T2[int] }] ... // illegal: T2 ссылается на себя
type T3[P interface{ m(T3[int])}] ... // illegal: T3 ссылается на себя
type T4[P T5[P]] ... // illegal: T4 ссылается на T5 и
type T5[P T4[P]] ... // T5 ссылается на T4
type T6[P int] struct{ f *T6[P] } // нормально: ссылка на T6 отсутствует в списке параметров типа
Ограничения типа
Ограничение типа - это интерфейс, определяющий набор допустимых аргументов типа для соответствующего параметра типа и контролирующий операции, поддерживаемые значениями этого параметра типа [Go 1.18].
TypeConstraint = TypeElem .
Если ограничение представляет собой интерфейсный литерал вида interface{E}, где E - элемент встроенного типа (не метод), то в списке параметров типа вложенный интерфейс{ ...} может быть опущен для удобства:
[T []P] // = [T interface{[]P}]
[T ~int] // = [T interface{~int}]
[T int|string] // = [T interface{int|string}]
type Constraint ~int // illegal: ~int is not in a type parameter list
Объявленный тип интерфейса comparable обозначает множество всех неинтерфейсных типов, которые строго сравнимы [Go 1.18].
Даже если интерфейсы, не являющиеся параметрами типов, сравнимы, они не являются строго сравнимыми и поэтому не реализуют comparable. Однако они удовлетворяют требованиям comparable.
int // реализует сравнимость (int строго сравнимо)
[]byte // не реализует сравнимость (кусочки не могут быть сравнимы)
interface{} // не реализует сравнимое (см. выше)
interface{ ~int | ~string } // только параметр типа: реализует сравнимость (типы int, string строго сравнимы)
interface{ comparable } // только параметр типа: реализует comparable (comparable реализует сам себя)
interface{ ~int | ~[]byte } // только параметр типа: не реализует сравнимое (срезы не сравнимы)
interface{ ~struct{ any } } // только параметр типа: не реализует сравнимое (поле any не является строго сравнимым)
Сопоставимый интерфейс и интерфейсы, которые (прямо или косвенно) встраивают сопоставимый, могут использоваться только в качестве ограничений типа. Они не могут быть типами значений или переменных, или компонентами других, неинтерфейсных типов.
Удовлетворение ограничения типа
Аргумент типа T удовлетворяет ограничению типа C, если T является элементом множества типов, определяемого C; другими словами, если T реализует C. В качестве исключения, строго сравнимое ограничение типа может быть также удовлетворено сравнимым (не обязательно строго сравнимым) аргументом типа [Go 1.20]. Точнее:
Тип T удовлетворяет ограничению C, если
T реализует C; или
C может быть записан в виде interface{ comparable; E }, где E - базовый интерфейс, а T сопоставим и реализует E.
| тип аргумента |
тип ограничения |
удовлетворение ограничений |
int |
interface{ ~int } |
удовлетворено: int реализует интерфейс{ ~int } |
string |
comparable |
удовлетворено: string реализует comparable (string строго сравнима) |
[]byte |
comparable |
не удовлетворено: срезы не сравнимы |
any |
interface{ comparable; int } |
не удовлетворено: any не реализует интерфейс{ int } |
any |
comparable |
удовлетворено: any сравнимо и реализует базовый интерфейс any |
struct{f any} |
comparable |
удовлетворено: struct{f any} сравнимо и реализует базовый интерфейс any |
any |
interface{ comparable; m() } |
не удовлетворено: any не реализует базовый интерфейс interface{ m() } |
interface{ m() } |
interface{ comparable; m() } |
удовлетворено: интерфейс{ m() } сопоставим и реализует базовый интерфейс интерфейс{ m() } |
Из-за исключения в правиле удовлетворения ограничений сравнение операндов типа parameter может вызвать панику во время выполнения (несмотря на то, что параметры сравнимого типа всегда строго сравнимы).
Объявление переменных
Объявление переменной создает одну или несколько переменных, привязывает к ним соответствующие идентификаторы и присваивает каждой из них тип и начальное значение.
VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
var i int
var U, V, W float64
var k = 0
var x, y float32 = -1, -2
var (
i int
u, v, s = 2.0, 3.0, "bar"
)
var re, im = complexSqrt(-1)
var _, found = entries[name] // map lookup; only interested in "found"
Если задан список выражений, переменные инициализируются этими выражениями в соответствии с правилами для операторов присваивания. В противном случае каждая переменная инициализируется нулевым значением.
Если задан тип, то каждой переменной присваивается этот тип. В противном случае каждой переменной присваивается тип соответствующего инициализирующего значения в присваивании. Если это значение является нетипизированной константой, то оно сначала неявно преобразуется к типу по умолчанию; если это нетипизированное булево значение, то оно сначала неявно преобразуется к типу bool. Объявленный идентификатор nil не может использоваться для инициализации переменной без явного типа.
var d = math.Sin(0.5) // d is float64
var i = 42 // i is int
var t, ok = x.(T) // t is T, ok is bool
var n = nil // illegal
Ограничение реализации: Компилятор может сделать незаконным объявление переменной внутри тела функции, если переменная никогда не будет использоваться.
Объявление коротких переменных
Объявление короткой переменной использует синтаксис:
ShortVarDecl = IdentifierList ":=" ExpressionList .
Это сокращение для обычного объявления переменной с выражениями-инициализаторами, но без типов:
"var" IdentifierList "=" ExpressionList .
i, j := 0, 10
f := func() int { return 7 }
ch := make(chan int)
r, w, _ := os.Pipe() // os. Pipe() возвращает соединенную пару Files и ошибку, если таковая имеется
_, y, _ := coord(p) // coord() возвращает три значения; интересует только координата y
В отличие от обычных объявлений переменных, короткое объявление переменных может повторно объявлять переменные, если они были первоначально объявлены ранее в том же блоке (или в списках параметров, если блок является телом функции) с тем же типом, и хотя бы одна из непустых переменных является новой. Как следствие, повторное объявление может появиться только в коротком объявлении с несколькими переменными. Повторное объявление не вводит новую переменную; оно просто присваивает новое значение исходной. Непустые имена переменных в левой части := должны быть уникальными.
field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset) // повторно объявляет смещение
x, y, x := 1, 2, 3 // незаконно: x повторяется в левой части :=
Короткие объявления переменных могут появляться только внутри функций. В некоторых контекстах, например в инициализаторах операторов “if”, “for” или “switch”, они могут использоваться для объявления локальных временных переменных.
Объявление функций
Объявление функции связывает идентификатор, имя функции, с функцией.
FunctionDecl = "func" FunctionName [ TypeParameters ] Signature [ FunctionBody ] .
FunctionName = identifier .
FunctionBody = Block .
Если в сигнатуре функции объявлены параметры результата, то список утверждений тела функции должен заканчиваться завершающим утверждением.
func IndexRune(s string, r rune) int {
for i, c := range s {
if c == r {
return i
}
}
// недопустимо: отсутствует оператор return
}
Если в объявлении функции указаны параметры типа, то имя функции обозначает родовую функцию. Родовая функция должна быть инстанцирована, прежде чем ее можно будет вызвать или использовать в качестве значения.
func min[T ~int|~float64](x, y T) T {
if x < y {
return x
}
return y
}
Объявление функции без параметров типа может не содержать тела. Такое объявление представляет собой сигнатуру для функции, реализованной вне Go, например, в ассемблере.
func flushICache(begin, end uintptr) // реализовано извне
Объявление методов
Метод - это функция с приемником. Объявление метода привязывает к методу идентификатор, имя метода, и связывает метод с базовым типом приемника.
MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver = Parameters .
Приемник указывается в дополнительной секции параметров, предшествующей имени метода. В этой секции параметров должен быть объявлен единственный невариативный параметр - приемник. Его тип должен быть определенным типом T или указателем на определенный тип T, за которым, возможно, следует список имен параметров типа [P1, P2, ...], заключенных в квадратные скобки. T называется базовым типом приемника. Базовый тип-приемник не может быть указателем или интерфейсным типом и должен быть определен в том же пакете, что и метод. Считается, что метод привязан к базовому типу-приемнику, и имя метода видно только в селекторах для типа T или *T.
Непустой идентификатор приемника должен быть уникальным в сигнатуре метода. Если на значение приемника не ссылаются в теле метода, его идентификатор можно не указывать в объявлении. То же самое относится и к параметрам функций и методов.
Для базового типа непустые имена методов, связанных с ним, должны быть уникальными. Если базовым типом является тип struct, то непустые имена методов и полей должны быть разными.
Заданный определенный тип Point
func (p *Point) Length() float64 {
return math.Sqrt(p.x * p.x + p.y * p.y)
}
func (p *Point) Scale(factor float64) {
p.x *= factor
p.y *= factor
}
привязать методы Length и Scale, с типом приемника *Point, к базовому типу Point.
Если базовый тип приемника является общим типом, спецификация приемника должна объявить соответствующие параметры типа для используемого метода. Это делает параметры типа-приемника доступными для метода.
Синтаксически объявление параметров типа выглядит как инстанцирование базового типа-приемника: аргументы типа должны быть идентификаторами, обозначающими объявляемые параметры типа, по одному на каждый параметр типа базового типа-приемника. Имена параметров типа не обязательно должны совпадать с соответствующими именами параметров в определении базового типа-приемника, а все непустые имена параметров должны быть уникальными в секции параметров приемника и в сигнатуре метода.
Ограничения параметров типа-приемника подразумеваются определением базового типа-приемника: соответствующие параметры типа имеют соответствующие ограничения.
type Pair[A, B any] struct {
a A
b B
}
func (p Pair[A, B]) Swap() Pair[B, A] { ... } // приемник объявляет A, B
func (p Pair[First, _]) First() First { ... } // получатель объявляет First, соответствует A в Pair
Если тип-приемник обозначается псевдонимом (указателем на него), то этот псевдоним не должен быть общим и не должен обозначать инстанцированный общий тип, ни прямо, ни косвенно через другой псевдоним, и независимо от перенаправления указателей.
type GPoint[P any] = Point
type HPoint = *GPoint[int]
type IPair = Pair[int, int]
func (*GPoint[P]) Draw(P) { ... } // illegal: alias не должен быть общим
func (HPPoint) Draw(P) { ... } // illegal: alias не должен обозначать инстанцированный тип GPoint[int]
func (*IPair) Second() int { ... } // illegal: псевдоним не должен обозначать инстанцированный тип Pair[int, int]
Выражения
Выражение задает вычисление значения путем применения операторов и функций к операндам.
Операнды
Операнды обозначают элементарные значения в выражении. Операнд может быть литералом, (возможно, квалифицированным) непустым идентификатором, обозначающим константу, переменную или функцию, или выражением в круглых скобках.
Operand = Literal | OperandName [ TypeArgs ] | "(" Expression ")" .
Literal = BasicLit | CompositeLit | FunctionLit .
BasicLit = int_lit | float_lit | imaginary_lit | rune_lit | string_lit .
OperandName = identifier | QualifiedIdent .
За именем операнда, обозначающего общую функцию, может следовать список аргументов типа; результирующий операнд является инстанцированной функцией.
Пустой идентификатор может появляться в качестве операнда только в левой части оператора присваивания.
Ограничение на реализацию: Компилятор не должен сообщать об ошибке, если тип операнда является параметром типа с пустым набором типов. Функции с такими параметрами типа не могут быть инстанцированы; любая попытка приведет к ошибке в месте инстанцирования.
Квалифицированные идентификаторы
Квалифицированный идентификатор - это идентификатор, снабженный префиксом имени пакета. И имя пакета, и идентификатор не должны быть пустыми.
QualifiedIdent = PackageName "." identifier .
Квалифицированный идентификатор обращается к идентификатору в другом пакете, который должен быть импортирован. Идентификатор должен быть экспортирован и объявлен в блоке пакета этого пакета.
math.Sin // обозначает функцию Sin в пакете math
Составные литералы
Составные литералы создают новые составные значения каждый раз, когда их объявляют. Они состоят из типа литерала, за которым следует ограниченный скобками список элементов. Каждому элементу может предшествовать соответствующий ключ.
CompositeLit = LiteralType LiteralValue .
LiteralType = StructType | ArrayType | "[" "..." "]" ElementType |
SliceType | MapType | TypeName [ TypeArgs ] .
LiteralValue = "{" [ ElementList [ "," ] ] "}" .
ElementList = KeyedElement { "," KeyedElement } .
KeyedElement = [ Key ":" ] Element .
Key = FieldName | Expression | LiteralValue .
FieldName = identifier .
Element = Expression | LiteralValue .
Основной тип LiteralType T должен быть типом struct, array, slice или map (в синтаксисе это ограничение соблюдается, за исключением случаев, когда тип задан как TypeName). Типы элементов и ключей должны быть присваиваемыми соответствующим типам поля, элемента и ключа типа T; дополнительное преобразование не требуется.
Ключ интерпретируется как имя поля для литералов struct, индекс для литералов array и slice и ключ для литералов map. Для литералов map все элементы должны иметь ключ. Ошибкой является указание нескольких элементов с одним и тем же именем поля или постоянным значением ключа. О непостоянных ключах map см. раздел о порядке объявлений.
Для литералов struct действуют следующие правила:
- Ключ должен быть именем поля, объявленного в типе
struct.
- Список элементов, не содержащий ключей, должен содержать элемент для каждого поля
struct в том порядке, в котором эти поля объявлены.
- Если любой элемент имеет ключ, то каждый элемент должен иметь ключ.
- Список элементов, содержащий ключи, не обязательно должен содержать элемент для каждого поля
struct. Опущенные поля получают нулевое значение для этого поля.
- Литерал может не содержать список элементов; такой литерал оценивается в нулевое значение для своего типа.
- Ошибкой является указание элемента для неэкспортируемого поля структуры, принадлежащей другому пакету.
Учитывая сказанное
type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }
и теперь можно написать
origin := Point3D{} // zero value for Point3D
line := Line{origin, Point3D{y: -4, z: 12.3}} // zero value for line.q.x
Для литералов массивов и фрагментов действуют следующие правила:
- Каждый элемент имеет связанный с ним целочисленный индекс, отмечающий его положение в массиве.
- Элемент с ключом использует ключ в качестве своего индекса. Ключ должен быть неотрицательной константой, представляемой значением типа
int; а если он типизирован, то должен быть целочисленного типа.
- Элемент без ключа использует индекс предыдущего элемента плюс один. Если первый элемент не имеет ключа, его индекс равен нулю.
При получении адреса составного литерала генерируется указатель на уникальную переменную, инициализированную значением литерала.
var pointer *Point3D = &Point3D{y: 1000}
Обратите внимание, что нулевое значение для типа slice или map - это не то же самое, что инициализированное, но пустое значение того же типа. Следовательно, получение адреса пустого составного литерала slice или map не имеет того же эффекта, что и выделение нового значения slice или map с помощью new.
p1 := &[]int{} // p1 указывает на инициализированный, пустой фрагмент со значением []int{} и длиной 0
p2 := new([]int) // p2 указывает на неинициализированный фрагмент со значением nil и длиной 0
Длина литерала массива равна длине, указанной в типе литерала. Если в литерале указано меньше элементов, чем длина, то недостающие элементы устанавливаются в нулевое значение для типа элемента массива. Предоставление элементов со значениями индексов вне диапазона индексов массива является ошибкой. Обозначение ... задает длину массива, равную максимальному индексу элемента плюс один.
buffer := [10]string{} // len(buffer) == 10
intSet := [6]int{1, 2, 3, 5} // len(intSet) == 6
days := [...]string{"Sat", "Sun"} // len(days) == 2
Литерал slice описывает весь лежащий в его основе литерал массива. Таким образом, длина и емкость литерала slice равны максимальному индексу элемента плюс единица.
Литерал среза имеет вид
и является сокращением для операции slice, применяемой к массиву:
tmp := [n]T{x1, x2, ... xn}
tmp[0 : n]
Внутри составного литерала массива, среза или карты типа T элементы или ключи карты, которые сами являются составными литералами, могут элиминировать соответствующий тип литерала, если он идентичен типу элемента или ключа T. Аналогично, элементы или ключи, которые являются адресами составных литералов, могут элиминировать &T, если тип элемента или ключа *T.
[...]Point{{1.5, -3.5}, {0, 0}} // аналогично [...]Point{Point{1.5, -3.5}, Point{0, 0}}
[][]int{{1, 2, 3}, {4, 5}} // аналогично [][]int{[]int{1, 2, 3}, []int{4, 5}}
[][]Point{{{0, 1}, {1, 2}}} // аналогично map[string]Point{"orig": Point{0, 0}}
map[Point]string{{0, 0}: "orig"} // аналогично map[Point]string{Point{0, 0}: "orig"}
type PPoint *Point
[2]*Point{{1.5, -3.5}, {}} // аналогично [2]*Point{&Point{1.5, -3.5}, &Point{}}
[2]PPoint{{1.5, -3.5}, {}} // аналогично [2]PPoint{PPoint(&Point{1.5, -3.5}), PPoint(&Point{})}
Неоднозначность разбора возникает, когда составной литерал, использующий форму TypeName типа LiteralType, появляется в качестве операнда между ключевым словом и открывающей скобкой блока оператора “if”, “for” или “switch”, а составной литерал не заключен в круглые скобки, квадратные скобки или фигурные скобки. В этом редком случае открывающая скобка литерала ошибочно воспринимается как скобка, вводящая блок операторов. Чтобы устранить двусмысленность, составной литерал должен быть заключен в круглые скобки.
if x == (T{a,b,c}[i]) { … }
if (x == T{a,b,c}[i]) { … }
Примеры допустимых литералов массивов, фрагментов и карт:
// список простых чисел
primes := []int{2, 3, 5, 7, 9, 2147483647}
// vowels[ch] - true, если ch - гласная
vowels := [128]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}
// массив [10]float32{-1, 0, 0, 0, -0.1, -0.1, 0, 0, 0, -1}
filter := [10]float32{-1, 4: -0.1, -0.1, 9: -1}
// частоты в Гц для равнотемперированной шкалы (A4 = 440 Гц)
noteFrequency := map[string]float32{
"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
"G0": 24.50, "A0": 27.50, "B0": 30.87,
}
Литералы функций
Литерал функции представляет собой анонимную функцию. Функциональные литералы не могут объявлять параметры типа.
FunctionLit = "func" Signature FunctionBody .
func(a, b int, z float64) bool { return a*b < int(z) }
Литерал функции может быть присвоен переменной или вызван напрямую.
f := func(x, y int) int { return x + y }
func(ch chan int) { ch <- ACK }(replyChan)
Функциональные литералы являются закрывающими: они могут ссылаться на переменные, определенные в окружающей функции. Эти переменные затем разделяются между окружающей функцией и литералом функции, и они сохраняются до тех пор, пока доступны.
Первичные выражения
Первичные выражения - это операнды для унарных и бинарных выражений.
PrimaryExpr = Operand |
Conversion |
MethodExpr |
PrimaryExpr Selector |
PrimaryExpr Index |
PrimaryExpr Slice |
PrimaryExpr TypeAssertion |
PrimaryExpr Arguments .
Selector = "." identifier .
Index = "[" Expression [ "," ] "]" .
Slice = "[" [ Expression ] ":" [ Expression ] "]" |
"[" [ Expression ] ":" Expression ":" Expression "]" .
TypeAssertion = "." "(" Type ")" .
Arguments = "(" [ ( ExpressionList | Type [ "," ExpressionList ] ) [ "..." ] [ "," ] ] ")" .
x
2
(s + ".txt")
f(3.1415, true)
Point{1, 2}
m["foo"]
s[i : j + 1]
obj.color
f.p[i].x()
Selectors (Селекторы)
Для первичного выражения x, не являющегося именем пакета, селекторное выражение
обозначает поле или метод f значения x (или иногда *x; см. ниже). Идентификатор f называется селектором (поля или метода); он не должен быть пустым идентификатором. Тип выражения селектора равен типу f. Если x - это имя пакета, см. раздел о квалифицированных идентификаторах.
Селектор f может обозначать поле или метод f типа T, либо ссылаться на поле или метод f вложенного в него вложенного поля T. Количество вложенных полей, пройденных для достижения f, называется его глубиной в T. Глубина поля или метода f, объявленного в T, равна нулю. Глубина поля или метода f, объявленного во вложенном поле A в T, равна глубине f в A плюс один.
К селекторам применяются следующие правила:
- Для значения
x типа T или *T, где T не является указателем или интерфейсным типом, x.f обозначает поле или метод на самой малой глубине в T, где есть такой f. Если не существует ровно одного f с самой малой глубиной, то выражение селектора является незаконным.
- Для значения
x типа I, где I - интерфейсный тип, x.f обозначает фактический метод с именем f динамического значения x. Если в наборе методов I нет метода с именем f, то селекторное выражение незаконно.
- В качестве исключения, если тип
x является определенным типом указателя и (*x).f является допустимым селекторным выражением, обозначающим поле (но не метод), x.f является сокращением для (*x).f.
- Во всех остальных случаях
x.f является незаконным.
- Если
x имеет тип указателя и значение nil, а x.f обозначает поле struct, присваивание или вычисление x.f вызовет панику во время выполнения.
- Если
x имеет тип интерфейса и значение nil, вызов или оценка метода x.f приводит к панике во время выполнения.
Например, учитывая сказанное:
type T0 struct {
x int
}
func (*T0) M0()
type T1 struct {
y int
}
func (T1) M1()
type T2 struct {
z int
T1
*T0
}
func (*T2) M2()
type Q *T2
var t T2 // with t.T0 != nil
var p *T2 // with p != nil and (*p).T0 != nil
var q Q = p
можно написать:
t.z // t.z
t.y // t.T1.y
t.x // (*t.T0).x
p.z // (*p).z
p.y // (*p).T1.y
p.x // (*(*p).T0).x
q.x // (*(*q).T0).x (*q).x является допустимым селектором поля
p.M0() // ((*p).T0).M0() M0 ожидает приемник *T0
p.M1() // ((*p).T1).M1() M1 ожидает приемник T1
p.M2() // p.M2() M2 ожидает приемник *T2
t.M2() // (&t).M2() M2 ожидает *T2-приемник, см. раздел Calls (Вызовы)
но следующее недействительно:
q.M0() // (*q).M0 действителен, но не является селектором поля
Выражения в методах
Если M находится в наборе методов типа T, то T.M - это функция, которая может быть вызвана как обычная функция с теми же аргументами, что и M, с префиксом в виде дополнительного аргумента, который является приемником метода.
MethodExpr = ReceiverType "." MethodName .
ReceiverType = Type .
Рассмотрим структуру типа T с двумя методами, Mv, приемник который имеет тип T, и Mp, приемник который имеет тип *T.
type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 } // приемник значений
func (tp *T) Mp(f float32) float32 { return 1 } // приемник указателей
var t T
Выражение
дает функцию, эквивалентную Mv, но с явным приемником в качестве первого аргумента; она имеет сигнатуру
Эта функция может быть вызвана обычным образом с явным приемником, так что эти пять вызовов эквивалентны:
t.Mv(7)
T.Mv(t, 7)
(T).Mv(t, 7)
f1 := T.Mv; f1(t, 7)
f2 := (T).Mv; f2(t, 7)
Аналогично, выражение
дает функцию, представляющую значение Mp с сигнатурой
func(tp *T, f float32) float32
Для метода с приемником значений можно вывести функцию с явным приемником указателей, так что
дает функцию, представляющую значение Mv с сигнатурой
Такая функция косвенно через приемник создает значение для передачи в качестве приемника базовому методу; при этом метод не перезаписывает значение, адрес которого передается в вызове функции.
Последний случай, функция-приемник значения для метода-приемника указателя, является незаконным, поскольку методы-приемники указателей не входят в набор методов типа значения.
Функции-значения, получаемые из методов, вызываются с помощью синтаксиса вызова функции; приемник предоставляется в качестве первого аргумента вызова. То есть, если дано f := T.Mv, то f вызывается как f(t, 7), а не t.f(7). Чтобы сконструировать функцию, связывающую приемник, используйте литерал функции или значение метода.
Законно выводить значение функции из метода интерфейсного типа. Результирующая функция принимает явный приемник этого интерфейсного типа.
Значения метода
Если выражение x имеет статический тип T, а M находится в наборе методов типа T, то x.M называется значением метода. Значение метода x.M - это значение функции, которое можно вызвать с теми же аргументами, что и вызов метода x.M. Выражение x рассчитывается и сохраняется во время вычислений значения метода; сохраненная копия затем используется в качестве приемника в любых вызовах, которые могут быть выполнены позже.
type S struct { *T }
type T int
func (t T) M() { print(t) }
t := new(T)
s := S{T: t}
f := t.M // приемник *t рассчитывается и хранится в f
g := s.M // приемник *(s.T) рассчитывается и хранится в g
*t = 42 // не влияет на сохраненные приемники в f и g
Тип T может быть интерфейсным или неинтерфейсным типом.
Как и при обсуждении выражений методов выше, рассмотрим тип ёstruct Tё с двумя методами, Mv, приемник которого имеет тип T, и Mp, приемник которого имеет тип *T.
type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 } // value receiver
func (tp *T) Mp(f float32) float32 { return 1 } // pointer receiver
var t T
var pt *T
func makeT() T
Выражение
дает значение функции типа
Эти два вызова эквивалентны:
Аналогично, выражение
дает значение функции типа
Как и в случае с селекторами, ссылка на неинтерфейсный метод с приемником значения, использующим указатель, автоматически разыменовывает этот указатель: pt.Mv эквивалентно (*pt).Mv.
Как и в случае с вызовами методов, ссылка на неинтерфейсный метод с приемником указателя, использующим адресуемое значение, автоматически принимает адрес этого значения: t.Mp эквивалентно (&t).Mp.
f := t.Mv; f(7) // like t.Mv(7)
f := pt.Mp; f(7) // like pt.Mp(7)
f := pt.Mv; f(7) // like (*pt).Mv(7)
f := t.Mp; f(7) // like (&t).Mp(7)
f := makeT().Mp // invalid: result of makeT() is not addressable
Хотя в приведенных выше примерах используются неинтерфейсные типы, создание значения метода из значения интерфейсного типа также законно.
var i interface { M(int) } = myVal
f := i.M; f(7) // like i.M(7)
Индексные выражения
Первичное выражение вида
обозначает элемент массива, указатель на массив, фрагмент, строку или карту a, проиндексированный по x. Значение x называется индексом или ключом карты соответственно. При этом действуют следующие правила:
Если a не является ни картой, ни параметром типа:
- индекс
x должен быть нетипизированной константой или его основной тип должен быть целым числом
- константный индекс должен быть неотрицательным и представляемым значением типа
int
- нетипизированный константный индекс имеет тип
int
- индекс
x находится в диапазоне, если 0 <= x < len(a), иначе он вне диапазона
Для a типа массива A:
- постоянный индекс должен быть в диапазоне
- если
x выходит за пределы диапазона во время выполнения, возникает паника во время выполнения
a[x] - это элемент массива с индексом x, а тип a[x] - это тип элемента A
Для a типа указателя на массив:
a[x] - сокращение от (*a)[x]
Для a типа slice S:
- если
x выходит за пределы диапазона во время выполнения, возникает паника во время выполнения
a[x] - это элемент среза по индексу x, а тип a[x] - это тип элемента S
Для a строкового типа:
- константный индекс должен быть в диапазоне, если строка
a также константна
- если
x выходит за пределы диапазона во время выполнения, происходит паника во время выполнения
a[x] - это непостоянное байтовое значение по индексу x и тип a[x] - байт
a[x] не может быть присвоен
Для a типа map M:
- Тип
x должен быть назначен типу ключа M
- если
map содержит запись с ключом x, a[x] является элементом map с ключом x и тип a[x] является типом элемента M
- если
map равен nil или не содержит такой записи, a[x] является нулевым значением для типа элемента M
Для a типа параметра типа P:
- Индексное выражение
a[x] должно быть допустимым для значений всех типов в наборе типов P.
- Типы элементов всех типов в наборе типов
P должны быть одинаковыми. В данном контексте типом элемента строкового типа является байт.
- Если в наборе типов
P есть тип map, то все типы в этом наборе типов должны быть типами map, а соответствующие типы ключей должны быть идентичными.
a[x] - это элемент массива, среза или строки по индексу x или элемент map с ключом x аргумента типа, с которым инстанцируется P, а тип a[x] - это тип (идентичных) типов элементов.
a[x] не может быть присвоен, если набор типов P включает типы string.
В противном случае a[x] является незаконным.
Индексное выражение на карте a типа map[K]V, используемое в операторе присваивания или инициализации специальной формы
v, ok = a[x]
v, ok := a[x]
var v, ok = a[x]
дает дополнительное нетипизированное булево значение. Значение ok равно true, если ключ x присутствует в карте, и false в противном случае.
Присвоение элементу карты nil приводит к панике во время выполнения.
Выражения срезов (частей массивов)
Выражения-срезов строят подстроку или фрагмент из строки, массива, указателя на массив или фрагмента. Существует два варианта: простая форма, в которой указываются нижняя и верхняя границы, и полная форма, в которой также указывается ограничение на емкость.
Простые выражения срезов
Основное выражение
строит подстроку или фрагмент. Основной тип a должен быть строкой, массивом, указателем на массив, фрагментом или байтовой строкой. Индексы low и high определяют, какие элементы операнда a появятся в результате. Результат имеет индексы, начинающиеся с 0, и длину, равную high - low. После нарезки массива a
a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]
срез s имеет тип []int, длину 3, емкость 4 и элементы
s[0] == 2
s[1] == 3
s[2] == 4
Для удобства любой из индексов может быть опущен. Отсутствующий младший индекс по умолчанию равен нулю; отсутствующий старший индекс по умолчанию равен длине нарезанного операнда:
a[2:] // то же, что a[2 : len(a)]
a[:3] // то же, что a[0 : 3]
a[:] // то же, что a[0 : len(a)]
Если a - указатель на массив, то a[low : high] - это сокращение от (*a)[low : high].
Для массивов или строк индексы находятся в диапазоне, если 0 <= low <= high <= len(a), в противном случае они выходят за пределы диапазона. Для срезов верхней границей индекса является емкость среза cap(a), а не его длина. Постоянный индекс должен быть неотрицательным и представляться значением типа int; для массивов или постоянных строк постоянные индексы также должны находиться в диапазоне. Если оба индекса постоянны, они должны удовлетворять условию low <= high. Если индексы выходят за пределы диапазона во время выполнения, происходит паника во время выполнения.
За исключением нетипизированных строк, если нарезанный операнд является строкой или фрагментом, результатом операции нарезки является неконстантное значение того же типа, что и операнд. Для нетипизированных строковых операндов результатом является неконстантное значение типа string. Если нарезанный операнд является массивом, он должен быть адресуемым, а результатом операции slice является фрагмент с тем же типом элемента, что и массив.
Если нарезанный операнд допустимого выражения slice является nil-фрагментом, то результатом будет nil-фрагмент. В противном случае, если результат является срезом, он разделяет с операндом его базовый массив.
var a [10]int
s1 := a[3:7] // базовый массив s1 - это массив a; &s1[2] == &a[5]
s2 := s1[1:4] // базовый массив s2 - это базовый массив s1, который является массивом a; &s2[1] == &a[5]
s2[1] = 42 // s2[1] == s1[2] == a[5] == 42; все они ссылаются на один и тот же элемент базового массива
var s []int
s3 := s[:0] // s3 == nil
Выражения полного среза
Первичное выражение
строит срез того же типа, с той же длиной и элементами, что и простое выражение a[low : high]. Кроме того, оно контролирует емкость результирующего среза, устанавливая ее в значение max - low. Только первый индекс может быть опущен; по умолчанию он равен 0. Основной тип a должен быть массивом, указателем на массив или срез (но не строкой). После нарезки массива a
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
Срез t имеет тип []int, длину 2, емкость 4 и элементы
Что касается простых выражений нарезки, то если a - указатель на массив, то a[low : high : max] - это сокращение от (*a)[low : high : max]. Если нарезаемый операнд является массивом, он должен быть адресуемым.
Индексы находятся в диапазоне, если 0 <= low <= high <= max <= cap(a), иначе они выходят за пределы диапазона. Постоянный индекс должен быть неотрицательным и представляться значением типа int; для массивов постоянные индексы также должны находиться в диапазоне. Если константными являются несколько индексов, то присутствующие константы должны находиться в диапазоне относительно друг друга. Если индексы выходят за пределы диапазона во время выполнения, возникает паника.
Утверждения типа
Для выражения x интерфейсного типа, но не параметра типа, и типа T, первичное выражение
утверждает, что x не является nil и что значение, хранящееся в x, имеет тип T. Выражение x.(T) называется утверждением типа.
Точнее, если T не является типом интерфейса, x.(T) утверждает, что динамический тип x идентичен типу T. В этом случае T должен реализовывать (интерфейсный) тип x; в противном случае утверждение типа недействительно, поскольку для x невозможно хранить значение типа T. Если T - интерфейсный тип, то x.(T) утверждает, что динамический тип x реализует интерфейс T.
Если утверждение типа верно, то значением выражения будет значение, хранящееся в x, а его типом - T. Если утверждение типа ложно, то произойдет паника во время выполнения. Другими словами, несмотря на то, что динамический тип x известен только во время выполнения, в корректной программе тип x.(T) заведомо равен T.
var x interface{} = 7 // x has dynamic type int and value 7
i := x.(int) // i has type int and value 7
type I interface { m() }
func f(y I) {
s := y.(string) // illegal: string does not implement I (missing method m)
r := y.(io.Reader) // r has type io.Reader and the dynamic type of y must implement both I and io.Reader
…
}
Утверждение типа, используемое в операторе присваивания или инициализации специального вида
v, ok = x.(T)
v, ok := x.(T)
var v, ok = x.(T)
var v, ok interface{} = x.(T) // динамические типы v и ok - T и bool
дает дополнительное нетипизированное булево значение. Значение ok будет истинным, если утверждение верно. В противном случае оно ложно, а значение v является нулевым значением для типа T. Паники во время выполнения в этом случае не возникает.
Calls (Вызовы)
Дано выражение типа функции f с основным типом F
вызывает f с аргументами a1, a2, ... an. За исключением одного особого случая, аргументы должны быть однозначными выражениями, присваиваемыми типам параметров F, и оцениваются до вызова функции. Тип выражения - это тип результата F. Вызов метода аналогичен, но сам метод указывается как селектор на значение типа приемника метода.
math.Atan2(x, y) // вызов функции
var pt *Point
pt.Scale(3.5) // вызов метода с приемником pt
Если f обозначает общую функцию, то перед вызовом или использованием в качестве значения функции она должна быть инстанцирована.
При вызове функции значение функции и аргументы оцениваются в обычном порядке. После их оценки выделяется новое хранилище для переменных функции, в которое входят ее параметры и результаты. Затем аргументы вызова передаются функции, то есть присваиваются соответствующим параметрам функции, и вызываемая функция начинает выполнение. Возвращаемые параметры функции передаются обратно вызывающему, когда функция возвращается.
Вызов функции с нулевым значением вызывает панику во время выполнения.
В качестве особого случая, если возвращаемые значения функции или метода g равны по числу и могут быть индивидуально назначены параметрам другой функции или метода f, то вызов f(g(параметры_от_g)) вызовет f после передачи возвращаемых значений g параметрам f по порядку. Вызов f не должен содержать никаких параметров, кроме вызова g, а g должен иметь хотя бы одно возвращаемое значение. Если у f есть конечный параметр ..., ему присваиваются возвращаемые значения g, оставшиеся после присвоения обычных параметров.
func Split(s string, pos int) (string, string) {
return s[0:pos], s[pos:]
}
func Join(s, t string) string {
return s + t
}
if Join(Split(value, len(value)/2)) != value {
log.Panic("test fails")
}
Вызов метода x.m() допустим, если набор методов (или типов) x содержит m и список аргументов может быть присвоен списку параметров m. Если x является адресуемым и набор методов &x содержит m, то x.m() - это сокращение от (&x).m():
Не существует отдельного типа метода и литералов метода.
Передача аргументов в ... параметры
Если f является переменной с конечным параметром p типа ...T, то внутри f тип p эквивалентен типу []T. Если f вызывается без фактических аргументов для p, то значение, передаваемое p, равно nil. В противном случае передаваемое значение - это новый фрагмент типа []T с новым нижележащим массивом, последовательными элементами которого являются фактические аргументы, которые все должны быть присваиваемыми в T. Длина и емкость фрагмента, таким образом, равна количеству аргументов, связанных с p, и может быть разной для каждого места вызова.
Даны функция и вызовы
func Greeting(prefix string, who ...string)
Greeting("nobody")
Greeting("hello:", "Joe", "Anna", "Eileen")
внутри Greeting, who будет иметь значение nil в первом вызове, и []string{"Joe", "Anna", "Eileen"} во втором
Если последний аргумент может быть отнесен к типу slice []T и за ним следует ..., он передается без изменений как значение для параметра ...T. В этом случае новый срез не создается.
Учитывая срез s и вызов
s := []string{"Джеймс", "Жасмин"}
Greeting("goodbye:", s...)
внутри Greeting, который будет иметь то же значение, что и s, с тем же базовым массивом.
Инстанцирование
Общая функция или тип инстанцируется путем замены аргументов типа на параметры типа [Go 1.18]. Инстанцирование происходит в два этапа:
Пояснение инстанцирования
Подробнее
В языке программирования Go термин “instantiation” (инстанцирование) относится к процессу создания конкретного экземпляра (реализации) обобщённого типа или функции с подставленными конкретными типами или значениями.
Instantiations в Go (на примере дженериков, появившихся в Go 1.18)
В Go инстанцирование чаще всего связано с дженериками (generics).
-
Инстанцирование обобщённого типа
При объявлении обобщённого типа (с параметрами типа [T any]) сам по себе тип является лишь шаблоном. Инстанцирование происходит, когда указывается конкретный тип вместо параметра T.
Пример:
type Stack[T any] struct {
items []T
}
// Инстанцирование Stack для типа int
var intStack Stack[int] // Stack[T] инстанцируется как Stack[int]
-
Инстанцирование обобщённой функции
Аналогично, обобщённые функции требуют подстановки конкретных типов при вызове, что приводит к созданию специализированной версии функции.
Пример:
func PrintSlice[T any](slice []T) {
for _, v := range slice {
fmt.Println(v)
}
}
// Инстанцирование функции для []string
PrintSlice[string]([]string{"hello", "world"})
// Компилятор Go может также вывести тип автоматически:
PrintSlice([]int{1, 2, 3}) // Инстанцируется как PrintSlice[int]
Когда происходит инстанцирование?
- Во время компиляции: Go создаёт конкретные реализации обобщённых типов и функций на этапе компиляции.
- Для каждого уникального типа: Например,
Stack[int] и Stack[string] — это два разных инстанцированных типа.
Важно
- В Go инстанцирование не происходит во время выполнения (в отличие, например, от некоторых реализаций шаблонов в C++).
- Компилятор стремится избегать дублирования кода, где это возможно, используя оптимизации.
Если вы работаете с дженериками в Go, понимание инстанцирования помогает избежать неожиданностей при использовании обобщённых типов и функций.
- Каждый аргумент типа заменяется на соответствующий параметр типа в родовом объявлении. Эта замена происходит во всем объявлении функции или типа, включая сам список параметров типа и любые типы в этом списке.
- После подстановки каждый аргумент типа должен удовлетворять ограничениям (инстанцированным, если необходимо) соответствующего параметра типа. В противном случае инстанцирование не происходит.
Инстанцирование типа приводит к созданию нового негенетического именованного типа; инстанцирование функции приводит к созданию новой негенетической функции.
| список параметров |
типа аргументы |
типа после подстановки |
| [P any] |
int |
int удовлетворяет any |
| [S ~[]E, E any] |
[]int, int |
[]int удовлетворяет ~[]int, int удовлетворяет any |
| [P io.Writer] |
string |
illegal: string не удовлетворяет io.Writer |
| [P comparable] |
any |
any удовлетворяет (но не реализует) comparable |
При использовании родовой функции аргументы типа могут быть указаны явно, либо они могут быть частично или полностью выведены из контекста, в котором используется функция. При условии, что они могут быть выведены, список аргументов типа может быть полностью опущен, если функция:
- вызывается с обычными аргументами,
- присваивается переменной с известным типом
- передается в качестве аргумента другой функции или
- возвращается как результат.
Во всех остальных случаях список аргументов типа (возможно, неполный) должен присутствовать. Если список аргументов типа отсутствует или является неполным, все недостающие аргументы типа должны быть выведены из контекста, в котором используется функция.
// sum возвращает сумму (конкатенацию, для строк) своих аргументов.
func sum[T ~int | ~float64 | ~string](x... T) T { ... }
x := sum // illegal: тип x неизвестен
intSum := sum[int] // intSum имеет тип func(x... int) int
a := intSum(2, 3) // a имеет значение 5 типа int
b := sum[float64](2.0, 3) // b имеет значение 5.0 типа float64
c := sum(b, -1) // c имеет значение 4.0 типа float64
type sumFunc func(x... string) string
var f sumFunc = sum // то же, что var f sumFunc = sum[string]
f = sum // то же, что f = sum[string]
Частичный список аргументов типа не может быть пустым; в нем должен присутствовать как минимум первый аргумент. Список является префиксом полного списка аргументов типа, оставляя остальные аргументы для вывода. Грубо говоря, аргументы типа могут быть опущены “справа налево”.
func apply[S ~[]E, E any](s S, f func(E) E) S { ... }
f0 := apply[] // illegal: список аргументов типа не может быть пустым
f1 := apply[[]int] // аргумент типа для S указан явно, аргумент типа для E выведен
f2 := apply[[]string, string] // оба аргумента типа указаны явно
var bytes []byte
r := apply(bytes, func(byte) byte { ... }) // оба аргумента типа выводятся из аргументов функции
Для общего типа все аргументы типа всегда должны быть предоставлены явно.
Выведение типа
При использовании общей функции могут отсутствовать некоторые или все аргументы типа, если они могут быть выведены из контекста, в котором используется функция, включая ограничения на параметры типа функции. Выведение типа проходит успешно, если он может вывести недостающие аргументы типа, и инстанцирование проходит успешно с аргументами типа, которые были выведены. В противном случае вывод типа не удается, и программа считается недействительной.
При выводе типов используются отношения типов между парами типов: Например, аргумент функции должен быть присваиваемым соответствующему параметру функции; это устанавливает связь между типом аргумента и типом параметра. Если один из этих двух типов содержит параметры типа, то при выводе типов ищутся аргументы типа, которыми можно заменить параметры типа так, чтобы отношение присваиваемости было выполнено. Аналогично, при выводе типов используется тот факт, что аргумент типа должен удовлетворять ограничению соответствующего параметра типа.
Каждая такая пара сопоставленных типов соответствует уравнению типа, содержащему один или несколько параметров типа, от одной или, возможно, нескольких родовых функций. Вывод недостающих аргументов типа означает решение полученного набора уравнений типа для соответствующих параметров типа.
Например, учитывая
// dedup возвращает копию аргумента slice с удаленными дублирующимися записями.
func dedup[S ~[]E, E comparable](S) S { ... }
type Slice []int
var s Slice
s = dedup(s) // то же самое, что s = dedup[Slice, int](s)
переменная s типа Slice должна быть присваиваема параметру функции типа S, чтобы программа была корректной. Для уменьшения сложности вывод типов игнорирует направленность присваиваний, поэтому отношение типов между Slice и S можно выразить через (симметричное) уравнение типа (Slice ≡_A S) (или (S ≡_A Slice)), где A в (≡_A) указывает на то, что типы LHS и RHS должны совпадать в соответствии с правилами присваиваемости (подробнее см. раздел об унификации типов). Аналогично, параметр типа S должен удовлетворять своему ограничению ~[]E. Это можно выразить как (S ≡_C ~[\ ]E), где (X ≡_C Y) означает X удовлетворяет ограничению Y. Эти наблюдения приводят к набору двух уравнений
$$
Slice ≡_A S (1)
$$
$$
S ≡_C ~[\ ]E (2)
$$
который теперь можно решить для параметров типа S и E. Из (1) компилятор может сделать вывод, что аргументом типа для S является Slice. Аналогично, поскольку базовым типом Slice является []int, а []int должен соответствовать []E ограничения, компилятор может сделать вывод, что E должно быть int. Таким образом, для этих двух уравнений вывод типа дает следующее
Если задан набор уравнений типа, то параметрами типа для решения являются параметры типа функций, которые должны быть инстанцированы и для которых не заданы явные аргументы типа. Такие параметры типа называются связанными параметрами типа. Например, в приведенном выше примере dedup параметры типа S и E привязаны к dedup. Аргументом вызова обобщенной функции может быть сама обобщенная функция. Параметры типа этой функции включаются в набор связанных параметров типа. Типы аргументов функции могут содержать параметры типа из других функций (например, родовой функции, заключающей в себе вызов функции). Эти параметры типа также могут встречаться в уравнениях типов, но в данном контексте они не связаны. Уравнения типов всегда решаются только для связанных параметров типа.
Вывод типов поддерживает вызовы общих функций и присваивание общих функций переменным (явно типизированным для функций). Это включает передачу общих функций в качестве аргументов другим (возможно, также общим) функциям и возврат общих функций в качестве результатов. Вывод типа оперирует набором уравнений, специфичных для каждого из этих случаев. Уравнения выглядят следующим образом (списки аргументов типов опущены для ясности):
Для вызова функции (f(a_0, a_1, …)), где (f) или аргумент функции (a_i) - общая функция:
- Каждая пара ((a_i, p_i)) соответствующих аргументов и параметров функции, где (a_i) не является нетипизированной константой, дает уравнение (typeof(p_i) ≡_A typeof(a_i)).
Если (a_i) - нетипизированная константа (c_j), а (typeof(p_i)) - параметр связанного типа (P_k), то пара ((c_j, P_k)) собирается отдельно от уравнений типа.
-
Для присваивания (v = f) родовой функции (f) переменной v (неродовой) типа функции:
(typeof(v) ≡_A typeof(f)).
-
Для оператора return ..., f, ..., где f - родовая функция, возвращаемая в качестве результата в (неродовую) переменную r типа функции result:
(typeof(r) ≡_A typeof(f)).
Кроме того, каждый параметр типа (P_k) и соответствующее ограничение типа (C_k) дают уравнение типа (P_k ≡_C C_k).
При выводе типов приоритет отдается информации о типе, полученной от типизированных операндов, перед рассмотрением нетипизированных констант. Поэтому вывод выполняется в два этапа:
-
Уравнения типов решаются для параметров связанных типов с помощью унификации типов. Если унификация не удается, то вывод типа не удается.
-
Для каждого связанного параметра типа (P_k), для которого еще не выведен аргумент типа и для которого собраны одна или несколько пар ((c_j, P_k)) с этим же параметром типа, определите константный вид констант (c_j) во всех этих парах так же, как и для константных выражений. Аргумент типа для (P_k) - это тип по умолчанию для определенного вида константы. Если вид константы не может быть определен из-за конфликтующих видов констант, вывод типа завершается неудачей.
Если после этих двух фаз не все аргументы типа были найдены, вывод типа завершается неудачей.
Если эти две фазы прошли успешно, то вывод типа определил аргумент типа для каждого связанного параметра типа: (P_k ➞ A_k)
Аргумент типа (A_k) может быть составным типом, содержащим другие связанные параметры типа (P_k) в качестве типов элементов (или даже быть просто другим связанным параметром типа). В процессе многократного упрощения связанные параметры типа в каждом аргументе типа заменяются соответствующими аргументами типа для этих параметров типа до тех пор, пока каждый аргумент типа не будет свободен от связанных параметров типа.
Если аргументы типа содержат циклические ссылки на себя через связанные параметры типа, упрощение и, следовательно, вывод типа не удается. В противном случае вывод типов проходит успешно.
Унификация типов
Вывод типа решает уравнения типа с помощью унификации типов.
Унификация типов рекурсивно сравнивает типы LHS и RHS уравнения, где один или оба типа могут быть или содержать связанные параметры типа, и ищет аргументы типа для этих параметров типа так, чтобы LHS и RHS совпадали (становились идентичными или совместимыми по присваиванию, в зависимости от контекста).
Для этого при выводе типов хранится карта связанных параметров типа и аргументов типа, которая обновляется во время унификации типов. Изначально параметры связанного типа известны, но карта пуста. При унификации типа, если выводится новый аргумент типа A, в карту добавляется соответствующее отображение P ➞ A из параметра типа в аргумент.
И наоборот, при сравнении типов аргумент известного типа (аргумент типа, для которого уже существует запись в карте) занимает место соответствующего параметра типа. По мере продвижения вывода типов карта заполняется все больше и больше, пока не будут рассмотрены все уравнения или пока унификация не завершится неудачей.
Вывод типа успешен, если ни один шаг унификации не завершился неудачей и в карте есть запись для каждого параметра типа.
Например, для уравнения типа с параметром связанного типа P
([10]struct{ elem P, list [\ ]P } ≡_A [10]struct{ elem string; list [\ ]string })
Вывод типа начинается с пустой карты. Сначала унификация сравнивает структуру верхнего уровня типов LHS и RHS. Оба типа являются массивами одинаковой длины; они унифицируются, если типы элементов унифицируются. Оба типа элементов являются структурами; они унифицируются, если у них одинаковое количество полей с одинаковыми именами и если типы полей унифицируются.
Аргумент типа для P пока неизвестен (нет записи в карте), поэтому унификация P со строкой добавляет в карту отображение P ➞ string. Унификация типов поля списка требует унификации []P и []string, а значит, P и string. Поскольку аргумент типа P на данный момент известен (есть запись map для P), его аргумент типа string занимает место P. А поскольку string идентичен string, этот шаг унификации также успешен. Унификация LHS и RHS уравнения теперь завершена. Вывод типа прошел успешно, поскольку существует только одно уравнение типа, ни один шаг унификации не завершился неудачей, и карта полностью заполнена.
Унификация использует комбинацию точной и свободной унификации в зависимости от того, должны ли два типа быть идентичными, совместимыми по назначению или только структурно одинаковыми. Соответствующие правила унификации типов подробно описаны в Приложении.
Для уравнения вида (X ≡_A Y), где X и Y - типы, участвующие в присваивании (включая передачу параметров и операторы возврата), структуры типов верхнего уровня могут объединяться слабо, но типы элементов должны объединяться точно, что соответствует правилам для присваиваний.
Для уравнения вида (P ≡_C C), где P - параметр типа, а C - соответствующее ограничение, правила унификации немного сложнее:
- Если
C имеет основной тип core(C), а P имеет аргумент известного типа A, то core(C) и A должны слабо унифицироваться. Если у P нет аргумента известного типа и C содержит ровно один терм типа T, который не является базовым (тильда) типом, унификация добавляет отображение P ➞ T в карту.
- Если
C не имеет базового типа, а P имеет аргумент известного типа A, то A должен иметь все методы C, если таковые имеются, и соответствующие типы методов должны точно унифицироваться.
При решении уравнений типа из ограничений типа, решение одного уравнения может вывести дополнительные аргументы типа, которые, в свою очередь, могут позволить решить другие уравнения, зависящие от этих аргументов типа. Вывод типов повторяет унификацию типов до тех пор, пока выводятся новые аргументы типов.
Операторы
Операторы объединяют операнды в выражения.
Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr = PrimaryExpr | unary_op UnaryExpr .
binary_op = "||" | "&&" | rel_op | add_op | mul_op .
rel_op = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op = "+" | "-" | "|" | "^" .
mul_op = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .
unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
Сравнения обсуждаются в другом разделе. Для других бинарных операторов типы операндов должны быть одинаковыми, если только операция не включает сдвиги или нетипизированные константы. Об операциях, связанных только с константами, см. раздел о константных выражениях.
За исключением операций сдвига, если один из операндов является нетипизированной константой, а другой - нет, константа неявно преобразуется к типу другого операнда.
Правый операнд в выражении сдвига должен иметь тип integer [Go 1.13] или быть нетипизированной константой, представляемой значением типа uint. Если левый операнд неконстантного выражения сдвига является нетипизированной константой, он сначала неявно преобразуется к тому типу, который он принял бы, если бы выражение сдвига было заменено только его левым операндом.
var a [1024]byte
var s uint = 33
// Результаты следующих примеров приведены для 64-битных интов.
var i = 1<<s // 1 имеет тип int
var j int32 = 1<<s // 1 имеет тип int32; j == 0
var k = uint64(1<<s) // 1 имеет тип uint64; k == 1<<33
var m int = 1. 0<<s // 1.0 имеет тип int; m == 1<<33
var n = 1.0<<s == j // 1.0 имеет тип int32; n == true
var o = 1<<s == 2<<s // 1 и 2 имеют тип int; o == false
var p = 1<<s == 1<<33 // 1 имеет тип int; p == true
var u = 1.0<<s // illegal: 1.0 имеет тип float64, не может сдвигаться
var u1 = 1.0<<s != 0 // illegal: 1.0 имеет тип float64, сдвиг невозможен
var u2 = 1<<s != 1.0 // illegal: 1 имеет тип float64, сдвиг невозможен
var v1 float32 = 1<<s // illegal: 1 имеет тип float32, сдвиг невозможен
var v2 = string(1<<s) // illegal: 1 преобразуется в строку, сдвиг невозможен
var w int64 = 1.0<<33 // 1.0<<33 - постоянное выражение сдвига; w == 1<<33
var x = a[1.0<<s] // паника: 1.0 имеет тип int, но 1<<33 переполняет границы массива
var b = make([]byte, 1.0<<s) // 1.0 имеет тип int; len(b) == 1<<33
// Результаты следующих примеров приведены для 32-битных интов,
// что означает, что сдвиги будут переполняться.
var mm int = 1.0<<s // 1.0 имеет тип int; mm == 0
var oo = 1<<s == 2<<s // 1 и 2 имеют тип int; oo == true
var pp = 1<<s == 1<<33 // незаконно: 1 имеет тип int, но 1<<33 переполняет int
var xx = a[1.0<<s] // 1.0 имеет тип int; xx == a[0]
var bb = make([]byte, 1.0<<s) // 1.0 имеет тип int; len(bb) == 0
Старшинство операторов
Унарные операторы имеют наивысшее старшинство. Поскольку операторы ++ и -- формируют утверждения, а не выражения, они не входят в иерархию операторов. Как следствие, оператор *p++ - это то же самое, что и (*p)++.
Существует пять уровней старшинства для бинарных операторов. Операторы умножения связываются сильнее всего, затем следуют операторы сложения, сравнения, && (логическое И) и, наконец, || (логическое ИЛИ):
| Старшинство |
Оператор |
| 5 |
* / % << >> & &^ |
| 4 |
+ - ^ | |
| 3 |
== != < <= > >= |
| 2 |
&& |
| 1 |
|| |
Бинарные операторы с одинаковым старшинством связываются слева направо. Например, x / y * z - это то же самое, что (x / y) * z.
+x // x
42 + a - b // (42 + a) - b
23 + 3*x[i] // 23 + (3 * x[i])
x <= f() // x <= f()
^a >> b // (^a) >> b
f() || g() // f() || g()
x == y+1 && <-chanInt > 0 // (x == (y+1)) && ((<-chanInt) > 0)
Арифметические операторы
Арифметические операторы применяются к числовым значениям и дают результат того же типа, что и первый операнд. Четыре стандартных арифметических оператора (+, -, *, /) применяются к целым, плавающим точкам и комплексным типам; + также применяется к строкам. Побитовые логические операторы и операторы сдвига применяются только к целым числам.
+ sum integers, floats, complex values, strings
- difference integers, floats, complex values
* product integers, floats, complex values
/ quotient integers, floats, complex values
% remainder integers
& bitwise AND integers
| bitwise OR integers
^ bitwise XOR integers
&^ bit clear (AND NOT) integers
<< left shift integer << integer >= 0
>> right shift integer >> integer >= 0
Если тип операнда является параметром типа, оператор должен применяться к каждому типу в этом наборе типов. Операнды представляются как значения аргумента типа, с которым инстанцирован параметр типа, а операция вычисляется с точностью этого аргумента типа. Например, для функции:
func dotProduct[F ~float32|~float64](v1, v2 []F) F {
var s F
for i, x := range v1 {
y := v2[i]
s += x * y
}
return s
}
произведение x * y и сложение s += x * y вычисляются с точностью float32 или float64, соответственно, в зависимости от аргумента типа для F.
Целочисленные операторы¶
Для двух целых значений x и y целочисленный коэффициент q = x / y и остаток r = x % y удовлетворяют следующим соотношениям:
x = q*y + r and |r| < |y|
при усечении x / y до нуля (“усеченное деление”).
x y x / y x % y
5 3 1 2
-5 3 -1 -2
5 -3 -1 2
-5 -3 1 -2
Единственным исключением из этого правила является то, что если делитель x является отрицательным значением для типа int, то коэфициент q = x / -1 равен x (и r = 0) из-за переполнения двух дополняющих целых чисел:
x, q
int8 -128
int16 -32768
int32 -2147483648
int64 -9223372036854775808
Если делитель является константой, он не должен быть нулевым. Если делитель равен нулю во время выполнения, возникает паника во время выполнения. Если делимое неотрицательно, а делитель - постоянная степень 2, деление можно заменить сдвигом вправо, а вычисление остатка - побитовой операцией AND:
x x / 4 x % 4 x >> 2 x & 3
11 2 3 2 3
-11 -2 -3 -3 1
Операторы сдвига сдвигают левый операнд на заданный правым операндом счетчик сдвига, который должен быть неотрицательным. Если во время выполнения счетчик сдвигов отрицателен, возникает паника во время выполнения.
Операторы сдвига выполняют арифметические сдвиги, если левый операнд является знаковым целым числом, и логические сдвиги, если он является беззнаковым целым числом.
Верхнее ограничение на количество сдвигов отсутствует. Операторы сдвига ведут себя так, как будто левый операнд сдвигается n раз на 1 при количестве сдвигов n. В результате x << 1 - то же самое, что x*2, а x >> 1 - то же самое, что x/2, но усеченное в сторону отрицательной бесконечности.
Для целочисленных операндов унарные операторы +, - и ^ определяются следующим образом:
+x 0 + x
-x негатив 0 - x
^x побитовое дополнение m ^ x, причем m = "все биты установлены в 1" для беззнакового x
и m = -1 для знакового x
Переполнение целого числа
Для беззнаковых целых значений операции +, -, * и << вычисляются по модулю 2n, где n - битовая ширина типа беззнакового целого числа. Грубо говоря, эти беззнаковые целочисленные операции отбрасывают старшие биты при переполнении, и программы могут полагаться на “обход”.
Для знаковых целых чисел операции +, -, *, / и << могут легально переполняться, а результирующее значение существует и детерминированно определяется представлением знакового целого, операцией и ее операндами. Переполнение не вызывает паники во время выполнения. Компилятор не может оптимизировать код в предположении, что переполнение не произойдет. Например, он не может считать, что x < x + 1 всегда истинно.
Операторы с плавающей точкой
Для чисел с плавающей точкой и комплексных чисел +x равно x, а -x - отрицание x. Результат деления на ноль с плавающей точкой или комплексного числа не определен за пределами стандарта IEEE 754; возникнет ли паника во время выполнения, зависит от конкретной реализации.
Реализация может объединить несколько операций с плавающей точкой в одну операцию, возможно, в нескольких операторах, и получить результат, отличающийся от значения, полученного при выполнении и округлении инструкций по отдельности. Явное преобразование типа с плавающей точкой округляет с точностью целевого типа, предотвращая слияние, которое отменит это округление.
Например, в некоторых архитектурах предусмотрена инструкция “слитного умножения и сложения” (FMA), которая вычисляет x*y + z без округления промежуточного результата x*y. Эти примеры показывают, когда реализация Go может использовать эту инструкцию:
// FMA допускает вычисление r, поскольку x*y не округляется явно:
r = x*y + z
r = z; r += x*y
t = x*y; r = t + z
*p = x*y; r = *p + z
r = x*y + float64(z)
// FMA запрещено для вычисления r, так как при этом опускается округление x*y:
r = float64(x*y) + z
r = z; r += float64(x*y)
t = float64(x*y); r = t + z
Конкатенация строк
Строки можно конкатенировать с помощью оператора + или оператора присваивания +=
s := "hi" + string(c)
s += " and good bye"
Сложение строк создает новую строку путем конкатенации операндов.
Операторы сравнения
Операторы сравнения сравнивают два операнда и выдают нетипизированное булево значение.
== equal
!= not equal
< less
<= less or equal
> greater
>= greater or equal
В любом сравнении первый операнд должен быть приписан к типу второго операнда, или наоборот.
Операторы равенства == и != применяются к операндам сравниваемых типов. Операторы упорядочивания <, <=, > и >= применяются к операндам упорядоченных типов. Эти термины и результат сравнения определяются следующим образом:
- Булевы типы сравнимы. Два булевых значения равны, если они либо оба истинны, либо оба ложны.
- Целочисленные типы сравнимы и упорядочены. Два целочисленных значения сравниваются обычным способом.
- Типы с плавающей точкой сравнимы и упорядочены. Два значения с плавающей точкой сравниваются в соответствии со стандартом IEEE 754.
- Комплексные типы сравнимы. Два комплексных значения
u и v равны, если real(u) == real(v) и imag(u) == imag(v).
- Строковые типы сравнимы и упорядочены. Два строковых значения сравниваются лексически побайтно.
- Типы указателей сравнимы. Два значения указателя равны, если они указывают на одну и ту же переменную или если оба имеют значение
nil. Указатели на разные переменные нулевого размера могут быть равны, а могут и не быть.
- Типы каналов сопоставимы. Два значения канала равны, если они были созданы одним и тем же вызовом
make или если оба имеют значение nil.
- Типы интерфейсов, которые не являются параметрами типа, сравнимы. Два значения интерфейса равны, если у них одинаковые динамические типы и одинаковые динамические значения или если оба имеют значение
nil.
- Значение
x неинтерфейсного типа X и значение t интерфейсного типа T можно сравнивать, если тип X сопоставим и X реализует T. Они равны, если динамический тип t идентичен X, а динамическое значение t равно x.
- Типы структур сопоставимы, если все их типы полей сопоставимы. Два значения структуры равны, если их соответствующие непустые значения полей равны. Поля сравниваются в исходном порядке, и сравнение прекращается, как только два значения полей различаются (или все поля были сравнены).
- Типы массивов сравнимы, если типы их элементов сравнимы. Два значения массива равны, если равны значения соответствующих элементов. Элементы сравниваются в порядке возрастания индекса, и сравнение прекращается, как только два значения элемента различаются (или все элементы были сравнены).
- Параметры типа сравнимы, если они строго сравнимы (см. ниже).
Сравнение двух значений интерфейса с одинаковыми динамическими типами вызывает панику во время выполнения, если этот тип не является сопоставимым. Это поведение применимо не только к прямому сравнению значений интерфейса, но и к сравнению массивов значений интерфейса или структур с полями, имеющими значения интерфейса.
Типы slice, map и function не поддаются сравнению. Однако в качестве особого случая значение slice, map или function может быть сравнено с заранее объявленным идентификатором nil. Сравнение значений указателя, канала и интерфейса с nil также допускается и следует из общих правил, приведенных выше.
const c = 3 < 4 // c — нетипизированная булева константа true
type MyBool bool
var x, y int
var (
// Результат сравнения — булево значение без типа.
// Применяются обычные правила присваивания.
b3 = x == y // b3 имеет тип bool
b4 bool = x == y // b4 имеет тип bool
b5 MyBool = x == y // b5 имеет тип MyBool)
Тип строго сопоставим, если он сопоставим, не является интерфейсным типом и не состоит из интерфейсных типов. В частности:
- Булевы, числовые, строковые, указательные и канальные типы строго сопоставимы.
- Типы структур строго сопоставимы, если все их типы полей строго сопоставимы.
- Типы массивов строго сопоставимы, если типы их элементов строго сопоставимы.
- Параметры типа строго сравнимы, если все типы в их наборе типов строго сравнимы.
Логические операторы
Логические операторы применяются к булевым значениям и дают результат того же типа, что и операнды. Сначала вычисляется левый операнд, а затем правый, если этого требует условие.
&& conditional AND p && q is "if p then q else false"
|| conditional OR p || q is "if p then true else q"
! NOT !p is "not p"
Операторы адресации
Для операнда x типа T операция адресации &x генерирует указатель типа *T на x. Операнд должен быть адресуемым, то есть либо операцией индексации переменной, указателя или фрагмента; либо селектором поля адресуемого операнда struct; либо операцией индексации массива адресуемого массива. В качестве исключения из требования адресуемости x может также быть составным литералом (возможно, со скобками). Если вычисление x приведет к панике во время выполнения, то и вычисление &x тоже.
Для операнда x типа указателя *T, указатель *x обозначает переменную типа T, на которую указывает x. Если x - nil, попытка вычислить *x вызовет панику во время выполнения.
&x
&a[f(2)]
&Point{2, 3}
*p
*pf(x)
var x *int = nil
*x // вызывает панику во время выполнения
&*x // вызывает панику во время выполнения
Оператор приема
Для операнда ch, основной тип которого - канал, значением операции приема <-ch является значение, полученное из канала ch. Направление канала должно разрешать операции приема, а тип операции приема - тип элемента канала. Выражение блокируется до тех пор, пока не будет получено значение. Получение из канала нулевого значения блокируется навсегда. Операция приема по закрытому каналу всегда может быть выполнена немедленно, выдавая нулевое значение типа элемента после получения всех ранее отправленных значений.
v1 := <-ch
v2 = <-ch
f(<-ch)
<-strobe // подождать до появления тактового импульса и отбросить полученное значение
Выражение приема, используемое в операторе присваивания или инициализации специальной формы
x, ok = <-ch
x, ok := <-ch
var x, ok = <-ch
var x, ok T = <-ch
возвращает дополнительный нетипизированный результат типа boolean, сообщающий об успешности связи. Значение ok равно true, если полученное значение было успешно отправлено в канал, или false, если это нулевое значение, сгенерированное из-за того, что канал закрыт и пуст.
Преобразования
Преобразование изменяет тип выражения на тип, указанный преобразованием. Преобразование может появляться буквально в исходном коде или подразумеваться контекстом, в котором появляется выражение.
Явное преобразование — это выражение вида T(x), где T — тип, а x — выражение, которое может быть преобразовано в тип T.
Conversion = Type "(" Expression [ "," ] ")" .
Если тип начинается с оператора * или <-, или если тип начинается с ключевого слова func и не имеет списка результатов, он должен быть заключен в круглые скобки, когда это необходимо, чтобы избежать двусмысленности:
*Point(p) // то же, что *(Point(p))
(*Point)(p) // p преобразуется в *Point
<-chan int(c) // то же, что <-(chan int(c))
(<-chan int)(c) // c преобразуется в <- chan int
func()(x) // сигнатура функции func() x
(func())(x) // x преобразуется в func()
(func() int)(x) // x преобразуется в func() int
func() int(x) // x преобразуется в func() int (недвусмысленно)
Константное значение x может быть приведено к типу T, если x может быть представлено значением T. Как частный случай, целочисленная константа x может быть явно приведена к строковому типу по тому же правилу, что и для неконстантных x.
Преобразование константы к типу, который не является параметром типа, дает типизированную константу.
uint(iota) // йота значение типа uint
float32(2.718281828) // 2.718281828 типа float32
complex128(1) // 1.0 + 0.0i типа complex128
float32(0.49999999) // 0.5 типа float32
float64(-1e-1000) // 0.0 типа float64
string('x') // "x" типа string
string(0x266c) // "♬" типа string
myString("foo" + "bar") // "foobar" типа myString
string([]byte{'a'}) // не константа: []byte{'a'} не является константой
(*int)(nil) // не является константой: nil не является константой, *int не является булевым, числовым или строковым типом
int(1.2) // незаконно: 1.2 не может быть представлено как int
string(65.0) // незаконно: 65.0 не является целочисленной константой
Преобразование константы в параметр типа дает неконстантное значение этого типа, причем это значение представлено как значение аргумента типа, с которым инстанцирован параметр типа. Например, для функции:
func f[P ~float32|~float64]() {
… P(1.1) …
}
преобразование P(1.1) приводит к непостоянному значению типа P, а значение 1.1 представляется как float32 или float64 в зависимости от аргумента типа для f. Соответственно, если f инстанцировано с типом float32, то числовое значение выражения P(1.1) + 1.2 будет вычислено с той же точностью, что и соответствующее непостоянное сложение float32.
Неконстантное значение x может быть преобразовано к типу T в любом из этих случаев:
x присваивается T.
- без учета тегов
struct (см. ниже), тип x и T не являются параметрами типа, но имеют одинаковые базовые типы.
- без учета тегов
struct (см. ниже), тип x и T являются типами-указателями, которые не являются именованными типами, а их базовые типы-указатели не являются параметрами типа, но имеют одинаковые базовые типы.
- Тип
x и T - оба целочисленные типы или типы с плавающей точкой.
- Тип
x и T - оба комплексные типы.
x - целое число или фрагмент байтов или рун, а T - строковый тип.
x - строка, а T - фрагмент байтов или рун.
x - фрагмент, T - массив [Go 1.20] или указатель на массив [Go 1.17], причем типы фрагмента и массива имеют одинаковые типы элементов.
Кроме того, если T или x типа V являются параметрами типа, x также может быть преобразован в тип T, если выполняется одно из следующих условий:
- И
V, и T являются параметрами типа, и значение каждого типа в наборе типов V может быть преобразовано в каждый тип в наборе типов T.
- Только
V является параметром типа и значение каждого типа из набора типов V может быть преобразовано в T.
- Только
T является параметром типа и x может быть преобразовано в каждый тип из набора типов T.
- Теги
struct игнорируются при сравнении типов struct на идентичность для целей преобразования:
type Person struct {
Name string
Address *struct {
Street string
City string
}
}
var data *struct {
Name string `json:"name"`
Address *struct {
Street string `json:"street"`
City string `json:"city"`
} `json:"address"`
}
var person = (*Person)(data) // // игнорируя теги, базовые типы идентичны
Особые правила применяются к (неконстантным) преобразованиям между числовыми типами или к и от строкового типа. Эти преобразования могут изменить представление x и повлечь за собой затраты времени выполнения. Все остальные преобразования изменяют только тип, но не представление x.
Не существует лингвистического механизма для преобразования между указателями и целыми числами. Пакет unsafe реализует эту функциональность в ограниченных условиях.
Преобразования между числовыми типами
Для преобразования неконстантных числовых значений применяются следующие правила:
- При преобразовании между целочисленными типами, если значение является целым числом со знаком, оно расширяется по знаку до неявной бесконечной точности; в противном случае оно расширяется до нуля. Затем оно усекается, чтобы поместиться в размер типа результата. Например, если
v := uint16(0x10F0), то uint32(int8(v)) == 0xFFFFFFF0. Преобразование всегда дает действительное значение; нет никаких признаков переполнения.
- При преобразовании числа с плавающей запятой в целое число дробная часть отбрасывается (усечение в сторону нуля).
- При преобразовании целого числа или числа с плавающей запятой в тип с плавающей запятой или комплексного числа в другой комплексный тип, значение результата округляется с точностью, указанной типом назначения. Например, значение переменной
x типа float32 может храниться с дополнительной точностью, превышающей точность 32-разрядного числа IEEE 754, но float32(x) представляет результат округления значения x до 32-разрядной точности. Аналогично, x + 0.1 может использовать более 32 бит точности, но float32(x + 0.1) — нет.
- Во всех неконстантных преобразованиях, включающих значения с плавающей запятой или комплексные значения, если тип результата не может представить значение, преобразование выполняется успешно, но значение результата зависит от реализации.
Преобразования в строковый тип и из него
- Преобразование фрагмента байтов в строковый тип дает строку, последовательные байты которой являются элементами фрагмента.
string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"
string([]byte{}) // ""
string([]byte(nil)) // ""
type bytes []byte
string(bytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"
type myByte byte
string([]myByte{'w', 'o', 'r', 'l', 'd', '!'}) // "world!"
myString([]myByte{'\xf0', '\x9f', '\x8c', '\x8d'}) // "🌍"
- Преобразование фрагмента рун в строковый тип дает строку, которая представляет собой конкатенацию отдельных значений рун, преобразованных в строки.
string([]rune{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"
string([]rune{}) // ""
string([]rune(nil)) // ""
type runes []rune
string(runes{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"
type myRune rune
string([]myRune{0x266b, 0x266c}) // "\u266b\u266c" == "♫♬"
myString([]myRune{0x1f30e}) // "\U0001f30e" == "🌎"
- Преобразование значения типа
string в фрагмент типа bytes дает ненулевой фрагмент, последовательными элементами которого являются байты строки. Объем результирующего фрагмента зависит от реализации и может быть больше длины фрагмента.
[]byte("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
[]byte("") // []byte{}
bytes("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
[]myByte("world!") // []myByte{'w', 'o', 'r', 'l', 'd', '!'}
[]myByte(myString("🌏")) // []myByte{'\xf0', '\x9f', '\x8c', '\x8f'}
- Преобразование значения типа
string в фрагмент типа runes дает фрагмент, содержащий отдельные кодовые позиции Unicode строки. Объем результирующего фрагмента зависит от реализации и может быть больше длины фрагмента.
[]rune(myString("白鵬翔")) // []rune{0x767d, 0x9d6c, 0x7fd4}
[]rune("") // []rune{}
runes("白鵬翔") // []rune{0x767d, 0x9d6c, 0x7fd4}
[]myRune("♫♬") // []myRune{0x266b, 0x266c}
[]myRune(myString("🌐")) // []myRune{0x1f310}
- Наконец, по историческим причинам целочисленное значение может быть преобразовано в строковый тип. При таком преобразовании получается строка, содержащая (возможно, многобайтовое) представление UTF-8 кодовой точки Юникода с заданным целочисленным значением. Значения, выходящие за пределы диапазона допустимых кодовых точек Юникода, преобразуются в
"\uFFFD".
string('a') // "a"
string(65) // "A"
string('\xf8') // "\u00f8" == "ø" == "\xc3\xb8"
string(-1) // "\ufffd" == "\xef\xbf\xbd"
type myString string
myString('\u65e5') // "\u65e5" == "日" == "\xe6\x97\xa5"
Примечание: Возможно, со временем эта форма преобразования будет удалена из языка. Инструмент go vet отмечает некоторые преобразования целых чисел в строки как потенциальные ошибки. Вместо них следует использовать библиотечные функции, такие как utf8.AppendRune или utf8.EncodeRune.
Преобразования из среза в массив или указатель массива
Преобразование среза в массив дает массив, содержащий элементы массива, лежащего в основе среза. Аналогично, преобразование среза в указатель массива дает указатель на массив, лежащий в основе среза. В обоих случаях, если длина фрагмента меньше длины массива, возникает паника во время выполнения.
s := make([]byte, 2, 4)
a0 := [0]byte(s)
a1 := [1]byte(s[1:]) // a1[0] == s[1]
a2 := [2]byte(s) // a2[0] == s[0]
a4 := [4]byte(s) // panics: len([4]byte) > len(s)
s0 := (*[0]byte)(s) // s0 != nil
s1 := (*[1]byte)(s[1:]) // &s1[0] == &s[1]
s2 := (*[2]byte)(s) // &s2[0] == &s[0]
s4 := (*[4]byte)(s) // panics: len([4]byte) > len(s)
var t []string
t0 := [0]string(t) // ok for nil slice t
t1 := (*[0]string)(t) // t1 == nil
t2 := (*[1]string)(t) // panics: len([1]string) > len(t)
u := make([]byte, 0)
u0 := (*[0]byte)(u) // u0 != nil
Константные выражения
Константные выражения могут содержать только константные операнды и оцениваются во время компиляции.
Нетипизированные булевы, числовые и строковые константы могут использоваться в качестве операндов везде, где допустимо использовать операнд булевого, числового или строкового типа соответственно.
Сравнение констант всегда дает нетипизированную булеву константу. Если левый операнд выражения сдвига констант является нетипизированной константой, то результатом будет целочисленная константа, в противном случае - константа того же типа, что и левый операнд, который должен быть целочисленного типа.
Любая другая операция над нетипизированными константами приводит к нетипизированной константе того же типа, то есть к булевой, целочисленной, с плавающей точкой, комплексной или строковой константе. Если нетипизированные операнды бинарной операции (кроме сдвига) имеют разные типы, то результатом будет операнд того типа, который встречается далее в этом списке: целое число, руна, плавающая точка, комплекс. Например, нетипизированная целочисленная константа, деленная на нетипизированную комплексную константу, дает нетипизированную комплексную константу.
const a = 2 + 3.0 // a == 5.0 (untyped floating-point constant)
const b = 15 / 4 // b == 3 (untyped integer constant)
const c = 15 / 4.0 // c == 3.75 (untyped floating-point constant)
const Θ float64 = 3/2 // Θ == 1.0 (type float64, 3/2 is integer division)
const Π float64 = 3/2. // Π == 1.5 (type float64, 3/2. is float division)
const d = 1 << 3.0 // d == 8 (untyped integer constant)
const e = 1.0 << 3 // e == 8 (untyped integer constant)
const f = int32(1) << 33 // illegal (constant 8589934592 overflows int32)
const g = float64(2) >> 1 // illegal (float64(2) is a typed floating-point constant)
const h = "foo" > "bar" // h == true (untyped boolean constant)
const j = true // j == true (untyped boolean constant)
const k = 'w' + 1 // k == 'x' (untyped rune constant)
const l = "hi" // l == "hi" (untyped string constant)
const m = string(k) // m == "x" (type string)
const Σ = 1 - 0.707i // (untyped complex constant)
const Δ = Σ + 2.0e-4 // (untyped complex constant)
const Φ = iota*1i - 1/1i // (untyped complex constant)
Применение встроенной функции complex к нетипизированным константам типа integer, rune или floating-point дает нетипизированную константу типа complex.
const ic = complex(0, c) // ic == 3.75i (untyped complex constant)
const iΘ = complex(0, Θ) // iΘ == 1i (type complex128)
Выражения констант всегда оцениваются точно; промежуточные значения и сами константы могут требовать точности, значительно большей, чем поддерживается любым заранее объявленным типом в языке. Следующие объявления являются законными:
const Huge = 1 << 100 // Huge == 1267650600228229401496703205376 (untyped integer constant)
const Four int8 = Huge >> 98 // Four == 4 (type int8)
Делитель постоянной операции деления или остатка не должен быть равен нулю:
3.14 / 0.0 // illegal: division by zero
Значения типизированных констант всегда должны быть точно представлены значениями типа константы. Следующие константные выражения являются незаконными:
uint(-1) // -1 cannot be represented as a uint
int(3.14) // 3.14 cannot be represented as an int
int64(Huge) // 1267650600228229401496703205376 cannot be represented as an int64
Four * 300 // operand 300 cannot be represented as an int8 (type of Four)
Four * 100 // product 400 cannot be represented as an int8 (type of Four)
Маска, используемая унарным оператором побитового дополнения ^, соответствует правилу для неконстант: маска - это все 1 для беззнаковых констант и -1 для знаковых и нетипизированных констант.
^1 // untyped integer constant, equal to -2
uint8(^1) // illegal: same as uint8(-2), -2 cannot be represented as a uint8
^uint8(1) // typed uint8 constant, same as 0xFF ^ uint8(1) = uint8(0xFE)
int8(^1) // same as int8(-2)
^int8(1) // same as -1 ^ int8(1) = -2
Ограничение на реализацию: Компилятор может использовать округление при вычислении нетипизированных выражений с плавающей точкой или сложных констант; см. ограничение на реализацию в разделе о константах. Такое округление может привести к тому, что константное выражение с плавающей точкой окажется недопустимым в контексте целого числа, даже если оно будет целочисленным при вычислении с бесконечной точностью, и наоборот.
Порядок вычислений
На уровне пакета зависимости инициализации определяют порядок вычислений отдельных выражений инициализации в объявлениях переменных. В противном случае при вычислении операндов выражения, присваивания или оператора return все вызовы функций, вызовы методов, операции приема и двоичные логические операции вычисляются в лексическом порядке слева направо.
Например, в присваивании (локальном для функции)
y[f()], ok = g(z || h(), i()+x[j()], <-c), k()
вызовы функций и обмен данными происходят в порядке f(), h() (если z оценивается как false), i(), j(), <-c, g() и k(). Однако порядок этих событий по сравнению с вычислением и индексацией x и вычислением y и z не определен, за исключением лексических требований. Например, функция g не может быть вызвана до того, как будут вычислены ее аргументы.
a := 1
f := func() int { a++; return a }
x := []int{a, f()} // x может быть [1, 2] или [2, 2]: порядок оценки между a и f() не указан
m := map[int]int{a: 1, a: 2} // m может быть {2: 1} или {2: 2}: порядок оценки между двумя присваиваниями map не указан
n := map[int]int{a: f()} // n может быть {2: 3} или {3: 3}: порядок оценки между ключом и значением не указан
На уровне пакетов зависимости инициализации отменяют правило “слева направо” для отдельных выражений инициализации, но не для операндов внутри каждого выражения:
var a, b, c = f() + v(), g(), sqr(u()) + v()
func f() int { return c }
func g() int { return a }
func sqr(x int) int { return x*x }
// функции u и v независимы от всех других переменных и функций
Вызовы функций происходят в порядке u(), sqr(), v(), f(), v() и g().
Операции с плавающей точкой в одном выражении оцениваются в соответствии с ассоциативностью операторов. Явные круглые скобки влияют на оценку, переопределяя ассоциативность по умолчанию. В выражении x + (y + z) сложение y + z выполняется до сложения x.
Операторы
Операторы управляют исполнением.
Statement = Declaration | LabeledStmt | SimpleStmt |
GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
DeferStmt .
SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .
Завершающие операторы
Завершающий оператор прерывает обычный поток управления в блоке. Следующие операторы являются завершающими:
- Оператор
«return» или «goto».
- Вызов встроенной функции
panic.
- Блок, в котором список операторов заканчивается завершающим оператором.
- Оператор
«if», в котором:
- присутствует ветвь
«else», и
- обе ветви являются завершающими операторами.
- Оператор
«for», в котором:
- нет операторов
«break», относящихся к оператору «for», и
- отсутствует условие цикла, и
- оператор
«for» не использует диапазон (range).
- Оператор
«switch», в котором:
- нет операторов
«break», относящихся к оператору «switch»,
- есть
default case, и
- списки операторов в каждом случае, включая случай по умолчанию (
default case), заканчиваются завершающим оператором или, возможно, помеченным оператором «fallthrough».
- Оператор
«select», в котором:
- нет операторов
«break», относящихся к оператору «select», и
- списки операторов в каждом случае, включая (
default case), если он присутствует, заканчиваются завершающим оператором.
Маркированный оператор, обозначает завершающий оператор.
Все остальные операторы не являются завершающими.
Список операторов заканчивается завершающим оператором, если список не пустой и его последний непустой оператор является завершающим.
Пустые операторы
Пустой оператор ничего не делает.
Маркированные операторы
Маркированный оператор может быть целью оператора goto, break или continue.
LabeledStmt = Label ":" Statement .
Label = identifier .
Error: log.Panic("error encountered")
Выражения
За исключением специальных встроенных функций, вызовы функций и методов, а также операции получения могут появляться в контексте выражений. Такие выражения могут быть заключены в круглые скобки.
ExpressionStmt = Expression .
Следующие встроенные функции не допускаются в контексте оператора:
append cap complex imag len make new real
unsafe.Add unsafe.Alignof unsafe.Offsetof unsafe.Sizeof unsafe.Slice unsafe.SliceData unsafe.String unsafe.StringData
h(x+y)
f.Close()
<-ch
(<-ch)
len("foo") // illegal if len is the built-in function
Операторы отправки
Оператор отправки отправляет значение по каналу. Тип ядра выражения канала должен быть каналом, направление канала должно разрешать операции отправки, а тип отправляемого значения должен быть присвоен типу элемента канала.
SendStmt = Channel "<-" Expression .
Channel = Expression .
И канал, и выражение значения оцениваются перед началом обмена данными. Обмен данными блокируется до тех пор, пока отправка не будет выполнена. Отправка по небуферизованному каналу может продолжаться, если приемник готов. Отправка по буферизованному каналу может продолжаться, если в буфере есть место. Отправка по закрытому каналу продолжается, вызывая панику во время выполнения. Отправка по нулевому каналу блокируется навсегда.
ch <- 3 // отправляем значение 3 в канал ch
Операторы IncDec
Операторы "++" и "--" инкрементируют или декрементируют свои операнды на нетипизированную константу 1. Как и в случае с присваиванием, операнд должен быть адресуемым или выражением индекса карты.
IncDecStmt = Expression ( "++" | "--" ) .
Следующие операторы присваивания семантически эквивалентны:
IncDec statement Assignment
x++ x += 1
x-- x -= 1
Операторы присваивания
Оператор присваивания заменяет текущее значение, хранящееся в переменной, новым значением, заданным выражением. Оператор присваивания может присваивать одно значение одной переменной или несколько значений соответствующему числу переменных.
Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .
Каждый операнд левой стороны должен быть адресуемым, выражением индекса карты или (только для присваивания =) идентификатором пробела. Операнды могут быть заключены в круглые скобки.
x = 1
*p = f()
a[i] = 23
(k) = <-ch // same as: k = <-ch
Операция присваивания x op= y, где op - двоичный арифметический оператор, эквивалентна x = x op (y), но оценивает x только один раз. Конструкция op= является единственной лексемой. В операциях присваивания и левый, и правый списки выражений должны содержать ровно по одному однозначному выражению, причем левое выражение не должно быть пустым идентификатором.
Кортеж присваивает отдельные элементы многозначной операции списку переменных. Существует две формы. В первом случае правым операндом является одно многозначное выражение, например вызов функции, операция канала или карты, или утверждение типа. Количество операндов в левой части должно соответствовать количеству значений. Например, если f - функция, возвращающая два значения,
присваивает первое значение x, а второе - y. Во второй форме количество операндов слева должно быть равно количеству выражений справа, каждое из которых должно быть однозначным, и n-е выражение справа присваивается n-му операнду слева:
one, two, three = '一', '二', '三'
Идентификатор blank _ позволяет игнорировать значения правой стороны в присваивании:
_ = x // evaluate x but ignore it
x, _ = f() // evaluate f() but ignore second result value
Присваивание выполняется в два этапа. Во-первых, операнды индексных выражений и указателей (включая неявные указатели в селекторах) слева и выражения справа оцениваются в обычном порядке. Во-вторых, присваивания выполняются в порядке слева направо.
a, b = b, a // exchange a and b
x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2 // set i = 1, x[0] = 2
i = 0
x[i], i = 2, 1 // set x[0] = 2, i = 1
x[0], x[0] = 1, 2 // set x[0] = 1, then x[0] = 2 (so x[0] == 2 at end)
x[1], x[3] = 4, 5 // set x[1] = 4, then panic setting x[3] = 5.
type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7 // set x[2] = 6, then panic setting p.x = 7
i = 2
x = []int{3, 5, 7}
for i, x[i] = range x { // set i, x[2] = 0, x[0]
break
}
// after this loop, i == 0 and x is []int{3, 5, 3}
В присваиваниях каждое значение должно быть присваиваемым типу операнда, которому оно присваивается, за исключением следующих особых случаев:
- Любое типизированное значение может быть присвоено пустому идентификатору.
- Если нетипизированная константа присваивается переменной типа
interface или идентификатору blank, константа сначала неявно преобразуется к своему типу по умолчанию.
- Если переменной типа
interface или идентификатору blank присваивается нетипизированное булево значение, оно сначала неявно преобразуется к типу bool.
Когда переменной присваивается значение, заменяются только те данные, которые хранятся в переменной. Если значение содержит ссылку, присваивание копирует ссылку, но не делает копию данных, на которые ссылается переменная (например, базового массива фрагмента).
var s1 = []int{1, 2, 3}
var s2 = s1 // s2 хранит дескриптор среза s1
s1 = s1[:1] // длина s1 равна 1, но он по-прежнему разделяет свой базовый массив с s2
s2[0] = 42 // установка s2[0] изменяет и s1[0]
fmt.Println(s1, s2) // выводит [42] [42 2 3]
var m1 = make(map[string]int)
var m2 = m1 // m2 хранит дескриптор карты m1
m1["foo"] = 42 // установка m1["foo"] изменяет m2["foo"] также
fmt.Println(m2["foo"]) // печатает 42
Операторы If
Операторы If задают условное выполнение двух ветвей в зависимости от значения булева выражения. Если выражение равно true, то выполняется ветвь "if", в противном случае, если оно присутствует, выполняется ветвь "else".
IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
Выражению может предшествовать простой оператор, который выполняется до того, как выражение if будет выполнено.
if x := f(); x < y {
return x
} else if x > z {
return z
} else {
return y
}
Оператор Switch
Операторы "Switch" обеспечивают многоходовое выполнение. Выражение или тип сравнивается с "case" внутри "switch", чтобы определить, какую ветвь следует выполнить.
SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .
Существует две формы: переключатели выражений и переключатели типов. В переключателе выражений case содержат выражения, которые сравниваются со значением выражения switch. В переключателе типов case содержат типы, которые сравниваются с типом специально аннотированного выражения switch. Выражение switch оценивается ровно один раз в операторе switch.
Переключатели выражений
В switch оценивается выражение, а выражения case, которые не обязательно должны быть константами, оцениваются слева направо и сверху вниз; первое из них, которое совпадает с выражением switch, запускает выполнение утверждений связанного с ним case; остальные case пропускаются. Если ни один случай не совпадает и существует случай “по умолчанию”, то выполняются его утверждения. Может быть не более одного case по умолчанию, и он может находиться в любом месте оператора “switch”. Отсутствующее выражение switch эквивалентно булевому значению true.
ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
ExprCaseClause = ExprSwitchCase ":" StatementList .
ExprSwitchCase = "case" ExpressionList | "default" .
Если выражение switch оценивается как константа без типа, оно сначала неявно преобразуется в свой тип по умолчанию. Предварительно объявленное значение без типа nil не может использоваться в качестве выражения switch. Тип выражения switch должен быть сопоставимым.
Если выражение case не имеет типа, оно сначала неявно преобразуется в тип выражения switch. Для каждого выражения case x и значения t выражения switch должно быть допустимым сравнение x == t.
Другими словами, выражение switch обрабатывается так, как если бы оно использовалось для объявления и инициализации временной переменной t без явного типа; именно это значение t используется для проверки равенства каждого выражения case x.
В case или default последнее непустое выражение может быть выражением «fallthrough», указывающим, что управление должно перейти от конца этого условия к первому выражению следующего условия. В противном случае управление переходит к концу выражения «switch». Выражение «fallthrough» может появляться в качестве последнего выражения всех условий, кроме последнего, выражения switch.
Выражение switch может предваряться простым оператором, который выполняется до вычисления выражения.
switch tag {
default: s3()
case 0, 1, 2, 3: s1()
case 4, 5, 6, 7: s2()
}
switch x := f(); { // missing switch expression means "true"
case x < 0: return -x
default: return x
}
switch {
case x < y: f1()
case x < z: f2()
case x == 4: f3()
}
Ограничение на реализацию: Компилятор может запретить несколько выражений case, оценивающих одну и ту же константу. Например, современные компиляторы не допускают дублирования целых чисел, констант с плавающей точкой или строк в выражениях case.
Переключатели типов
Переключатель типов сравнивает типы, а не значения. В остальном он похож на переключатель выражений. Он обозначается специальным выражением switch, которое имеет вид утверждения типа с использованием ключевого слова type, а не фактического типа:
switch x.(type) {
// cases
}
Как и в утверждениях типа, x должен быть типом интерфейса, но не параметром типа, и каждый неинтерфейсный тип T, перечисленный в случае, должен реализовывать тип x. Типы, перечисленные в случаях переключателя типов, должны быть разными.
TypeSwitchStmt = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause = TypeSwitchCase ":" StatementList .
TypeSwitchCase = "case" TypeList | "default" .
TypeSwitchGuard может включать короткое объявление переменной. Если используется такая форма, переменная объявляется в конце TypeSwitchCase в неявном блоке каждого условия. В условиях с регистром, перечисляющим ровно один тип, переменная имеет этот тип; в противном случае переменная имеет тип выражения в TypeSwitchGuard.
Вместо типа в case может использоваться заранее объявленный идентификатор nil; этот case выбирается, когда выражение в TypeSwitchGuard является значением интерфейса nil. Может существовать не более одного case nil.
Для выражения x типа interface{} используется следующий переключатель типов:
switch i := x.(type) {
case nil:
printString("x is nil") // type of i is type of x (interface{})
case int:
printInt(i) // type of i is int
case float64:
printFloat64(i) // type of i is float64
case func(int) float64:
printFunction(i) // type of i is func(int) float64
case bool, string:
printString("type is bool or string") // type of i is type of x (interface{})
default:
printString("don't know the type") // type of i is type of x (interface{})
}
или в нотации if else
v := x // x is evaluated exactly once
if v == nil {
i := v // type of i is type of x (interface{})
printString("x is nil")
} else if i, isInt := v.(int); isInt {
printInt(i) // type of i is int
} else if i, isFloat64 := v.(float64); isFloat64 {
printFloat64(i) // type of i is float64
} else if i, isFunc := v.(func(int) float64); isFunc {
printFunction(i) // type of i is func(int) float64
} else {
_, isBool := v.(bool)
_, isString := v.(string)
if isBool || isString {
i := v // type of i is type of x (interface{})
printString("type is bool or string")
} else {
i := v // type of i is type of x (interface{})
printString("don't know the type")
}
}
В качестве типа в case может использоваться параметр типа или общий тип. Если при инстанцировании этот тип окажется дублирующим другую запись в switch, то будет выбран первый подходящий case.
func f[P any](x any) int {
switch x.(type) {
case P:
return 0
case string:
return 1
case []P:
return 2
case []byte:
return 3
default:
return 4
}
}
var v1 = f[string]("foo") // v1 == 0
var v2 = f[byte]([]byte{}) // v2 == 2
Защите переключателя типов может предшествовать простой оператор, который выполняется до того, как будет оценена защита.
Оператор "fallthrough" не допускается в переключателе типов.
Операторы for
Оператор "for" задает повторное выполнение блока. Существует три формы: Итерация может управляться одним условием, предложением "for" или предложением "range".
ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
Condition = Expression .
Операторы For с одним условием
В своей простейшей форме оператор "for" задает повторное выполнение блока, пока булево условие оценивается как истинное. Условие оценивается перед каждой итерацией. Если условие отсутствует, оно эквивалентно булевому значению true.
Операторы for с предложением for
Оператор "for" с предложением ForClause также управляется условием, но дополнительно может содержать начальный и конечный оператор, например присваивание, оператор инкремента или декремента. Оператор InitStmt может быть коротким объявлением переменной, а оператор PostStmt - нет.
ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt = SimpleStmt .
PostStmt = SimpleStmt .
for i := 0; i < 10; i++ {
f(i)
}
Если он непустой, оператор init выполняется один раз перед оценкой условия для первой итерации; оператор post выполняется после каждого выполнения блока (и только если блок был выполнен). Любой элемент ForClause может быть пустым, но точка с запятой обязательна, если нет только условия. Если условие отсутствует, оно эквивалентно булевому значению true.
for cond { S() } is the same as for ; cond ; { S() }
for { S() } is the same as for true { S() }
Каждая итерация имеет свою отдельную объявленную переменную (или переменные) [Go 1.22]. Переменная, используемая в первой итерации, объявляется в операторе init. Переменная, используемая в каждой последующей итерации, объявляется неявно перед выполнением оператора post и инициализируется значением переменной предыдущей итерации в этот момент.
var prints []func()
for i := 0; i < 5; i++ {
prints = append(prints, func() { println(i) })
i++
}
for _, p := range prints {
p()
}
напечатает
До версии [Go 1.22] итерации использовали один набор переменных, а не отдельные переменные. В этом случае пример выше выводит
Операторы for с предложением range
Оператор "for" с предложением range выполняет итерацию по всем элементам массива, фрагмента, строки или карты, значениям, полученным по каналу, целочисленным значениям от нуля до верхнего предела [Go 1.22] или значениям, переданным в функцию yield функции итератора [Go 1.23]. Для каждой записи он присваивает значения итерации соответствующим переменным итерации, если они есть, а затем выполняет блок.
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
Выражение справа в предложении "range" называется выражением диапазона, его основной тип должен быть массивом, указателем на массив, фрагментом, строкой, картой, каналом, разрешающим операции приема, целым числом или функцией с определенной сигнатурой (см. ниже).
Как и в случае с присваиванием, операнды слева, если они присутствуют, должны быть адресными или индексными выражениями карты; они обозначают итерационные переменные.
- Если выражение диапазона является функцией, максимальное количество итерационных переменных зависит от сигнатуры функции.
- Если выражение диапазона является каналом или целым числом, допускается не более одной итерационной переменной; в противном случае их может быть до двух.
- Если последняя итерационная переменная является пустым идентификатором, то выражение range эквивалентно тому же выражению без этого идентификатора.
Выражение диапазона x оценивается перед началом цикла, за одним исключением: если присутствует не более одной итерационной переменной и x или len(x) постоянны, выражение диапазона не оценивается.
Вызовы функций слева оцениваются один раз за итерацию. Для каждой итерации значения итераций формируются следующим образом, если присутствуют соответствующие переменные итерации:
Range expression 1st value 2nd value
array or slice a [n]E, *[n]E, or []E index i int a[i] E
string s string type index i int see below rune
map m map[K]V key k K m[k] V
channel c chan E, <-chan E element e E
integer value n integer type, or untyped int value i see below
function, 0 values f func(func() bool)
function, 1 value f func(func(V) bool) value v V
function, 2 values f func(func(K, V) bool) key k K v V
- Для массива, указателя на массив или значения среза a значения итерации индекса генерируются в порядке возрастания, начиная с индекса элемента
0. Если присутствует не более одной переменной итерации, цикл range генерирует значения итерации от 0 до len(a)-1 и не индексирует сам массив или срез. Для нулевого среза количество итераций равно 0.
- Для строкового значения оператор
«range» итерирует кодовые позиции Unicode в строке, начиная с байтового индекса 0. При последующих итерациях значение индекса будет индексом первого байта последующих кодовых позиций, закодированных в UTF-8, в строке, а второе значение типа rune будет значением соответствующей кодовой позиции. Если при итерации встречается недопустимая последовательность UTF-8, второе значение будет 0xFFFD, замещающий символ Unicode, и следующая итерация продвинется на один байт в строке.
- Порядок итерации по картам не указан и не гарантируется, что он будет одинаковым от одной итерации к другой. Если запись карты, которая еще не была достигнута, удаляется во время итерации, соответствующее значение итерации не будет произведено. Если запись карты создается во время итерации, эта запись может быть произведена во время итерации или может быть пропущена. Выбор может варьироваться для каждой созданной записи и от одной итерации к другой. Если карта равна
nil, количество итераций равно 0.
- Для каналов произведенные значения итерации являются последовательными значениями, отправленными по каналу, пока канал не будет закрыт. Если канал равен
nil, выражение диапазона блокируется навсегда.
- Для целочисленного значения
n, где n является целочисленным типом или нетипизированной целочисленной константой, значения итерации от 0 до n-1 генерируются в порядке возрастания. Если n является целочисленным типом, значения итерации имеют тот же тип. В противном случае тип n определяется так, как если бы он был присвоен переменной итерации. В частности: если переменная итерации уже существует, тип значений итерации является типом переменной итерации, который должен быть целочисленным типом. В противном случае, если переменная итерации объявлена с помощью клаузулы «range» или отсутствует, тип значений итерации является типом по умолчанию для n. Если n <= 0, цикл не выполняет никаких итераций.
- Для функции
f итерация продолжается путем вызова f с новой синтезированной функцией yield в качестве аргумента. Если yield вызывается до возврата f, аргументы yield становятся значениями итерации для однократного выполнения тела цикла. После каждой последующей итерации цикла yield возвращает true и может быть вызван снова для продолжения цикла. Пока тело цикла не завершается, команда «range» будет продолжать генерировать значения итерации таким образом для каждого вызова yield, пока f не вернет результат. Если тело цикла завершается (например, оператором break), yield возвращает false и не должен вызываться снова.
Переменные итерации могут быть объявлены командой «range» с использованием формы короткого объявления переменной (:=). В этом случае их область действия — блок оператора «for», и каждая итерация имеет свои собственные новые переменные [Go 1.22] (см. также операторы «for» с ForClause). Переменные имеют типы своих соответствующих значений итерации.
Если переменные итерации явно не объявлены с помощью команды «range», они должны существовать ранее. В этом случае значения итерации присваиваются соответствующим переменным, как в операторе присваивания.
var testdata *struct {
a *[7]int
}
for i, _ := range testdata.a {
// testdata.a is never evaluated; len(testdata.a) is constant
// i ranges from 0 to 6
f(i)
}
var a [10]string
for i, s := range a {
// type of i is int
// type of s is string
// s == a[i]
g(i, s)
}
var key string
var val interface{} // element type of m is assignable to val
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]
var ch chan Work = producer()
for w := range ch {
doWork(w)
}
// empty a channel
for range ch {}
// call f(0), f(1), ... f(9)
for i := range 10 {
// type of i is int (default type for untyped constant 10)
f(i)
}
// invalid: 256 cannot be assigned to uint8
var u uint8
for u = range 256 {
}
// invalid: 1e3 is a floating-point constant
for range 1e3 {
}
// fibo generates the Fibonacci sequence
fibo := func(yield func(x int) bool) {
f0, f1 := 0, 1
for yield(f0) {
f0, f1 = f1, f0+f1
}
}
// print the Fibonacci numbers below 1000:
for x := range fibo {
if x >= 1000 {
break
}
fmt.Printf("%d ", x)
}
// output: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
// iteration support for a recursive tree data structure
type Tree[K cmp.Ordered, V any] struct {
left, right *Tree[K, V]
key K
value V
}
func (t *Tree[K, V]) walk(yield func(key K, val V) bool) bool {
return t == nil || t.left.walk(yield) && yield(t.key, t.value) && t.right.walk(yield)
}
func (t *Tree[K, V]) Walk(yield func(key K, val V) bool) {
t.walk(yield)
}
// walk tree t in-order
var t Tree[string, int]
for k, v := range t.Walk {
// process k, v
}
Операторы Go
Оператор "go" запускает выполнение вызова функции как независимого параллельного потока управления, или goroutine, в том же адресном пространстве.
GoStmt = «go» Expression .
Выражение должно быть вызовом функции или метода; оно не может быть заключено в скобки. Вызовы встроенных функций ограничены, как и для выражений-операторов.
Значение функции и параметры оцениваются как обычно в вызывающей goroutine, но в отличие от обычного вызова, выполнение программы не ожидает завершения вызванной функции. Вместо этого функция начинает выполняться независимо в новой goroutine. Когда функция заканчивается, ее goroutine также заканчивается. Если функция имеет какие-либо возвращаемые значения, они отбрасываются по завершении функции.
go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)
Операторы select
Оператор select выбирает, какая из множества возможных операций отправки или получения будет выполнена. Он похож на оператор "switch", но все случаи относятся к коммуникационным операциям.
SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr = Expression .
В случае с RecvStmt результат RecvExpr может быть присвоен одной или двум переменным, которые могут быть объявлены с помощью короткого объявления переменной. RecvExpr должен быть операцией приема (возможно в скобках). Может быть не более одного случая по умолчанию, и он может появляться в любом месте списка случаев.
Выполнение оператора «select» происходит в несколько этапов:
- Для всех случаев в операторе канал-операнды операций приема и канал и выражения правой части операторов отправки оцениваются ровно один раз, в порядке источника, при входе в оператор
«select». Результатом является набор каналов для приема или отправки и соответствующие значения для отправки. Любые побочные эффекты в этой оценке будут происходить независимо от того, какая (если таковая имеется) операция связи выбрана для выполнения. Выражения в левой части RecvStmt с коротким объявлением переменной или присваиванием еще не оцениваются.
- Если одна или несколько коммуникаций могут быть выполнены, выбирается одна из них с помощью единого псевдослучайного выбора. В противном случае, если есть случай по умолчанию, выбирается этот случай. Если случая по умолчанию нет, оператор
«select» блокируется до тех пор, пока хотя бы одна из коммуникаций не сможет быть выполнена.
- Если выбранный случай не является случаем по умолчанию, выполняется соответствующая операция связи.
- Если выбранный случай является
RecvStmt с коротким объявлением переменной или присваиванием, выражения в левой части оцениваются, и полученное значение (или значения) присваиваются.
- Выполняется список операторов выбранного случая.
Поскольку связь по нулевым каналам никогда не может быть выполнена, оператор select, содержащий только нулевые каналы и не имеющий случая по умолчанию, блокируется навсегда.
var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
print("received ", i1, " from c1\n")
case c2 <- i2:
print("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
print("received ", i3, " from c3\n")
} else {
print("c3 is closed\n")
}
case a[f()] = <-c4:
// same as:
// case t := <-c4
// a[f()] = t
default:
print("no communication\n")
}
for { // send random sequence of bits to c
select {
case c <- 0: // note: no statement, no fallthrough, no folding of cases
case c <- 1:
}
}
select {} // block forever
Операторы возврата
Оператор "return" в функции F завершает выполнение F и, по желанию, предоставляет одно или несколько значений результата. Любые функции, отложенные F, выполняются до того, как F вернется к своему вызывающему устройству.
ReturnStmt = "return" [ ExpressionList ] .
В функции без типа результата оператор "return" не должен указывать никаких значений результата.
func noResult() {
return
}
Существует три способа возврата значений из функции с типом результата:
- Возвращаемое значение или значения могут быть явно перечислены в операторе
"return". Каждое выражение должно быть однозначным и присваиваться соответствующему элементу типа результата функции.
func simpleF() int {
return 2
}
func complexF1() (re float64, im float64) {
return -7.0, -4.0
}
- Список выражений в операторе
"return" может быть одним вызовом многозначной функции. Эффект будет таким, как если бы каждое значение, возвращаемое функцией, присваивалось временной переменной с типом соответствующего значения, а затем следовал оператор "return", перечисляющий эти переменные, и в этот момент применялись бы правила предыдущего случая.
func complexF2() (re float64, im float64) {
return complexF1()
}
- Список выражений может быть пустым, если тип результата функции задает имена для параметров результата. Параметры результата действуют как обычные локальные переменные, и функция может присваивать им значения по мере необходимости. Оператор
"return" возвращает значения этих переменных.
func complexF3() (re float64, im float64) {
re = 7.0
im = 4.0
return
}
func (devnull) Write(p []byte) (n int, _ error) {
n = len(p)
return
}
Независимо от того, как они объявлены, все значения результатов инициализируются нулевыми значениями для своего типа при входе в функцию. Оператор "return", указывающий результаты, устанавливает параметры результата до выполнения любых отложенных функций.
Ограничение на реализацию: Компилятор может запретить пустой список выражений в операторе "return", если в месте возврата в области видимости находится другая сущность (константа, тип или переменная) с тем же именем, что и параметр результата.
func f(n int) (res int, err error) {
if _, err := f(n-1); err != nil {
return // invalid return statement: err is shadowed
}
return
}
Операторы break
Оператор break завершает выполнение самого внутреннего оператора "for", "switch" или "select" внутри одной и той же функции.
BreakStmt = "break" [ Label ] .
Если есть метка, то она должна быть меткой вложенного оператора "for", "switch" или "select", и именно он завершает выполнение.
OuterLoop:
for i = 0; i < n; i++ {
for j = 0; j < m; j++ {
switch a[i][j] {
case nil:
state = Error
break OuterLoop
case item:
state = Found
break OuterLoop
}
}
}
Операторы continue
Оператор "continue" начинает следующую итерацию внутреннего цикла for, переводя управление в конец блока цикла. Цикл "for" должен находиться внутри одной и той же функции.
ContinueStmt = "continue" [ Label ] .
Если есть метка, то она должна быть меткой вложенного оператора "for", и именно он продолжает выполнение.
RowLoop:
for y, row := range rows {
for x, data := range row {
if data == endOfRow {
continue RowLoop
}
row[x] = data + bias(x, y)
}
}
Операторы goto
Оператор "goto" передает управление оператору с соответствующей меткой в той же функции.
GotoStmt = "goto" Label .
Выполнение оператора "goto" не должно приводить к появлению в области видимости переменных, которые еще не были в области видимости в момент перехода к нему.
Например:
является ошибочным, поскольку переход к метке L пропускает создание v.
Оператор "goto" вне блока не может перейти к метке внутри этого блока.
Например:
if n%2 == 1 {
goto L1
}
for n > 0 {
f()
n--
L1:
f()
n--
}
является ошибочным, поскольку метка L1 находится внутри блока оператора "for", а goto - нет.
Операторы перехода (fallthrough)
Оператор “fallthrough” передает управление первому оператору следующего выражения case в выражении "switch". Он может использоваться только в качестве последнего непустого оператора в таком выражении.
FallthroughStmt = "fallthrough" .
Операторы defer
Оператор «defer» вызывает функцию, выполнение которой откладывается до момента возврата окружающей функции, либо потому, что окружающая функция выполнила оператор return, достигла конца своего тела, либо потому, что соответствующая goroutine находится в состоянии паники.
DeferStmt = «defer» Expression .
Выражение должно быть вызовом функции или метода; оно не может быть заключено в скобки. Вызовы встроенных функций ограничены, как и для операторов выражений.
Каждый раз, когда выполняется оператор «defer», значение функции и параметры вызова оцениваются как обычно и сохраняются заново, но сама функция не вызывается. Вместо этого отложенные функции вызываются непосредственно перед возвратом окружающей функции в обратном порядке, в котором они были отложены. То есть, если окружающая функция возвращается с помощью явного оператора return, отложенные функции выполняются после того, как все параметры результата установлены этим оператором return, но до того, как функция возвращается к вызывающему ее. Если значение отложенной функции вычисляется как nil, при вызове функции происходит сбой выполнения, а не при выполнении оператора «defer».
Например, если отложенная функция является функциональным литералом, а окружающая функция имеет именованные параметры результата, которые находятся в области действия литерала, отложенная функция может получить доступ к параметрам результата и изменить их до того, как они будут возвращены. Если отложенная функция имеет какие-либо возвращаемые значения, они отбрасываются по завершении функции. (См. также раздел об обработке паники.)
lock(l)
defer unlock(l) // unlocking happens before surrounding function returns
// prints 3 2 1 0 before surrounding function returns
for i := 0; i <= 3; i++ {
defer fmt.Print(i)
}
// f returns 42
func f() (result int) {
defer func() {
// result is accessed after it was set to 6 by the return statement
result *= 7
}()
return 6
}
Built-in функции
Встроенные функции объявляются заранее. Они вызываются как любая другая функция, но некоторые из них принимают в качестве первого аргумента не выражение, а тип.
append, cap, close, complex, copy, delete, imag, len, make, new, panic, print, println, real, recover
Встроенные функции не имеют стандартных типов Go, поэтому они могут появляться только в выражениях вызова; их нельзя использовать в качестве значений функций.
Добавление append к срезам и копирование copy срезов
Встроенные функции append и copy помогают выполнять распространенные операции со срезами. Для обеих функций результат не зависит от того, перекрывается ли память, на которую ссылаются аргументы.
Вариативная функция append добавляет ноль или более значений x к срезу s и возвращает результирующий срез того же типа, что и s. Основной тип s должен быть срезом типа []E. Значения x передаются в параметр типа ...E, и к ним применяются соответствующие правила передачи параметров. В качестве особого случая, если основной тип s - []byte, append также принимает второй аргумент с основным типом bytestring, за которым следует .... Эта форма добавляет байты байтового фрагмента или строки.
append(s S, x ...E) S // core type of S is []E
емкость s недостаточно велика, чтобы вместить дополнительные значения, append выделяет новый, достаточно большой базовый массив, в который помещаются как существующие элементы среза, так и дополнительные значения. В противном случае append повторно использует базовый массив.
s0 := []int{0, 0}
s1 := append(s0, 2) // append a single element s1 is []int{0, 0, 2}
s2 := append(s1, 3, 5, 7) // append multiple elements s2 is []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...) // append a slice s3 is []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...) // append overlapping slice s4 is []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
var t []interface{}
t = append(t, 42, 3.1415, "foo") // t is []interface{}{42, 3.1415, "foo"}
var b []byte
b = append(b, "bar"...) // append string contents b is []byte{'b', 'a', 'r' }
Функция copy копирует элементы среза из источника src в пункт назначения dst и возвращает количество скопированных элементов. Типы ядра обоих аргументов должны быть срезами с одинаковым типом элемента. Количество скопированных элементов равно минимальному из len(src) и len(dst). В качестве особого случая, если основной тип места назначения - []byte, copy также принимает аргумент источника с основным типом bytestring. Эта форма копирует байты из байтового фрагмента или строки в байтовый фрагмент.
copy(dst, src []T) int
copy(dst []byte, src string) int
Например:
var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:]) // n1 == 6, s is []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:]) // n2 == 4, s is []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!") // n3 == 5, b is []byte("Hello")
Clear
Встроенная функция clear принимает аргумент типа map, slice или параметр type и удаляет или обнуляет все элементы [Go 1.21].
Вызов Тип аргумента Результат
clear(m) map[K]T удаляет все элементы, в результате чего получается пустая карта
(len(m) == 0)
clear(s) []T устанавливает все элементы длиной до
s в нулевое значение T
clear(t) параметр типа см. ниже
Если тип аргумента clear является параметром типа, то все типы в его наборе типов должны быть картами или срезами, и clear выполняет операцию, соответствующую фактическому типу аргумента.
Если map или slice равен nil, clear не выполняется.
Close
Для аргумента ch с типом ядра, являющимся каналом, встроенная функция close фиксирует, что по этому каналу больше не будут передаваться значения. Ошибкой будет, если ch является каналом только для приема. Отправка в закрытый канал или его закрытие вызывают панику во время выполнения. Закрытие канала nil также вызывает панику во время выполнения. После вызова close и после получения всех ранее отправленных значений операции приема возвращают нулевое значение для типа канала без блокировки. Многозначная операция receive возвращает полученное значение вместе с указанием того, закрыт ли канал.
Работа с комплексными числами
Три функции собирают и разбирают комплексные числа. Встроенная функция complex строит комплексное значение из действительной и мнимой частей с плавающей запятой, а real и imag извлекают действительную и мнимую части комплексного значения.
complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT
Тип аргументов и возвращаемого значения соответствуют друг другу. Для complex оба аргумента должны быть одного типа с плавающей точкой, а возвращаемое значение - это комплексный тип с соответствующими составляющими с плавающей точкой: complex64 для аргументов float32 и complex128 для аргументов float64.
Если один из аргументов оценивается как нетипизированная константа, он сначала неявно преобразуется к типу другого аргумента. Если оба аргумента оцениваются как нетипизированные константы, они должны быть некомплексными числами или их мнимые части должны быть равны нулю, а возвращаемое значение функции - нетипизированная комплексная константа.
Для real и imag аргумент должен быть комплексного типа, а возвращаемый тип - соответствующий тип с плавающей точкой: float32 для аргумента complex64 и float64 для аргумента complex128. Если аргумент оценивается в нетипизированную константу, он должен быть числом, а возвращаемое значение функции - нетипизированная константа с плавающей точкой.
Функции real и imag вместе образуют обратную функцию complex, поэтому для значения z комплексного типа Z, z == Z(complex(real(z), imag(z))).
Если все операнды этих функций - константы, то возвращаемое значение - константа.
var a = complex(2, -2) // complex128
const b = complex(1.0, -1.4) // нетипизированная комплексная константа 1 - 1.4i
x := float32(math.Cos(math.Pi/2)) // float32
var c64 = complex(5, -x) // complex64
var s int = complex(1, 0) // нетипизированная комплексная константа 1 + 0i может быть преобразована в int
_ = complex(1, 2<<s) // illegal: 2 предполагает тип с плавающей точкой, нельзя сдвигать
var rl = real(c64) // float32
var im = imag(a) // float64
const c = imag(b) // нетипизированная константа -1.4
_ = imag(3 << s) // illegal: 3 предполагает комплексный тип, сдвиг невозможен
Аргументы типа parameter type недопустимы.
Удаление элементов карты (map)
Встроенная функция delete удаляет элемент с ключом k из карты m. Значение k должно быть присваиваемым типу ключа m.
delete(m, k) // удалить элемент m[k] из карты m
Если тип m - это параметр типа, то все типы в этом наборе типов должны быть картами, и все они должны иметь одинаковые типы ключей.
Если map m равен nil или элемент m[k] не существует, delete не выполняется.
Длина и емкость
Встроенные функции len и cap принимают аргументы различных типов и возвращают результат типа int. Реализация гарантирует, что результат всегда помещается в int.
Вызов Тип аргумента Результат
len(s) тип string длина строки в байтах
[n]T, *[n]T длина массива (== n)
[]T длина фрагмента
map[K]T длина карты (количество определенных ключей)
chan T количество элементов в очереди буфера канала
тип параметра см. ниже
cap(s) [n]T, *[n]T длина массива (== n)
[]T емкость фрагмента
chan T емкость буфера канала
тип параметра см. ниже
Если тип аргумента является типовым параметром P, вызов len(e) (или cap(e) соответственно) должен быть действительным для каждого типа в наборе типов P. Результатом является длина (или емкость, соответственно) аргумента, тип которого соответствует типовому аргументу, с которым был создан экземпляр P.
Емкость фрагмента — это количество элементов, для которых выделено место в базовом массиве. В любой момент времени выполняется следующее соотношение:
Длина нулевого фрагмента, карты или канала равна 0. Емкость нулевого фрагмента или канала равна 0.
Выражение len(s) является константой, если s является строковой константой. Выражения len(s) и cap(s) являются константами, если тип s является массивом или указателем на массив, а выражение s не содержит приёмов канала или (неконстантных) вызовов функций; в этом случае s не вычисляется. В противном случае вызовы len и cap не являются константами, и s вычисляется.
const (
c1 = imag(2i) // imag(2i) = 2.0 is a constant
c2 = len([10]float64{2}) // [10]float64{2} contains no function calls
c3 = len([10]float64{c1}) // [10]float64{c1} contains no function calls
c4 = len([10]float64{imag(2i)}) // imag(2i) is a constant and no function call is issued
c5 = len([10]float64{imag(z)}) // invalid: imag(z) is a (non-constant) function call
)
var z complex128
Make (Создание срезов, карт и каналов)
Встроенная функция make принимает тип T, за которым опционально следует список выражений, специфичных для данного типа. Основной тип T должен быть срезом, картой или каналом. Она возвращает значение типа T (не *T) Память инициализируется, как описано в разделе об начальных значениях.
Вызов Основной тип Результат
make(T, n) slice срез типа T с длиной n и емкостью n
make(T, n, m) slice срез типа T с длиной n и емкостью m
make(T) map карта типа T
make(T, n) map карта типа T с начальным пространством примерно для n элементов
make(T) channel небуферизованный канал типа T
make(T, n) канал буферизованный канал типа T, размер буфера n
Каждый из аргументов размера n и m должен быть целочисленного типа, иметь набор типов, содержащий только целочисленные типы, или быть константой без типа. Аргумент постоянного размера должен быть неотрицательным и представляться значением типа int; если это константа без типа, ей присваивается тип int. Если указаны оба аргумента n и m и они являются константами, то n не должен быть больше m. Для срезов и каналов, если n является отрицательным или больше m во время выполнения, возникает паника во время выполнения.
s := make([]int, 10, 100) // срез с len(s) == 10, cap(s) == 100
s := make([]int, 1e3) // срез с len(s) == cap(s) == 1000
s := make([]int, 1<<63) // недопустимо: len(s) не может быть представлено значением типа int
s := make([]int, 10, 0) // недопустимо: len(s) > cap(s)
c := make(chan int, 10) // канал с размером буфера 10
m := make(map[string]int, 100) // карта с начальным пространством примерно для 100 элементов
Вызов make с типом карты и подсказкой размера n создаст карту с начальным пространством для хранения n элементов карты. Точное поведение зависит от реализации.
Min и max
Встроенные функции min и max вычисляют соответственно наименьшее или наибольшее значение из фиксированного числа аргументов упорядоченных типов. Должно быть как минимум один аргумент [Go 1.21].
Применяются те же правила типов, что и для операторов: для упорядоченных аргументов x и y min(x , y) является действительным, если x + y является действительным, и тип min(x, y) является типом x + y (аналогично для max). Если все аргументы являются константами, результат является константой.
var x, y int
m := min(x) // m == x
m := min(x, y) // m — меньшее из x и y
m := max(x, y, 10) // m — большее из x и y, но не меньше 10
c := max(1, 2.0, 10) // c == 10.0 (тип с плавающей запятой)
f := max(0, float32(x)) // тип f — float32
var s []string
_ = min(s...) // недопустимо: аргументы slice не допускаются
t := max(«», «foo», „bar“) // t == «foo» (тип string)
Для числовых аргументов, предполагая, что все NaN равны, min и max являются коммутативными и ассоциативными:
min(x, y) == min(y, x)
min(x, y, z) == min(min(x, y), z) == min(x, min(y, z))
Для аргументов с плавающей запятой отрицательный ноль, NaN и бесконечность применяются следующие правила:
x y min(x, y) max(x, y)
-0.0 0.0 -0.0 0.0 // отрицательный ноль меньше (неотрицательного) нуля
-Inf y -Inf y // отрицательная бесконечность меньше любого другого числа
+Inf y y +Inf // положительная бесконечность больше любого другого числа
NaN y NaN NaN // если любой аргумент является NaN, результат будет NaN
Для строковых аргументов результатом для min будет первый аргумент с наименьшим (или для max — наибольшим) значением, сравниваемым лексически побайтно:
min(x, y) == if x <= y then x else y
min(x, y, z) == min(min(x, y), z)
New (Выделение памяти)
Встроенная функция new принимает тип T, выделяет память для переменной этого типа во время выполнения и возвращает значение типа *T, указывающее на нее. Переменная инициализируется, как описано в разделе об начальных значениях.
Например
type S struct { a int; b float64 }
new(S)
выделяет память для переменной типа S, инициализирует ее (a=0, b=0.0) и возвращает значение типа *S, содержащее адрес этого места.
Обработка панических состояний
Две встроенные функции, panic и recover, помогают в отчетности и обработке панических состояний во время выполнения и определенных программой условий ошибок.
func panic(interface{})
func recover() interface{}
Во время выполнения функции F явный вызов panic или паническое состояние во время выполнения прерывает выполнение F. Любые функции, отложенные F, затем выполняются как обычно. Далее выполняются все отложенные функции, запущенные вызывающим F, и так далее, вплоть до всех отложенных функций верхнего уровня в выполняющейся goroutine. На этом программа завершается и сообщается об ошибке, включая значение аргумента panic. Эта последовательность завершения называется паникой.
panic(42)
panic(«unreachable»)
panic(Error(«cannot parse»))
Функция recover позволяет программе управлять поведением паникующего goroutine. Предположим, что функция G откладывает функцию D, которая вызывает recover, и происходит паника в функции того же goroutine, в котором выполняется G. Когда выполнение отложенных функций достигает D, возвращаемое значение вызова D для recover будет значением, переданным вызову panic. Если D возвращается нормально, не запуская новую панику, последовательность паники останавливается. В этом случае состояние функций, вызванных между G и вызовом panic, отбрасывается, и возобновляется нормальное выполнение. Затем выполняются все функции, отложенные G перед D, и выполнение G завершается возвратом к вызывающему.
Возвращаемое значение recover равно nil, если goroutine не находится в состоянии паники или recover не был вызван непосредственно отложенной функцией. И наоборот, если goroutine находится в состоянии паники и recover был вызван непосредственно отложенной функцией, возвращаемое значение recover гарантированно не будет nil. Чтобы обеспечить это, вызов panic с нулевым значением интерфейса (или нетипизированным nil) вызывает панику во время выполнения.
Функция protect в примере ниже вызывает функцию-аргумент g и защищает вызывающих от паники во время выполнения, вызванной g.
func protect(g func()) {
defer func() {
log.Println(«done») // Println выполняется нормально, даже если происходит паника
if x := recover(); x != nil {
log.Printf(«run time panic: %v», x)
}
}()
log.Println(«start»)
g()
}
Загрузка
Текущие реализации предоставляют несколько встроенных функций, полезных во время загрузки. Эти функции задокументированы для полноты, но их сохранение в языке не гарантируется. Они не возвращают результат.
Функция Поведение
print выводит все аргументы; форматирование аргументов зависит от реализации
println как print, но выводит пробелы между аргументами и новую строку в конце
Ограничение реализации: print и println не обязательно должны принимать произвольные типы аргументов, но должна поддерживаться печать типов boolean, numeric и string.
Packages (Пакеты)
Программы Go создаются путем связывания пакетов. Пакет, в свою очередь, состоит из одного или нескольких исходных файлов, которые вместе объявляют константы, типы, переменные и функции, принадлежащие пакету и доступные во всех файлах того же пакета. Эти элементы могут быть экспортированы и использованы в другом пакете.
Организация исходных файлов
Каждый исходный файл состоит из пакетного предложения, определяющего пакет, к которому он принадлежит, за которым следует, возможно, пустой набор деклараций импорта, объявляющих пакеты, содержимое которых он хочет использовать, а за ним следует, возможно, пустой набор деклараций функций, типов, переменных и констант.
SourceFile = PackageClause «;» { ImportDecl «;» } { TopLevelDecl «;» } .
Пакетное предложение
Пакетное объявление начинает каждый исходный файл и определяет пакет, к которому принадлежит файл.
PackageClause = «package» PackageName .
PackageName = identifier .
PackageName не должен быть пустым идентификатором.
Набор файлов, имеющих один и тот же PackageName, образует реализацию пакета. Реализация может требовать, чтобы все исходные файлы пакета находились в одном каталоге.
Декларации импорта
Декларация импорта указывает, что исходный файл, содержащий декларацию, зависит от функциональности импортируемого пакета и обеспечивает доступ к экспортируемым идентификаторам этого пакета. Импорт указывает идентификатор (PackageName), который будет использоваться для доступа, и ImportPath, который указывает пакет, который будет импортирован.
ImportDecl = «import» ( ImportSpec | «(» { ImportSpec «;» } «)» ) .
ImportSpec = [ «.» | PackageName ] ImportPath .
ImportPath = string_lit .
PackageName используется в квалифицированных идентификаторах для доступа к экспортируемым идентификаторам пакета в импортирующем исходном файле. Он объявляется в блоке файла. Если PackageName опущен, по умолчанию используется идентификатор, указанный в объявлении package импортируемого пакета. Если вместо имени явно указана точка (.), все экспортированные идентификаторы пакета, объявленные в блоке пакета этого пакета, будут объявлены в блоке файла импортирующего исходного файла и должны быть доступны без квалификатора.
Интерпретация ImportPath зависит от реализации, но обычно это подстрока полного имени файла скомпилированного пакета и может быть относительной по отношению к репозиторию установленных пакетов.
Ограничение реализации: компилятор может ограничить ImportPaths непустыми строками, использующими только символы, принадлежащие общим категориям Unicode L, M, N, P и S (графические символы без пробелов), а также может исключить символы !"#$%&'()*,:;<=>?[\]^`{\|} и символ замены Unicode U+FFFD.
Рассмотрим скомпилированный пакет, содержащий объявление package math, которая экспортирует функцию Sin, и установим скомпилированный пакет в файл, идентифицируемый как «lib/math». В этой таблице показано, как Sin доступен в файлах, которые импортируют пакет после различных типов деклараций импорта.
Декларация импорта Локальное имя Sin
import "lib/math" math.Sin
import m "lib/math" m.Sin
import . "lib/math" Sin
Декларация импорта объявляет зависимость между импортирующим и импортируемым пакетом. Не допускается, чтобы пакет импортировал сам себя, прямо или косвенно, или напрямую импортировал пакет, не ссылаясь на какие-либо из его экспортируемых идентификаторов. Чтобы импортировать пакет исключительно для его побочных эффектов (инициализации), используйте пустой идентификатор в качестве явного имени пакета:
Пример package
Полный пакет Go, который реализует параллельное сито простых чисел.
package main
import «fmt»
// Отправить последовательность 2, 3, 4, … в канал „ch“.
func generate(ch chan<- int) {
for i := 2; ; i++ {
ch <- i // Отправить „i“ в канал „ch“.
}
}
// Скопировать значения из канала „src“ в канал „dst“,
// удалив те, которые делятся на „prime“.
func filter(src <-chan int, dst chan<- int, prime int) {
for i := range src { // Цикл по значениям, полученным из „src“.
if i%prime != 0 {
dst <- i // Отправить „i“ в канал „dst“.
}
}
}
// Простое сито: последовательный фильтр обрабатывает вместе.
func sieve() {
ch := make(chan int) // Создать новый канал.
go generate(ch) // Запустить generate() как подпроцесс.
for {
prime := <-ch
fmt.Print(prime, «\n»)
ch1 := make(chan int)
go filter(ch, ch1, prime)
ch = ch1
}
}
func main() {
sieve()
}
Инициализация и выполнение программы
Нулевое значение
Когда для переменной выделяется память, либо через объявление, либо через вызов new, либо когда создается новое значение, либо через составной литерал, либо через вызов make, и явная инициализация не предоставляется, переменной или значению присваивается значение по умолчанию. Каждый элемент такой переменной или значения устанавливается в нулевое значение для своего типа: false для булевых значений, 0 для числовых типов, "" для строк и nil для указателей, функций, интерфейсов, срезов, каналов и карт. Эта инициализация выполняется рекурсивно, поэтому, например, каждый элемент массива структур будет иметь обнуленные поля, если значение не указано.
Эти два простых объявления эквивалентны:
После
type T struct { i int; f float64; next *T }
t := new(T)
имеет место следующее:
t.i == 0
t.f == 0.0
t.next == nil
То же самое будет верно и после
Инициализация пакета
Внутри пакета инициализация переменных на уровне пакета происходит пошагово, причем на каждом шаге выбирается переменная, которая находится в начале порядка объявления и не имеет зависимостей от неинициализированных переменных.
Точнее, переменная на уровне пакета считается готовой к инициализации, если она еще не инициализирована и либо не имеет выражения инициализации, либо ее выражение инициализации не зависит от неинициализированных переменных. Инициализация происходит путем повторной инициализации следующей переменной на уровне пакета, которая является самой ранней в порядке объявления и готова к инициализации, пока не останется переменных, готовых к инициализации.
Если по окончании этого процесса какие-либо переменные остаются неинициализированными, эти переменные являются частью одного или нескольких циклов инициализации, и программа является недействительной.
Несколько переменных в левой части объявления переменной, инициализируемых одним (многозначным) выражением в правой части, инициализируются вместе: если какая-либо из переменных в левой части инициализирована, все эти переменные инициализируются на одном и том же шаге.
var x = a
var a, b = f() // a и b инициализируются вместе, перед инициализацией x
Для целей инициализации пакета пустые переменные обрабатываются как любые другие переменные в объявлениях.
Порядок объявления переменных, объявленных в нескольких файлах, определяется порядком, в котором файлы представлены компилятору: переменные, объявленные в первом файле, объявляются перед любыми переменными, объявленными во втором файле, и так далее. Для обеспечения воспроизводимого поведения инициализации рекомендуется, чтобы системы сборки представляли компилятору несколько файлов, принадлежащих одному пакету, в порядке лексического имени файла.
Анализ зависимостей не зависит от фактических значений переменных, а только от лексических ссылок на них в исходном коде, анализируемых транзитивно. Например, если выражение инициализации переменной x ссылается на функцию, тело которой ссылается на переменную y, то x зависит от y. В частности:
- Ссылка на переменную или функцию — это идентификатор, обозначающий эту переменную или функцию.
- Ссылка на метод
m — это значение метода или выражение метода в форме t.m, где (статический) тип t не является типом интерфейса, а метод m находится в наборе методов t. Не имеет значения, вызывается ли результирующее значение функции t.m.
- Переменная, функция или метод
x зависит от переменной y, если выражение инициализации или тело x (для функций и методов) содержит ссылку на y или на функцию или метод, которые зависят от y.
Например, при следующих объявлениях
var (
a = c + b // == 9
b = f() // == 4
c = f() // == 5
d = 3 // == 5 после завершения инициализации)
func f() int {
d++
return d
}
порядок инициализации будет d, b, c, a. Обратите внимание, что порядок подвыражений в выражениях инициализации не имеет значения: a = c + b и a = b + c приводят к одинаковому порядку инициализации в этом примере.
Анализ зависимостей выполняется для каждого пакета; учитываются только ссылки на переменные, функции и методы (не интерфейсы), объявленные в текущем пакете. Если между переменными существуют другие, скрытые зависимости данных, порядок инициализации между этими переменными не определён.
Например, при следующих объявлениях
var x = I(T{}).ab() // x имеет не обнаруженную, скрытую зависимость от a и b
var _ = sideEffect() // не связан с x, a или b
var a = b
var b = 42
type I interface { ab() []int }
type T struct{}
func (T) ab() []int { return []int{a, b} }
переменная a будет инициализирована после b, но не определено, будет ли x инициализирована до b, между b и a или после a, и, следовательно, также не определено, в какой момент будет вызвана sideEffect() (до или после инициализации x).
Переменные также могут инициализироваться с помощью функций с именем init, объявленных в блоке пакета, без аргументов и без параметров результата.
В одном пакете может быть определено несколько таких функций, даже в одном исходном файле. В блоке пакетов идентификатор init может использоваться только для объявления функций init, но сам идентификатор не объявляется. Таким образом, на функции init нельзя ссылаться нигде в программе.
Весь пакет инициализируется путем присвоения начальных значений всем его переменным уровня пакета с последующим вызовом всех функций init в том порядке, в котором они появляются в исходном тексте, возможно, в нескольких файлах, как это представлено компилятору.
Инициализация программы
Пакеты полной программы инициализируются пошагово, по одному пакету за раз. Если пакет имеет импорты, импортируемые пакеты инициализируются перед инициализацией самого пакета. Если несколько пакетов импортируют один пакет, импортируемый пакет будет инициализирован только один раз. Импорт пакетов по конструкции гарантирует, что не может быть циклических зависимостей инициализации. Точнее:
Имея список всех пакетов, отсортированный по пути импорта, на каждом шаге инициализируется первый неинициализированный пакет в списке, для которого все импортированные пакеты (если таковые имеются) уже инициализированы. Этот шаг повторяется до тех пор, пока не будут инициализированы все пакеты.
Инициализация пакета — инициализация переменных и вызов функций init — происходит в одной goroutine, последовательно, по одному пакету за раз. Функция init может запускать другие goroutines, которые могут выполняться одновременно с кодом инициализации. Однако инициализация всегда последовательно выполняет функции init: она не вызовет следующую, пока предыдущая не вернется.
Выполнение программы
Полная программа создается путем связывания одного неимпортированного пакета, называемого основным пакетом, со всеми пакетами, которые он импортирует, транзитивно.
Основной пакет должен иметь имя main и объявлять функцию main, которая не принимает аргументов и не возвращает значения.
Выполнение программы начинается с инициализации программы, а затем вызова функции main в пакете main. Когда этот вызов функции возвращается, программа завершается. Она не ждет завершения других (не основных) горутин.
Errors (Ошибки)
Объявленный тип error определяется как
type error interface {
Error() string
}
Это обычный интерфейс для представления состояния ошибки, причем значение nil означает отсутствие ошибки. Например, может быть определена функция для чтения данных из файла:
func Read(f *File, b []byte) (n int, err error)
Паника во время выполнения
Ошибки выполнения, такие как попытка индексировать массив за пределами границ, вызывают панику во время выполнения, эквивалентную вызову встроенной функции panic со значением типа runtime.Error, определяемого интерфейсом реализации. Этот тип удовлетворяет заранее объявленному интерфейсному типу error. Точные значения ошибок, которые представляют различные условия ошибки во время выполнения, не определены.
package runtime
type Error interface {
error
// и, возможно, другие методы
}
Системные аспекты
Пакет unsafe
Встроенный пакет unsafe, известный компилятору и доступный через путь импорта "unsafe", предоставляет средства для низкоуровневого программирования, включая операции, нарушающие систему типов. Пакет, использующий unsafe, должен быть проверен вручную на безопасность типов и может быть не переносимым. Пакет предоставляет следующий интерфейс:
package unsafe
type ArbitraryType int // shorthand for an arbitrary Go type; it is not a real type
type Pointer *ArbitraryType
func Alignof(variable ArbitraryType) uintptr
func Offsetof(selector ArbitraryType) uintptr
func Sizeof(variable ArbitraryType) uintptr
type IntegerType int // shorthand for an integer type; it is not a real type
func Add(ptr Pointer, len IntegerType) Pointer
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte
Pointer - это тип указателя, но значение Pointer не может быть разыменовано. Любой указатель или значение основного типа uintptr может быть преобразован в основной тип Pointer и наоборот. Эффект преобразования между Pointer и uintptr определяется реализацией.
var f float64
bits = *(*uint64)(unsafe.Pointer(&f))
type ptr unsafe.Pointer
bits = *(*uint64)(ptr(&f))
func f[P ~*B, B any](p P) uintptr {
return uintptr(unsafe.Pointer(p))
}
var p ptr = nil
Функции Alignof и Sizeof принимают выражение x любого типа и возвращают выравнивание или размер, соответственно, гипотетической переменной v, как если бы v была объявлена через var v = x.
Функция Offsetof принимает (возможно, заключенный в скобки) селектор s.f, обозначающий поле f структуры, обозначенной s или *s, и возвращает смещение поля в байтах относительно адреса структуры. Если f является встроенным полем, оно должно быть доступно без косвенных указателей через поля структуры. Для структуры s с полем f:
uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))
Архитектуры компьютеров могут требовать выравнивания адресов памяти, то есть адреса переменной должны быть кратными коэффициенту, выравниванию типа переменной. Функция Alignof принимает выражение, обозначающее переменную любого типа, и возвращает выравнивание (типа) переменной в байтах. Для переменной x:
uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0
Переменная типа T имеет переменный размер, если T является параметром типа или если это тип массива или структуры, содержащий элементы или поля переменного размера. В противном случае размер является постоянным. Вызовы Alignof, Offsetof и Sizeof являются константами времени компиляции типа uintptr, если их аргументы (или структура s в выражении селектора s.f для Offsetof) являются типами постоянного размера.
Функция Add добавляет len к ptr и возвращает обновленный указатель unsafe.Pointer(uintptr(ptr) + uintptr(len)) [Go 1.17]. Аргумент len должен быть целочисленного типа или нетипизированной константой. Константный аргумент len должен быть представлен значением типа int; если он является нетипизированной константой, ему присваивается тип int. Правила допустимого использования Pointer по-прежнему применяются.
Функция Slice возвращает фрагмент, базовый массив которого начинается с ptr, а длина и емкость — len. Slice(ptr, len) эквивалентно
(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]
за исключением того, что в особом случае, если ptr равно nil, а len равно нулю, Slice возвращает nil [Go 1.17].
Аргумент len должен быть целочисленного типа или нетипизированной константой. Константа len должна быть неотрицательной и представляться значением типа int; если это нетипизированная константа, ей присваивается тип int. Во время выполнения, если len отрицательно, или если ptr равно nil, а len не равно нулю, возникает паника во время выполнения [Go 1.17].
Функция SliceData возвращает указатель на базовый массив аргумента slice. Если емкость slice cap(slice) не равна нулю, этот указатель равен &slice[:1][0]. Если slice равен nil, результат равен nil. В противном случае это не равный nil указатель на неуказанный адрес памяти [Go 1.20].
Функция String возвращает строковое значение, байты которого начинаются с ptr и длина которых равна len. К аргументам ptr и len применяются те же требования, что и в функции Slice. Если len равно нулю, результатом будет пустая строка "". Поскольку строки Go являются неизменяемыми, байты, переданные в String, не должны изменяться впоследствии [Go 1.20].
Функция StringData возвращает указатель на базовые байты аргумента str. Для пустой строки возвращаемое значение не определено и может быть nil. Поскольку строки Go являются неизменяемыми, байты, возвращаемые StringData, не должны изменяться [Go 1.20].
Гарантии размеров и выравнивания
Для числовых типов гарантируются следующие размеры:
type size in bytes
byte, uint8, int8 1
uint16, int16 2
uint32, int32, float32 4
uint64, int64, float64, complex64 8
complex128 16
Гарантируются следующие минимальные свойства выравнивания:
- Для переменной
x любого типа: unsafe.Alignof(x) не меньше 1.
- Для переменной
x типа struct: unsafe.Alignof(x) - наибольшее из всех значений unsafe.Alignof(x.f) для каждого поля f из x, но не менее 1.
- Для переменной
x типа array: unsafe.Alignof(x) - это то же самое, что и выравнивание переменной типа элемента массива.
Тип struct или array имеет нулевой размер, если он не содержит полей (или элементов, соответственно), имеющих размер больше нуля. Две разные переменные нулевого размера могут иметь один и тот же адрес в памяти.
Приложение
Версии языка
Гарантия совместимости Go 1 гарантирует, что программы, написанные в соответствии со спецификацией Go 1, будут продолжать компилироваться и работать правильно, без изменений, в течение всего срока действия этой спецификации. В более общем случае, по мере внесения изменений и добавления возможностей в язык, гарантия совместимости гарантирует, что программа на Go, работающая с определенной версией языка Go, будет продолжать работать и с любой последующей версией.
Например, возможность использовать префикс 0b для двоичных целочисленных литералов была введена в Go 1.13, на что указывает [Go 1.13] в разделе о целочисленных литералах. Исходный код, содержащий целочисленный литерал, например 0b1011, будет отклонен, если подразумеваемая или требуемая версия языка, используемая компилятором, старше Go 1.13.
В следующей таблице описана минимальная версия языка, необходимая для функций, появившихся после Go 1.
Go 1.9
- Декларация псевдонима может использоваться для объявления псевдонима для типа.
Go 1.13
- Целые литералы могут использовать префиксы 0b, 0B, 0o и 0O для двоичных и восьмеричных литералов соответственно.
- Шестнадцатеричные литералы с плавающей запятой могут быть написаны с использованием префиксов 0x и 0X.
- Суффикс i может использоваться с любыми (двоичными, десятичными, шестнадцатеричными) целыми или плавающими литералами, а не только с десятичными литералами.
- Цифры любого числового литерала могут быть разделены (сгруппированы) с помощью подчеркиваний _.
- Число сдвига в операции сдвига может быть целым числом со знаком.
Go 1.14
- Внедрение метода более одного раза через разные встроенные интерфейсы не является ошибкой.
Go 1.17
- Срез может быть преобразован в указатель массива, если типы элементов среза и массива совпадают, а массив не длиннее среза.
- Встроенный пакет unsafe включает новые функции Add и Slice.
Go 1.18
В версии 1.18 в язык добавлены полиморфные функции и типы («генерики»). В частности:
- Набор операторов и знаков препинания включает новый токен ~.
- В объявлениях функций и типов можно объявлять параметры типов.
- Типы интерфейсов могут встраивать произвольные типы (а не только имена типов интерфейсов), а также элементы типов union и ~T.
- Набор предварительно объявленных типов включает новые типы any и comparable.
Go 1.20
- Срез может быть преобразован в массив, если типы элементов среза и массива совпадают, а массив не длиннее среза.
- Встроенный пакет unsafe включает новые функции SliceData, String и StringData.
- Сравнимые типы (такие как обычные интерфейсы) могут удовлетворять сравнимым ограничениям, даже если аргументы типа не являются строго сравнимыми.
Go 1.21
- Набор предварительно объявленных функций включает новые функции min, max и clear.
- Типовое выведение использует типы методов интерфейса для выведения. Оно также выводит типовые аргументы для обобщенных функций, назначенных переменным или переданных в качестве аргументов другим (возможно, обобщенным) функциям.
Go 1.22
- В операторе «for» каждая итерация имеет свой собственный набор переменных итерации, а не использует одни и те же переменные в каждой итерации.
- Оператор «for» с клаузулой «range» может выполнять итерацию над целыми значениями от нуля до верхнего предела.
Go 1.23
- Оператор «for» с клаузулой «range» принимает функцию итератора в качестве выражения диапазона.
Go 1.24
- Объявление псевдонима может объявлять параметры типа.
Правила унификации типов
Правила унификации типов описывают, могут ли и как два типа унифицироваться. Точные детали имеют значение для реализаций Go, влияют на особенности сообщений об ошибках (например, сообщает ли компилятор об ошибке вывода типа или другой ошибке) и могут объяснить, почему вывод типа не работает в необычных ситуациях в коде. Но в целом эти правила можно игнорировать при написании кода Go: вывод типов в основном «работает как ожидается», и правила унификации настроены соответствующим образом.
Унификация типов контролируется режимом сопоставления, который может быть точным или приблизительным. Поскольку унификация рекурсивно спускается по структуре составного типа, режим сопоставления, используемый для элементов типа, режим сопоставления элементов, остается таким же, как режим сопоставления, за исключением случаев, когда два типа унифицируются для присваиваемости (≡A): в этом случае режим сопоставления является неточным на верхнем уровне, но затем меняется на точный для типов элементов, отражая тот факт, что типы не должны быть идентичными, чтобы быть присваиваемыми.
Два типа, которые не являются связанными типовыми параметрами, унифицируются точно, если выполняется любое из следующих условий:
- Оба типа идентичны.
- Оба типа имеют идентичную структуру, и их типы элементов унифицируются точно.
- Точно один тип является несвязанным типовым параметром с базовым типом, и этот базовый тип унифицируется с другим типом в соответствии с правилами унификации для ≡A (нестрогая унификация на верхнем уровне и точная унификация для типов элементов).
Если оба типа являются связанными параметрами типа, они унифицируются в соответствии с заданными режимами сопоставления, если:
- Оба параметра типа идентичны.
- Не более одного из параметров типа имеет известный аргумент типа. В этом случае параметры типа объединяются: оба они обозначают один и тот же аргумент типа. Если ни один из параметров типа еще не имеет известного аргумента типа, будущий аргумент типа, выведенный для одного из параметров типа, одновременно выводится для обоих.
- Оба типа-параметра имеют известный тип-аргумент, и типы-аргументы унифицируются в соответствии с заданными режимами сопоставления.
Один связанный тип-параметр P и другой тип T унифицируются в соответствии с заданными режимами сопоставления, если:
- P не имеет известного типа-аргумента. В этом случае T выводится как тип-аргумент для P.
- P имеет известный тип-аргумент A, A и T унифицируются в соответствии с заданными режимами сопоставления, и выполняется одно из следующих условий:
- И A, и T являются типами интерфейса: в этом случае, если A и T также являются определёнными типами, они должны быть идентичными. В противном случае, если ни один из них не является определённым типом, они должны иметь одинаковое количество методов (унификация A и T уже установила, что методы совпадают).
- Ни A, ни T не являются типами интерфейса: в этом случае, если T является определённым типом, T заменяет A в качестве выведенного аргумента типа для P.
Наконец, два типа, которые не являются связанными параметрами типа, объединяются слабо (и в соответствии с режимом сопоставления элементов), если:
- Оба типа унифицируются точно.
- Один тип является определенным типом, другой тип является типовым литералом, но не интерфейсом, и их базовые типы унифицируются в соответствии с режимом сопоставления элементов.
- Оба типа являются интерфейсами (но не параметрами типа) с идентичными типами термов, оба или ни один из них не встраивают предварительно объявленный тип comparable, соответствующие типы методов унифицируются точно, а набор методов одного из интерфейсов является подмножеством набора методов другого интерфейса.
- Только один тип является интерфейсом (но не параметром типа), соответствующие методы двух типов унифицируются в соответствии с режимом сопоставления элементов, а набор методов интерфейса является подмножеством набора методов другого типа.
- Оба типа имеют одинаковую структуру, и их типы элементов унифицируются в соответствии с режимом сопоставления элементов.
8 - Спецификация по модулям языка программирования Go
Этот документ представляет собой подробное справочное руководство по системе модулей Go.
Введение
Модули — это способ, с помощью которого Go управляет зависимостями.
Этот документ является подробным справочным руководством по модульной системе Go. Введение в создание проектов Go см. в статье «Как писать код на Go». Информацию об использовании модулей, переносе проектов на модули и других темах см. в серии статей в блоге, начиная с «Использование модулей Go».
Модули, пакеты и версии
Модуль — это набор пакетов, которые выпускаются, версионируются и распространяются вместе. Модули можно загружать напрямую из репозиториев контроля версий или с прокси-серверов модулей.
Модуль идентифицируется по пути модуля, который объявляется в файле go.mod вместе с информацией о зависимостях модуля. Корневой каталог модуля — это каталог, содержащий файл go.mod. Главный модуль — это модуль, содержащий каталог, в котором вызывается команда go.
Каждый пакет в модуле представляет собой набор исходных файлов в одном каталоге, которые компилируются вместе. Путь к пакету — это путь к модулю, соединенный с подкаталогом, содержащим пакет (относительно корня модуля). Например, модуль «golang.org/x/net» содержит пакет в каталоге «html». Путь к этому пакету — «golang.org/x/net/html».
Пути к модулям
Путь модуля — это каноническое имя модуля, объявленное с помощью директивы module в файле go.mod модуля. Путь модуля является префиксом для путей пакетов внутри модуля.
Путь модуля должен описывать как то, что делает модуль, так и то, где его найти. Обычно путь модуля состоит из пути корня репозитория, каталога внутри репозитория (обычно пустого) и суффикса основной версии (только для основной версии 2 или выше).
-
Путь корня репозитория — это часть пути модуля, которая соответствует корневому каталогу репозитория контроля версий, в котором разрабатывается модуль. Большинство модулей определены в корневом каталоге своего репозитория, поэтому обычно это весь путь. Например, golang.org/x/net — это путь корня репозитория для модуля с таким же именем. См. раздел Поиск репозитория для пути модуля, чтобы узнать, как команда go находит репозиторий с помощью HTTP-запросов, полученных из пути модуля.
-
Если модуль не определен в корневом каталоге репозитория, подкаталог модуля является частью пути модуля, которая называет каталог, не включая суффикс основной версии. Это также служит префиксом для семантических тегов версии. Например, модуль golang.org/x/tools/gopls находится в подкаталоге gopls репозитория с корневым путем golang.org/x/tools, поэтому он имеет подкаталог модуля gopls. См. раздел «Сопоставление версий с коммитами» и «Каталоги модулей в репозитории».
-
Если модуль выпускается с основной версией 2 или выше, путь модуля должен заканчиваться суффиксом основной версии, например /v2. Это может быть частью имени подкаталога или нет. Например, модуль с путем golang.org/x/repo/sub/v2 может находиться в подкаталоге /sub или /sub/v2 репозитория golang.org/x/repo.
Если модуль может зависеть от других модулей, необходимо соблюдать эти правила, чтобы команда go могла найти и загрузить модуль. Существует также несколько лексических ограничений на символы, разрешенные в путях модулей.
Модуль, который никогда не будет загружен в качестве зависимости другого модуля, может использовать любой допустимый путь пакета в качестве пути модуля, но необходимо следить за тем, чтобы он не конфликтовал с путями, которые могут использоваться зависимостями модуля или стандартной библиотекой Go. Стандартная библиотека Go использует пути к пакетам, которые не содержат точку в первом элементе пути, и команда go не пытается разрешить такие пути с сетевых серверов. Пути example и test зарезервированы для пользователей: они не будут использоваться в стандартной библиотеке и подходят для использования в автономных модулях, таких как те, которые определены в учебных пособиях или примерах кода, или созданы и обрабатываются как часть теста.
Версии
Версия идентифицирует неизменяемый снимок модуля, который может быть либо релизом, либо предварительной версией. Каждая версия начинается с буквы v, за которой следует семантическая версия. Подробнее о том, как версии форматируются, интерпретируются и сравниваются, см. Semantic Versioning 2.0.0.
Вкратце, семантическая версия состоит из трех неотрицательных целых чисел (версии major, minor и patch, слева направо), разделенных точками. За версией patch может следовать необязательная строка предварительной версии, начинающаяся с дефиса. За строкой предварительной версии или версией patch может следовать строка метаданных сборки, начинающаяся с плюса. Например, v0.0.0, v1.12.134, v8.0.5-pre и v2.0.9+meta являются действительными версиями.
Каждая часть версии указывает, является ли версия стабильной и совместимой с предыдущими версиями.
- Основная версия должна быть увеличена, а второстепенная и патч-версия должны быть установлены в ноль после внесения обратно несовместимых изменений в публичный интерфейс модуля или задокументированную функциональность, например, после удаления пакета.
- Второстепенная версия должна быть увеличена, а патч-версия установлена в ноль после внесения обратно совместимых изменений, например, после добавления новой функции.
Версия патча должна быть увеличена после изменения, которое не влияет на публичный интерфейс модуля, например исправления ошибки или оптимизации.
- Суффикс предварительной версии указывает, что версия является предварительной. Предварительные версии сортируются перед соответствующими выпусками. Например,
v1.2.3-pre идет перед v1.2.3.
- Суффикс метаданных сборки игнорируется при сравнении версий. Команда
go принимает версии с метаданными сборки и преобразует их в псевдоверсии, чтобы сохранить полный порядок между версиями.
- Специальный суффикс
+incompatible обозначает версию, выпущенную до перехода на модули версии 2 или более поздней (см. Совместимость с репозиториями, не являющимися модулями).
- Специальный суффикс
+dirty добавляется к информации о версии бинарного файла, когда он скомпилирован с помощью инструментария Go 1.24 или более поздней версии в рамках действительного локального репозитория системы контроля версий (VCS), который содержит нефиксированные изменения в рабочем каталоге.
Версия считается нестабильной, если ее основная версия равна 0 или она имеет суффикс предварительной версии. Нестабильные версии не подпадают под требования совместимости. Например, v0.2.0 может быть несовместима с v0.1.0, а v1.5.0-beta может быть несовместима с v1.5.0.
Go может получать доступ к модулям в системах контроля версий, используя теги, ветки или ревизии, которые не соответствуют этим соглашениям.
Однако в основном модуле команда go автоматически преобразует имена ревизий, которые не соответствуют этому стандарту, в канонические версии.
Команда go также удаляет суффиксы метаданных сборки (за исключением +incompatible) в рамках этого процесса. Это может привести к появлению псевдоверсии, предварительной версии, которая кодирует идентификатор ревизии (например, хэш Git commit) и временную метку из системы контроля версий.
Например, команда go get golang.org/x/net@daa7c041 преобразует хеш коммита daa7c041 в псевдоверсию v0.0.0-20191109021931-daa7c04131f5. Канонические версии требуются за пределами основного модуля, и команда go сообщит об ошибке, если в файле go.mod появится неканоническая версия, такая как master.
Псевдоверсии
Псевдоверсия — это специально отформатированная предварительная версия, которая кодирует информацию об определенной ревизии в репозитории системы контроля версий. Например, v0.0.0-20191109021931-daa7c04131f5 — это псевдоверсия.
Псевдоверсии могут относиться к ревизиям, для которых нет семантических тегов версий. Они могут использоваться для тестирования коммитов перед созданием тегов версий, например, в ветке разработки.
Каждая псевдоверсия состоит из трех частей:
- Префикс базовой версии (
vX.0.0 или vX.Y.Z-0), который либо получен из семантического тега версии, предшествующего ревизии, либо vX.0.0, если такого тега нет.
- Временная метка (ггггммддччммсс), которая представляет собой время UTC, когда была создана ревизия. В Git это время коммита, а не время автора.
- Идентификатор ревизии (abcdefabcdef), который представляет собой 12-символьный префикс хеша коммита или, в Subversion, номер ревизии с нулевым заполнением.
Каждая псевдоверсия может иметь одну из трех форм, в зависимости от базовой версии. Эти формы гарантируют, что псевдоверсия будет выше базовой версии, но ниже следующей версии с тегом.
vX.0.0-yyyymmddhhmmss-abcdefabcdef используется, когда базовая версия неизвестна. Как и во всех версиях, основная версия X должна совпадать с суффиксом основной версии модуля.
vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef используется, когда базовая версия является предварительной версией, такой как vX.Y.Z-pre.
vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef используется, когда базовая версия является релизной версией, такой как vX.Y.Z. Например, если базовая версия — v1.2.3, псевдоверсия может быть v1.2.4-0.20191109021931-daa7c04131f5.
Несколько псевдоверсий могут ссылаться на одну и ту же фиксацию, используя разные базовые версии. Это происходит естественным образом, когда после записи псевдоверсии помечается более низкая версия.
Эти формы придают псевдоверсиям два полезных свойства:
- Псевдоверсии с известными базовыми версиями сортируются выше этих версий, но ниже других предварительных версий для более поздних версий.
- Псевдоверсии с одинаковым префиксом базовой версии сортируются в хронологическом порядке.
Команда go выполняет несколько проверок, чтобы убедиться, что авторы модулей контролируют сравнение псевдоверсий с другими версиями и что псевдоверсии ссылаются на ревизии, которые действительно являются частью истории коммитов модуля.
- Если указана базовая версия, должен быть соответствующий семантический тег версии, который является предком ревизии, описанной псевдоверсией. Это предотвращает обход минимального выбора версии разработчиками с помощью псевдоверсии, которая сравнивается выше всех тегированных версий, таких как
v1.999.999-99999999999999-daa7c04131f5.
- Временная метка должна совпадать с временной меткой ревизии. Это предотвращает затопление прокси-серверов модулей неограниченным количеством идентичных псевдоверсий. Это также предотвращает изменение относительного порядка версий потребителями модулей.
- Ревизия должна быть предком одной из веток или тегов репозитория модуля. Это предотвращает ссылки злоумышленников на не утвержденные изменения или запросы на извлечение.
Псевдоверсии никогда не нужно вводить вручную. Многие команды принимают хэш коммита или имя ветки и автоматически преобразуют его в псевдоверсию (или версию с тегом, если она доступна).
Например:
go get example.com/mod@master
go list -m -json example.com/mod@abcd1234
Суффиксы основных версий
Начиная с основной версии 2, пути к модулям должны иметь суффикс основной версии, такой как /v2, который соответствует основной версии. Например, если модуль имеет путь example.com/mod в версии v1.0.0, он должен иметь путь example.com/mod/v2 в версии v2.0.0.
Суффиксы основных версий реализуют правило совместимости импорта:
- Если старый пакет и новый пакет имеют одинаковый путь импорта, новый пакет должен быть обратно совместим со старым пакетом.
По определению, пакеты в новой основной версии модуля не являются обратно совместимыми с соответствующими пакетами в предыдущей основной версии. Следовательно, начиная с v2, пакетам требуются новые пути импорта. Это достигается путем добавления суффикса основной версии к пути модуля.
Поскольку путь модуля является префиксом пути импорта для каждого пакета в модуле, добавление суффикса основной версии к пути модуля обеспечивает отдельный путь импорта для каждой несовместимой версии.
Суффиксы основной версии не допускаются в основных версиях v0 или v1. Нет необходимости изменять путь модуля между v0 и v1, поскольку версии v0 нестабильны и не имеют гарантии совместимости. Кроме того, для большинства модулей v1 обратно совместима с последней версией v0; версия v1 действует как обязательство по совместимости, а не как указание на несовместимые изменения по сравнению с v0.
В качестве особого случая пути модулей, начинающиеся с gopkg.in/, всегда должны иметь суффикс основной версии, даже в v0 и v1. Суффикс должен начинаться с точки, а не с косой черты (например, gopkg.in/yaml.v2).
Суффиксы основных версий позволяют нескольким основным версиям модуля сосуществовать в одной сборке. Это может быть необходимо из-за проблемы алмазной зависимости.
Обычно, если модуль требуется в двух разных версиях по транзитивным зависимостям, будет использоваться более высокая версия. Однако, если две версии несовместимы, ни одна из них не удовлетворит всех клиентов.
Поскольку несовместимые версии должны иметь разные номера основных версий, они также должны иметь разные пути модулей из-за суффиксов основных версий. Это разрешает конфликт: модули с разными суффиксами рассматриваются как отдельные модули, и их пакеты — даже пакеты в одном и том же подкаталоге относительно корней модулей — являются разными.
Многие проекты Go выпускали версии v2 или выше без использования суффикса основной версии до перехода на модули (возможно, даже до того, как модули были введены). Эти версии помечены тегом +incompatible build (например, v2.0.0+incompatible). Дополнительную информацию см. в разделе «Совместимость с репозиториями, не являющимися модулями».
Преобразование пакета в модуль
Когда команда go загружает пакет, используя путь к пакету, ей необходимо определить, какой модуль предоставляет этот пакет.
Команда go начинает с поиска в списке сборки модулей с путями, которые являются префиксами пути пакета.
Например, если импортирован пакет example.com/a/b, а модуль example.com/a находится в списке сборки, команда go проверит, содержит ли example.com/a пакет в каталоге b.
Чтобы каталог считался пакетом, в нем должен присутствовать хотя бы один файл с расширением .go. Ограничения сборки для этой цели не применяются. Если ровно один модуль в списке сборки предоставляет пакет, используется этот модуль. Если ни один модуль не предоставляет пакет или если два или более модулей предоставляют пакет, команда go сообщает об ошибке.
Флаг -mod=mod указывает команде go попытаться найти новые модули, предоставляющие отсутствующие пакеты, и обновить go.mod и go.sum. Команды go get и go mod tidy делают это автоматически.
Когда команда go ищет новый модуль для пути пакета, она проверяет переменную окружения GOPROXY, которая представляет собой разделенный запятыми список URL-адресов прокси или ключевые слова direct или off. URL-адрес прокси указывает, что команда go должна связаться с прокси модуля, используя протокол GOPROXY. direct указывает, что команда go должна связываться с системой контроля версий. off указывает, что попытки связи не должны предприниматься. Переменные окружения GOPRIVATE и GONOPROXY также могут использоваться для управления этим поведением.
Для каждой записи в списке GOPROXY команда go запрашивает последнюю версию каждого пути модуля, который может предоставить пакет (то есть каждый префикс пути пакета). Для каждого успешно запрошенного пути модуля команда go загрузит модуль в последней версии и проверит, содержит ли модуль запрошенный пакет. Если один или несколько модулей содержат запрошенный пакет, используется модуль с самым длинным путем. Если найден один или несколько модулей, но ни один из них не содержит запрошенный пакет, выводится сообщение об ошибке. Если модули не найдены, команда go пробует следующую запись в списке GOPROXY. Если записей не осталось, выводится сообщение об ошибке.
Например, предположим, что команда go ищет модуль, который предоставляет пакет golang.org/x/net/html, а GOPROXY установлен на https://corp.example.com,https://proxy.golang.org. Команда go может сделать следующие запросы:
- К
https://corp.example.com/ (параллельно):
- Запрос на последнюю версию
golang.org/x/net/html
- Запрос на последнюю версию
golang.org/x/net
- Запрос на последнюю версию
golang.org/x
- Запрос на последнюю версию
golang.org
- К
https://proxy.golang.org/, если все запросы к https://corp.example.com/ завершились с ошибкой 404 или 410:
- Запрос на последнюю версию
golang.org/x/net/html
- Запрос на последнюю версию
golang.org/x/net
- Запрос на последнюю версию
golang.org/x
- Запрос на последнюю версию
golang.org
После того, как подходящий модуль найден, команда go добавляет новое требование с путем и версией нового модуля в файл go.mod основного модуля. Это гарантирует, что при загрузке того же пакета в будущем будет использоваться тот же модуль той же версии. Если разрешенный пакет не импортируется пакетом в основном модуле, новое требование будет иметь // косвенный комментарий.
Файлы go.mod
Модуль определяется текстовым файлом go.mod в кодировке UTF-8 в его корневом каталоге. Файл go.mod ориентирован на строки. Каждая строка содержит одну директиву, состоящую из ключевого слова и аргументов.
Например:
module example.com/my/thing
go 1.23.0
require example.com/other/thing v1.0.2
require example.com/new/thing/v2 v2.3.4
exclude example.com/old/thing v1.2.3
replace example.com/bad/thing v1.4.5 => example.com/good/thing v1.4.5
retract [v1.9.0, v1.9.5]
Ведущее ключевое слово может быть разложено на соседние строки, чтобы создать блок, как в импорте Go.
require (
example.com/new/thing/v2 v2.3.4
example.com/old/thing v1.2.3
)
Файл go.mod разработан таким образом, чтобы его можно было читать и записывать. Команда go предоставляет несколько подкоманд, которые изменяют файлы go.mod. Например, go get может обновить или понизить уровень определенных зависимостей.
Команды, загружающие граф модулей, автоматически обновляют go.mod, когда это необходимо. go mod edit может выполнять низкоуровневые правки. Пакет golang.org/x/mod/modfile может быть использован программами на Go для внесения тех же изменений программно.
Файл go.mod требуется для основного модуля и для любого заменяющего его модуля, указанного с помощью локального пути к файлу. Однако модуль, не имеющий явного файла go.mod, все равно может быть востребован в качестве зависимости или использоваться в качестве замены, указанной с помощью пути к модулю и его версии; см. раздел Совместимость с немодульными репозиториями.
Лексические элементы
При разборе файла go.mod его содержимое разбивается на последовательность токенов. Существует несколько видов токенов: пробелы, комментарии, знаки препинания, ключевые слова, идентификаторы и строки.
Пробелы состоят из пробелов (U+0020), табуляции (U+0009), возврата каретки (U+000D) и новой строки (U+000A). Пробелы, кроме новой строки, не имеют никакого эффекта, кроме разделения токенов, которые в противном случае были бы объединены. Новые строки являются значимыми токенами.
Комментарии начинаются с // и продолжаются до конца строки. Комментарии /* */ не допускаются.
Знаки препинания включают (, ) и =>.
Ключевые слова различают различные типы директив в файле go.mod. Допустимые ключевые слова: module, go, require, replace, exclude и retract.
Идентификаторы — это последовательности символов, не являющихся пробелами, такие как пути модулей или семантические версии.
Строки — это последовательности символов в кавычках. Существует два вида строк: интерпретируемые строки, начинающиеся и заканчивающиеся кавычками (", U+0022), и необработанные строки, начинающиеся и заканчивающиеся грависными акцентами (`, U+0060). Интерпретируемые строки могут содержать экранирующие последовательности, состоящие из обратной косой черты (\, U+005C), за которой следует другой символ. Экранированная кавычка (\») не завершает интерпретируемую строку. Незаключенное в кавычки значение интерпретируемой строки — это последовательность символов между кавычками, в которой каждая экранирующая последовательность заменяется символом, следующим за обратной косой чертой (например, \" заменяется на ", \n заменяется на n). Напротив, незаключенное в кавычки значение необработанной строки — это просто последовательность символов между грависами; обратные косые черты не имеют особого значения в необработанных строках.
Идентификаторы и строки взаимозаменяемы в грамматике go.mod.
Пути к модулям и версии
Большинство идентификаторов и строк в файле go.mod являются либо путями к модулям, либо версиями.
Путь к модулю должен удовлетворять следующим требованиям:
- Путь должен состоять из одного или нескольких элементов, разделенных косыми чертами
(/, U+002F). Он не должен начинаться или заканчиваться косой чертой.
- Каждый элемент пути представляет собой непустую строку, состоящую из букв
ASCII, цифр ASCII и ограниченного набора знаков препинания ASCII (-, ., _ и ~).
- Элемент пути не может начинаться или заканчиваться точкой
(., U+002E).
- Префикс элемента до первой точки не должен быть зарезервированным именем файла в Windows, независимо от регистра (CON, com1, NuL и т. д.).
- Префикс элемента до первой точки не должен заканчиваться на тильду, за которой следует одна или несколько цифр (например, EXAMPL~1.COM).
Если путь к модулю появляется в директиве require и не заменяется, или если путь к модулю появляется в правой части директивы replace, команде go может потребоваться загрузить модули с этим путем, и должны быть выполнены некоторые дополнительные требования.
- Ведущий элемент пути (до первой косой черты, если она есть), по соглашению являющийся доменным именем, должен содержать только строчные буквы ASCII, цифры ASCII, точки (., U+002E) и тире (-, U+002D); он должен содержать по крайней мере одну точку и не может начинаться с тире.
- Для конечного элемента пути в форме /vN, где N выглядит как число (цифры ASCII и точки), N не должно начинаться с нуля, не должно быть /v1 и не должно содержать точек.
- Для путей, начинающихся с gopkg.in/, это требование заменяется требованием, чтобы путь соответствовал соглашениям службы gopkg.in.
Версии в файлах
go.mod могут быть каноническими или неканоническими.
Каноническая версия начинается с буквы v, за которой следует семантическая версия, соответствующая спецификации Semantic Versioning 2.0.0. Дополнительные сведения см. в разделе «Версии».
Большинство других идентификаторов и строк могут использоваться в качестве неканонических версий, хотя существуют некоторые ограничения, позволяющие избежать проблем с файловыми системами, репозиториями и прокси-серверами модулей. Неканонические версии допускаются только в файле go.mod главного модуля. Команда go попытается заменить каждую неканоническую версию эквивалентной канонической версией при автоматическом обновлении файла go.mod.
В местах, где путь модуля связан с версией (как в директивах require, replace и exclude), конечный элемент пути должен соответствовать версии. См. Суффиксы основных версий.
Грамматика
Синтаксис go.mod указан ниже с использованием расширенной формы Бэкуса-Наура (EBNF). Подробнее о синтаксисе EBNF см. раздел “Нотация” в спецификации языка Go.
GoMod = { Directive } .
Directive = ModuleDirective |
GoDirective |
ToolDirective |
IgnoreDirective |
RequireDirective |
ExcludeDirective |
ReplaceDirective |
RetractDirective .
Новая строка, идентификаторы и строки обозначаются символами newline, ident и string, соответственно.
Пути и версии модулей обозначаются ModulePath и Version.
ModulePath = ident | string . /* см. ограничения выше */
Версия = ident | string . /* см. ограничения выше */
module директива
Директива module определяет путь к главному модулю. Файл go.mod должен содержать ровно одну директиву модуля.
Например:
Deprecation (Устаревший)
Модуль может быть помечен как устаревший в блоке комментариев, содержащем строку Deprecated: (с учетом регистра) в начале абзаца. Сообщение об устаревании начинается после двоеточия и идет до конца абзаца. Комментарии могут располагаться непосредственно перед директивой модуля или после нее в той же строке.
Например:
// Deprecated: use example.com/mod/v2 instead.
module example.com/mod
Начиная с Go 1.17, go list -m -u проверяет информацию обо всех устаревших модулях в списке сборки. go get проверяет устаревшие модули, необходимые для сборки пакетов, указанных в командной строке.
Когда команда go извлекает информацию об устаревании модуля, она загружает файл go.mod из версии, соответствующей запросу @latest, без учета отзывов или исключений. Команда go загружает список отозванных версий из того же файла go.mod.
Чтобы объявить модуль устаревшим, автор может добавить комментарий // Deprecated: и пометить новый выпуск. Автор может изменить или удалить сообщение об устаревании в более позднем выпуске.
Устаревание применяется ко всем второстепенным версиям модуля. Основные версии выше v2 считаются отдельными модулями для этой цели, поскольку их суффиксы основных версий дают им отличные пути модулей.
Сообщения об устаревании предназначены для информирования пользователей о том, что модуль больше не поддерживается, и для предоставления инструкций по миграции, например, на последнюю основную версию. Отдельные второстепенные и исправленные версии не могут быть признаны устаревшими; для этого более подходящим может быть отзыв.
go директива
Директива go указывает, что модуль был написан с учетом семантики определенной версии Go. Версия должна быть действительной версией Go, например 1.14, 1.21rc1 или 1.23.0.
Директива go устанавливает минимальную версию Go, необходимую для использования этого модуля. До Go 1.21 эта директива носила только рекомендательный характер, теперь же она является обязательным требованием: инструментарий Go отказывается использовать модули, объявляющие более новые версии Go.
Директива go является входными данными для выбора инструментария Go, который будет запущен. Подробности см. в разделе «Инструментарий Go».
Директива go влияет на использование новых языковых функций:
- Для пакетов в модуле компилятор отклоняет использование языковых функций, введенных после версии, указанной в директиве
go. Например, если модуль имеет директиву go 1.12, его пакеты не могут использовать числовые литералы, такие как 1_000_000, которые были введены в Go 1.13.
- Если более старая версия Go компилирует один из пакетов модуля и встречает ошибку компиляции, в сообщении об ошибке указывается, что модуль был написан для более новой версии Go. Например, предположим, что модуль имеет директиву
go 1.13, а пакет использует числовой литерал 1_000_000. Если этот пакет компилируется с помощью Go 1.12, компилятор отмечает, что код написан для Go 1.13.
Директива go также влияет на поведение команды go:
-
В go 1.14 и выше может быть включена автоматическая продажа. Если файл vendor/modules.txt присутствует и соответствует go.mod, нет необходимости явно использовать флаг -mod=vendor.
-
В go 1.16 и выше шаблон all package соответствует только пакетам, транзитивно импортированным пакетами и тестами в главном модуле. Это тот же набор пакетов, который сохраняется go mod vendor с момента введения модулей. В более ранних версиях all также включает тесты пакетов, импортированных пакетами в главном модуле, тесты этих пакетов и т. д.
-
В go 1.17 и выше:
- Файл
go.mod включает явную директиву require для каждого модуля, который предоставляет любой пакет, транзитивно импортированный пакетом или тестом в главном модуле. (В go 1.16 и ниже косвенная зависимость включается только в том случае, если при выборе минимальной версии в противном случае была бы выбрана другая версия.) Эта дополнительная информация позволяет выполнять обрезку графа модулей и отложенную загрузку модулей.
- Поскольку
// косвенных зависимостей может быть гораздо больше, чем в предыдущих версиях go, косвенные зависимости записываются в отдельном блоке в файле go.mod.
go mod vendor опускает файлы go.mod и go.sum для зависимостей, поставляемых поставщиком. (Это позволяет вызывать команду go в подкаталогах vendor для определения правильного основного модуля.)
go mod vendor записывает версию go из файла go.mod каждой зависимости в vendor/modules.txt.
-
В go 1.21 или выше:
- Строка
go объявляет минимальную версию Go, необходимую для использования с этим модулем.
- Строка
go должна быть больше или равна строке go всех зависимостей.
- Команда
go больше не пытается поддерживать совместимость с предыдущей старой версией Go.
- Команда
go более тщательно следит за сохранением контрольных сумм файлов go.mod в файле go.sum.
Файл go.mod может содержать не более одной директивы go. Большинство команд добавляют директиву go с текущей версией Go, если она отсутствует.
Если директива go отсутствует, предполагается, что используется go 1.16.
GoDirective = «go» GoVersion newline .
GoVersion = string | ident . /* действительная версия выпуска; см. выше */
Пример:
Директива toolchain объявляет предлагаемую цепочку инструментов Go для использования с модулем. Версия предлагаемой цепочки инструментов Go не может быть меньше требуемой версии Go, объявленной в директиве go. Директива toolchain действует только в том случае, если модуль является главным модулем и версия инструментальной цепочки по умолчанию меньше версии предлагаемой инструментальной цепочки.
Для воспроизводимости команда go записывает собственное имя цепочки инструментов в строку toolchain каждый раз, когда обновляет версию go в файле go.mod (обычно во время go get).
Подробности см. в разделе “Инструментальные цепочки Go”.
ToolchainDirective = "toolchain" ToolchainName newline .
ToolchainName = string | ident . /* действительное имя цепочки инструментов; см. "Цепочки инструментов Go" */
Пример:
godebug деректива
Директива godebug объявляет единственную настройку GODEBUG, которая должна применяться, когда данный модуль является основным. Таких строк может быть несколько, и они могут быть разделены по факторам. Ошибкой является то, что главный модуль называет несуществующий ключ GODEBUG. Эффект от применения godebug key=value такой, как если бы каждый компилируемый основной пакет содержал исходный файл, в котором был бы указан //go:debug key=value.
GodebugDirective = "godebug" ( GodebugSpec | "(" newline { GodebugSpec } ")" newline ) .
GodebugSpec = GodebugKey "=" GodebugValue newline.
GodebugKey = GodebugChar { GodebugChar }.
GodebugValue = GodebugChar { GodebugChar }.
GodebugChar = any non-space character except , " ` ' (comma and quotes).
Например:
godebug default=go1.21
godebug (
panicnil=1
asynctimerchan=0
)
require директива
Директива require объявляет минимальную требуемую версию данной зависимости модуля. Для каждой требуемой версии модуля команда go загружает файл go.mod для этой версии и включает требования из этого файла. После загрузки всех требований команда go разрешает их с помощью минимального выбора версии (MVS) для создания списка сборки.
Команда go автоматически добавляет // косвенные комментарии для некоторых требований. Косвенный комментарий // указывает, что ни один пакет из требуемого модуля не импортируется напрямую каким-либо пакетом в основном модуле.
Если директива go указывает go 1.16 или ниже, команда go добавляет косвенное требование, когда выбранная версия модуля выше, чем то, что уже подразумевается (транзитивно) другими зависимостями основного модуля. Это может произойти из-за явного обновления (go get -u ./...), удаления какой-либо другой зависимости, которая ранее налагала требование (go mod tidy), или зависимости, которая импортирует пакет без соответствующего требования в своем собственном файле go.mod (например, зависимость, у которой вообще отсутствует файл go.mod).
В go 1.17 и выше команда go добавляет косвенное требование для каждого модуля, который предоставляет любой пакет, импортируемый (даже косвенно) пакетом или тестом в основном модуле или передаваемый в качестве аргумента go get. Эти более комплексные требования позволяют выполнять обрезку графа модулей и отложенную загрузку модулей.
RequireDirective = "require" ( RequireSpec | "(" newline { RequireSpec } ")" newline ) .
RequireSpec = ModulePath Version newline .
Например:
require golang.org/x/net v1.2.3
require (
golang.org/x/crypto v1.4.5 // indirect
golang.org/x/text v1.6.7
)
Директива tool добавляет пакет в качестве зависимости от текущего модуля. Она также делает его доступным для запуска с помощью инструмента go, если текущий рабочий каталог находится в этом модуле или в рабочей области, содержащей этот модуль.
Если пакет инструмента не находится в текущем модуле, должна присутствовать директива require, указывающая версию используемого инструмента.
Меташаблон tool разрешается в список инструментов, определенных в go.mod текущего модуля, или в режиме рабочего пространства - в объединение всех инструментов, определенных во всех модулях рабочего пространства.
ToolDirective = "tool" ( ToolSpec | "(" newline { ToolSpec } ")" newline ) .
ToolSpec = ModulePath newline .
Например:
tool golang.org/x/tools/cmd/stringer
tool (
example.com/module/cmd/a
example.com/module/cmd/b
)
ignore директива
Директива ignore заставляет команду go игнорировать разделенные косой чертой пути к каталогам, а также любые файлы или каталоги, рекурсивно содержащиеся в них, при поиске шаблонов пакетов.
Если путь начинается с ./, то он интерпретируется относительно корневого каталога модуля, и этот каталог и любые каталоги или файлы, рекурсивно содержащиеся в нем, будут игнорироваться при поиске шаблонов пакетов.
В противном случае все каталоги с таким путем на любой глубине модуля и любые каталоги или файлы, рекурсивно содержащиеся в них, будут проигнорированы.
Например:
ignore ./node_modules
ignore (
static
content/html
./third_party/javascript
)
exclude директива
Директива exclude предотвращает загрузку версии модуля командой go.
Начиная с Go 1.16, если версия, на которую ссылается директива require в любом файле go.mod, исключена директивой exclude в файле go.mod основного модуля, требование игнорируется. Это может привести к тому, что команды go get и go mod tidy добавят новые требования к более высоким версиям в go.mod с // косвенным комментарием, если это уместно.
До Go 1.16, если исключенная версия была указана директивой require, команда go перечисляла доступные версии для модуля (как показано с помощью go list -m -versions) и загружала следующую более высокую неисключенную версию. Это могло привести к недетерминированному выбору версии, поскольку следующая более высокая версия могла измениться со временем. Для этой цели учитывались как релизные, так и предрелизные версии, но не учитывались псевдоверсии. Если более высоких версий не было, команда go сообщала об ошибке.
Директивы exclude применяются только в файле go.mod основного модуля и игнорируются в других модулях. Подробности см. в разделе «Минимальный выбор версии».
ExcludeDirective = "exclude" ( ExcludeSpec | "(" newline { ExcludeSpec } ")" newline ) .
ExcludeSpec = ModulePath Version newline .
Например:
exclude golang.org/x/net v1.2.3
exclude (
golang.org/x/crypto v1.4.5
golang.org/x/text v1.6.7
)
replace директива
Директива replace заменяет содержимое определенной версии модуля или всех версий модуля содержимым, найденным в другом месте. Замена может быть указана либо другим путем к модулю и версии, либо путем к файлу, специфичному для платформы.
Если версия указана слева от стрелки (=>), заменяется только эта конкретная версия модуля; доступ к другим версиям будет осуществляться в обычном режиме. Если левая версия опущена, заменяются все версии модуля.
Если путь справа от стрелки является абсолютным или относительным (начинается с ./ или ../), он интерпретируется как локальный путь к корневому каталогу модуля замены, который должен содержать файл go.mod. В этом случае версия замены должна быть опущена.
Если путь справа не является локальным путем, он должен быть действительным путем к модулю. В этом случае требуется указать версию. Та же версия модуля не должна также фигурировать в списке сборки.
Независимо от того, указана ли замена с помощью локального пути или пути к модулю, если модуль замены имеет файл go.mod, его директива модуля должна соответствовать пути к модулю, который он заменяет.
Директивы replace применяются только в файле go.mod главного модуля и игнорируются в других модулях. Подробности см. в разделе «Выбор минимальной версии».
Если есть несколько главных модулей, применяются файлы go.mod всех главных модулей. Конфликтующие директивы replace между главными модулями не допускаются и должны быть удалены или переопределены в replace в файле go.work.
Обратите внимание, что сама по себе директива replace не добавляет модуль в граф модулей. Также необходима директива require, которая ссылается на замененную версию модуля, либо в файле go.mod основного модуля, либо в файле go.mod зависимости. Директива replace не имеет эффекта, если версия модуля слева не требуется.
ReplaceDirective = "replace" ( ReplaceSpec | "(" newline { ReplaceSpec } ")" newline ) .
ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline
| ModulePath [ Version ] "=>" ModulePath Version newline .
FilePath = /* platform-specific relative or absolute file path */
Например:
replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
replace (
golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
golang.org/x/net => example.com/fork/net v1.4.5
golang.org/x/net v1.2.3 => ./fork/net
golang.org/x/net => ./fork/net
)
retract директива
Директива retract указывает, что версия или диапазон версий модуля, определенные в go.mod, не должны использоваться. Директива retract полезна, когда версия была опубликована преждевременно или после публикации версии была обнаружена серьезная проблема. Отмененные версии должны оставаться доступными в репозиториях контроля версий и на прокси-серверах модулей, чтобы обеспечить работоспособность сборок, которые от них зависят. Слово retract заимствовано из академической литературы: отмененная научная статья по-прежнему доступна, но в ней есть проблемы, и она не должна служить основой для будущих работ.
Когда версия модуля отзывается, пользователи не будут автоматически обновляться до нее с помощью go get, go mod tidy или других команд. Сборки, которые зависят от отозванных версий, должны продолжать работать, но пользователи будут уведомлены об отзывах, когда они будут проверять обновления с помощью go list -m -u или обновлять связанный модуль с помощью go get.
Чтобы отозвать версию, автор модуля должен добавить директиву retract в go.mod, а затем опубликовать новую версию, содержащую эту директиву. Новая версия должна быть выше других выпущенных или предварительных версий; то есть запрос @latest version должен возвращать новую версию, прежде чем будут рассмотрены отзывы. Команда go загружает и применяет отзывы из версии, показанной командой go list -m -retracted $modpath@latest (где $modpath — путь к модулю).
Отмененные версии скрываются из списка версий, выводимого командой go list -m -versions, если не используется флаг -retracted. Отмененные версии исключаются при разрешении запросов версий, таких как @>=v1.2.3 или @latest.
Версия, содержащая отмены, может отменить себя. Если самая высокая релизная или предрелизная версия модуля отменяет себя, запрос @latest разрешается в более низкую версию после исключения отмененных версий.
В качестве примера рассмотрим случай, когда автор модуля example.com/m случайно публикует версию v1.0.0. Чтобы пользователи не обновлялись до v1.0.0, автор может добавить две директивы retract в go.mod, а затем пометить v1.0.1 с отзывами.
retract (
v1.0.0 // Опубликовано случайно.
v1.0.1 // Содержит только отмены.)
Когда пользователь запускает go get example.com/m@latest, команда go считывает отмены из v1.0.1, которая теперь является самой высокой версией. И v1.0.0, и v1.0.1 отменены, поэтому команда go обновит (или понизит!) версию до следующей самой высокой версии, возможно v0.9.5.
Директивы retract могут быть написаны с одной версией (например, v1.0.0) или с закрытым интервалом версий с верхней и нижней границами, ограниченными символами [ и ] (например, [v1.1.0, v1.2.0]). Одна версия эквивалентна интервалу, где верхняя и нижняя границы одинаковы. Как и другие директивы, несколько директив retract могут быть сгруппированы в блок, ограниченный символами ( в конце строки и ) в отдельной строке.
Каждая директива retract должна иметь комментарий, объясняющий причину отзыва, хотя это не является обязательным. Команда go может отображать комментарии с объяснением причин в предупреждениях об отозванных версиях и в выводе go list. Комментарий с обоснованием может быть написан непосредственно над директивой retract (без пробела между ними) или после нее в той же строке. Если комментарий находится над блоком, он применяется ко всем директивам retract в блоке, которые не имеют собственных комментариев. Комментарий с обоснованием может занимать несколько строк.
RetractDirective = "retract" ( RetractSpec | "(" newline { RetractSpec } ")" newline ) .
RetractSpec = ( Version | "[" Version "," Version "]" ) newline .
Примеры:
- Извлечение всех версий между v1.0.0 и v1.9.9:
retract v1.0.0
retract [v1.0.0, v1.9.9]
retract (
v1.0.0
[v1.0.0, v1.9.9]
)
- Возврат к предварительной версии после преждевременного выпуска версии v1.0.0:
retract [v0.0.0, v1.0.1] // assuming v1.0.1 contains this retraction.
- Стирание модуля, включая все псевдо-версии и помеченные версии:
retract [v0.0.0-0, v0.15.2] // assuming v0.15.2 contains this retraction.
Директива retract была добавлена в Go 1.16. Go 1.15 и ниже сообщает об ошибке, если директива retract записана в файле go.mod главного модуля, и игнорирует директивы retract в файлах go.mod зависимых модулей.
Автоматические обновления
Большинство команд сообщают об ошибке, если в go.mod отсутствует информация или он не совсем точно отражает реальность. Команды go get и go mod tidy могут быть использованы для устранения большинства этих проблем. Кроме того, флаг -mod=mod можно использовать с большинством команд с поддержкой модулей (go build, go test и т. д.), чтобы указать команде go исправить проблемы в go.mod и go.sum автоматически.
Например, рассмотрим этот файл go.mod:
module example.com/M
go 1.23.0
require (
example.com/A v1
example.com/B v1.0.0
example.com/C v1.0.0
example.com/D v1.2.3
example.com/E dev
)
exclude example.com/D v1.2.3
Обновление, запущенное с помощью -mod=mod, переписывает неканонические идентификаторы версий в каноническую форму semver, поэтому v1 example.com/A становится v1.0.0, а dev example.com/E становится псевдоверсией для последней фиксации в ветке dev, возможно, v0.0. 0-20180523231146-b3f5c0f6e5f1.
Обновление изменяет требования с учетом исключений, поэтому требование к исключенному example.com/D v1.2.3 обновляется для использования следующей доступной версии example.com/D, возможно, v1.2.4 или v1.3.0.
Обновление удаляет избыточные или вводящие в заблуждение требования. Например, если example.com/A v1.0.0 сам требует example.com/B v1.2.0 и example.com/C v1.0.0, то требование go.mod к example.com/B v1.0.0 вводит в заблуждение (заменяется требованием example.com/A к v1.2.0), а его требование к example. com/C v1.0.0 является избыточным (поскольку example.com/A требует ту же версию), поэтому оба требования будут удалены. Если основной модуль содержит пакеты, которые напрямую импортируют пакеты из example.com/B или example.com/C, то требования будут сохранены, но обновлены до фактически используемых версий.
Наконец, обновление переформатирует go.mod в каноническом формате, так что будущие механические изменения приведут к минимальным различиям. Команда go не будет обновлять go.mod, если требуются только изменения форматирования.
Поскольку граф модулей определяет значение операторов import, любые команды, которые загружают пакеты, также используют go.mod и поэтому могут его обновлять, включая go build, go get, go install, go list, go test, go mod tidy.
В Go 1.15 и ниже флаг -mod=mod был включен по умолчанию, поэтому обновления выполнялись автоматически. Начиная с Go 1.16, команда go действует так, как если бы был установлен флаг -mod=readonly: если требуются какие-либо изменения в go.mod, команда go сообщает об ошибке и предлагает исправление.
Выбор минимальной версии (MVS)
Go использует алгоритм под названием «Выбор минимальной версии» (MVS) для выбора набора версий модулей, которые будут использоваться при сборке пакетов. MVS подробно описан в статье «Выбор минимальной версии» Расса Кокса.
Концептуально MVS работает с направленным графом модулей, указанным в файлах go.mod. Каждая вершина графа представляет версию модуля. Каждая ребро представляет минимальную требуемую версию зависимости, указанную с помощью директивы require. Граф может быть изменен с помощью директив exclude и replace в файлах go.mod основных модулей и с помощью директив replace в файле go.work.
MVS генерирует список сборок в качестве вывода, то есть список версий модулей, используемых для сборки.
MVS начинает с основных модулей (специальных вершин в графе, не имеющих версии) и проходит по графу, отслеживая наивысшую требуемую версию каждого модуля. В конце прохождения наивысшие требуемые версии составляют список сборок: это минимальные версии, удовлетворяющие всем требованиям.
Список сборки можно просмотреть с помощью команды go list -m all. В отличие от других систем управления зависимостями, список сборки не сохраняется в файле «lock». MVS является детерминированным, и список сборки не изменяется при выпуске новых версий зависимостей, поэтому MVS используется для его вычисления в начале каждой команды, учитывающей модули.
Рассмотрим пример на диаграмме ниже. Основной модуль требует модуль A версии 1.2 или выше и модуль B версии 1.2 или выше. A 1.2 и B 1.2 требуют C 1.3 и C 1.4 соответственно. C 1.3 и C 1.4 требуют D 1.2.

MVS посещает и загружает файл go.mod для каждой из версий модуля, выделенных синим цветом. В конце обхода графа MVS возвращает список сборок, содержащий выделенные жирным шрифтом версии: A 1.2, B 1.2, C 1.4 и D 1.2. Обратите внимание, что доступны более высокие версии B и D, но MVS не выбирает их, поскольку они не требуются.
Replacement (Замена)
Содержимое модуля (включая его файл go.mod) может быть заменено с помощью директивы replace в файле go.mod главного модуля или в файле go.work рабочего пространства. Директива replace может применяться к определенной версии модуля или ко всем версиям модуля.
Замены изменяют граф модулей, так как заменяющий модуль может иметь другие зависимости, чем заменяемые версии.
Рассмотрим пример ниже, где C 1.4 был заменен на R. R зависит от D 1.3, а не от D 1.2, поэтому MVS возвращает список сборки, содержащий A 1.2, B 1.2, C 1.4 (замененный на R) и D 1.3.

Exclusion (Исключение)
Модуль также может быть исключен из определенных версий с помощью директивы exclude в файле go.mod главного модуля.
Исключения также изменяют граф модулей. Когда версия исключается, она удаляется из графа модулей, а требования к ней перенаправляются на следующую более высокую версию.
Рассмотрим пример ниже. Версия C 1.3 была исключена. MVS будет действовать так, как если бы A 1.2 требовал C 1.4 (следующую более высокую версию), а не C 1.3.

Upgrades (Обновления)
Команда go get может быть использована для обновления набора модулей. Чтобы выполнить обновление, команда go изменяет граф модулей перед запуском MVS, добавляя ребра от посещенных версий к обновленным.
Рассмотрим пример ниже. Модуль B может быть обновлен с 1.2 до 1.3, C может быть обновлен с 1.3 до 1.4, а D может быть обновлен с 1.2 до 1.3.

Обновления (и понижения) могут добавлять или удалять косвенные зависимости. В этом случае E 1.1 и F 1.1 появляются в списке сборки после обновления, поскольку E 1.1 требуется B 1.3.
Чтобы сохранить обновления, команда go обновляет требования в файле go.mod. Она изменит требования к B на версию 1.3. Она также добавит требования к C 1.4 и D 1.3 с // косвенными комментариями, поскольку в противном случае эти версии не были бы выбраны.
Downgrade
Команда go get также может быть использована для понижения рейтинга набора модулей. Чтобы выполнить понижение, команда go изменяет граф модулей, удаляя версии выше пониженных версий. Она также удаляет версии других модулей, которые зависят от удаленных версий, поскольку они могут быть несовместимы с пониженными версиями своих зависимостей. Если главный модуль требует версию модуля, удаленную в результате понижения, требование меняется на предыдущую версию, которая не была удалена. Если предыдущая версия недоступна, требование отменяется.
Рассмотрим приведенный ниже пример. Предположим, что была обнаружена проблема с C 1.4, поэтому мы понижаем версию до C 1.3. C 1.4 удаляется из графа модулей. B 1.2 также удаляется, поскольку для него требуется C 1.4 или выше. Требование главного модуля к B изменяется на 1.1.

go get также может полностью удалить зависимости, используя суффикс @none после аргумента. Это работает аналогично Downgrade. Все версии именованного модуля удаляются из графа модулей.
Обрезка графа модулей
Если основной модуль находится на версии go 1.17 или выше, граф модулей, используемый для выбора минимальной версии, включает только непосредственные требования для каждой зависимости модуля, которая указывает go 1.17 или выше в своем собственном файле go.mod, за исключением случаев, когда эта версия модуля также (транзитивно) требуется какой-либо другой зависимостью на версии go 1.16 или ниже. (Транзитивные зависимости зависимостей go 1.17 удаляются из графа модулей.)
Поскольку файл go.mod go 1.17 включает директиву require для каждой зависимости, необходимой для сборки любого пакета или теста в этом модуле, обрезанный граф модулей включает все зависимости, необходимые для сборки или тестирования пакетов в любой зависимости, явно требуемой основным модулем. Модуль, который не требуется для сборки какого-либо пакета или теста в данном модуле, не может повлиять на поведение его пакетов во время выполнения, поэтому зависимости, удаленные из графа модулей, могут вызвать только помехи между модулями, которые в противном случае не были бы связаны между собой.
Модули, требования которых были удалены, по-прежнему отображаются в графе модулей и по-прежнему отображаются в go list -m all: их выбранные версии известны и четко определены, и пакеты могут быть загружены из этих модулей (например, как транзитивные зависимости тестов, загруженных из других модулей). Однако, поскольку команда go не может легко определить, какие зависимости этих модулей удовлетворены, аргументы go build и go test не могут включать пакеты из модулей, требования которых были удалены. go get продвигает модуль, содержащий каждый названный пакет, в явную зависимость, позволяя вызывать go build или go test для этого пакета.
Поскольку Go 1.16 и более ранние версии не поддерживали удаление графа модулей, полное транзитивное закрытие зависимостей — включая транзитивные зависимости go 1.17 — по-прежнему включено для каждого модуля, который указывает go 1.16 или ниже. (В go 1.16 и ниже файл go.mod включает только прямые зависимости, поэтому для обеспечения включения всех косвенных зависимостей необходимо загрузить гораздо более крупный граф).
Файл go.sum, записанный go mod tidy для модуля по умолчанию, включает контрольные суммы, необходимые для версии Go, на одну ниже версии, указанной в директиве go. Таким образом, модуль go 1.17 включает контрольные суммы, необходимые для полного графа модулей, загруженного Go 1.16, но модуль go 1.18 будет включать только контрольные суммы, необходимые для обрезанного графа модулей, загруженного Go 1.17. Флаг -compat можно использовать для переопределения версии по умолчанию (например, для более агрессивной обрезки файла go.sum в модуле go 1.17).
Более подробную информацию см. в документе по проектированию.
Ленивая загрузка модуля
Более полные требования, добавленные для обрезки графа модуля, также позволяют еще одну оптимизацию при работе внутри модуля. Если основной модуль находится на уровне go 1.17 или выше, команда go избегает загрузки полного графа модуля, пока (и если) это не требуется. Вместо этого она загружает только файл go.mod главного модуля, а затем пытается загрузить пакеты, которые нужно собрать, используя только эти требования. Если импортируемый пакет (например, зависимость теста для пакета вне основного модуля) не найден среди этих требований, то остальная часть графа модулей загружается по требованию.
Если все импортируемые пакеты могут быть найдены без загрузки графа модулей, команда go загружает файлы go.mod только для модулей, содержащих эти пакеты, и их требования сверяются с требованиями основного модуля, чтобы убедиться, что они локально согласованы. (Несоответствия могут возникнуть из-за слияния версий, ручного редактирования и изменений в модулях, которые были заменены с использованием путей локальной файловой системы).
Workspaces (Рабочие пространства)
Рабочее пространство - это набор модулей на диске, которые используются в качестве основных модулей при запуске минимального выбора версий (MVS).
Рабочее пространство может быть объявлено в файле go.work, в котором указываются относительные пути к каталогам модулей каждого из модулей рабочего пространства. Если файл go.work отсутствует, рабочая область состоит из единственного модуля, содержащего текущий каталог.
Большинство подкоманд go, работающих с модулями, работают с набором модулей, определяемым текущим рабочим пространством. go mod init, go mod why, go mod edit, go mod tidy, go mod vendor и go get всегда работают с одним основным модулем.
Команда определяет, находится ли она в контексте рабочего пространства, сначала изучив переменную окружения GOWORK. Если GOWORK имеет значение off, команда будет находиться в одномодульном контексте.
Если она пуста или не указана, команда будет искать файл go.work в текущем рабочем каталоге, а затем в последовательных родительских каталогах.
Если файл найден, команда будет работать в определенном ею рабочем пространстве; в противном случае рабочее пространство будет включать только модуль, содержащий рабочий каталог.
Если GOWORK называет путь к существующему файлу, который заканчивается на .work, режим рабочего пространства будет включен.
Любое другое значение является ошибкой. Вы можете использовать команду go env GOWORK, чтобы определить, какой файл go.work использует команда go. go env GOWORK будет пустой, если команда go не находится в режиме рабочего пространства.
Файлы go.work
Рабочая область определяется текстовым файлом с кодировкой UTF-8 под названием go.work. Файл go.work ориентирован на строки. Каждая строка содержит одну директиву, состоящую из ключевого слова, за которым следуют аргументы. Например:
go 1.23.0
use ./my/first/thing
use ./my/second/thing
replace example.com/bad/thing v1.4.5 => example.com/good/thing v1.4.5
Как и в файлах go.mod, начальное ключевое слово может быть вынесено из соседних строк для создания блока.
use (
./my/first/thing
./my/second/thing
)
Команда go предоставляет несколько подкоманд для работы с файлами go.work.
go work init создает новые файлы go.work.
go work use добавляет каталоги модулей в файл go.work.
go work edit выполняет низкоуровневые изменения.
Пакет golang.org/x/mod/modfile может использоваться программами Go для программного внесения тех же изменений.
Команда go будет поддерживать файл go.work.sum, который отслеживает хэши, используемые рабочим пространством, которые не находятся в файлах go.sum модулей коллективного рабочего пространства.
Обычно не рекомендуется фиксировать файлы go.work в системах контроля версий по двум причинам:
- Зарегистрированный файл
go.work может переопределить собственный файл go.work разработчика из родительского каталога, что приведет к путанице, когда их директивы использования не будут применяться.
- Зарегистрированный файл
go.work может привести к тому, что система непрерывной интеграции (CI) выберет и, следовательно, протестирует неправильные версии зависимостей модуля. Системам CI, как правило, не следует разрешать использовать файл go.work, чтобы они могли тестировать поведение модуля так, как он будет использоваться при необходимости другими модулями, где файл go.work внутри модуля не имеет никакого эффекта.
Тем не менее, в некоторых случаях фиксация файла go.work имеет смысл. Например, когда модули в репозитории разрабатываются исключительно друг с другом, но не вместе с внешними модулями, у разработчика может не быть причин использовать другую комбинацию модулей в рабочей области. В этом случае автор модуля должен убедиться, что отдельные модули протестированы и выпущены надлежащим образом.
Лексические элементы
Лексические элементы в файлах go.work определяются точно так же, как и в файлах go.mod.
Грамматика
Синтаксис go.work указан ниже с использованием расширенной формы Бэкуса-Наура (EBNF). Подробнее о синтаксисе EBNF см. раздел “Нотация” в спецификации языка Go.
GoWork = { Directive } .
Directive = GoDirective |
ToolchainDirective |
UseDirective |
ReplaceDirective .
Новая строка, идентификаторы и строки обозначаются символами newline, ident и string, соответственно.
Пути и версии модулей обозначаются ModulePath и Version. Пути и версии модулей указываются точно так же, как и для файлов go.mod.
ModulePath = ident | string . /* see restrictions above */
Version = ident | string . /* see restrictions above */
go директива
Директива go необходима в правильном файле go.work. Версия должна быть действительной версией релиза Go: целое положительное число, за которым следует точка и целое неотрицательное число (например, 1.18, 1.19).
Директива go указывает на версию инструментария go, с которым должен работать файл go.work. Если в формат файла go.work будут внесены изменения, будущие версии цепочки инструментов будут интерпретировать файл в соответствии с указанной версией.
Файл go.work может содержать не более одной директивы go.
GoDirective = "go" GoVersion newline .
GoVersion = string | ident . /* действительная версия релиза; см. выше */
Пример:
Директива toolchain объявляет предлагаемую цепочку инструментов Go для использования в рабочем пространстве. Она действует только в том случае, если цепочка инструментов по умолчанию старше предлагаемой цепочки инструментов.
Подробнее см. в разделе “Цепочки инструментов Go”.
ToolchainDirective = "toolchain" ToolchainName newline .
ToolchainName = string | ident . /* действительное имя цепочки инструментов; см. раздел "Цепочки инструментов Go" */
Пример:
godebug директива
Директива godebug объявляет единственную настройку GODEBUG, которая должна применяться при работе в этом рабочем пространстве. Синтаксис и действие такие же, как у директивы godebug в файле go.mod. Когда рабочая область используется, директивы godebug в файлах go.mod игнорируются.
use директива
Директива use добавляет модуль на диск к набору основных модулей в рабочей области. Ее аргумент - относительный путь к директории, содержащей файл go.mod модуля. Директива use не добавляет модули, содержащиеся в подкаталогах каталога-аргумента. Эти модули могут быть добавлены в каталог, содержащий их файл go.mod, в отдельных директивах use.
UseDirective = "use" ( UseSpec | "(" newline { UseSpec } ")" newline ) .
UseSpec = FilePath newline .
FilePath = /* platform-specific relative or absolute file path */
Например:
use ./mymod // example.com/mymod
use (
../othermod
./subdir/thirdmod
)
replace директива
Подобно директиве replace в файле go.mod, директива replace в файле go.work заменяет содержимое определенной версии модуля или всех версий модуля на содержимое, найденное в другом месте. Директива replace в go.work отменяет замену в файле go.mod для конкретной версии.
Директивы replace в файлах go.work переопределяют любые замены того же модуля или версии модуля в модулях рабочего пространства.
ReplaceDirective = "replace" ( ReplaceSpec | "(" newline { ReplaceSpec } ")" newline ) .
ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline
| ModulePath [ Version ] "=>" ModulePath Version newline .
FilePath = /* platform-specific relative or absolute file path */
Например:
replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
replace (
golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
golang.org/x/net => example.com/fork/net v1.4.5
golang.org/x/net v1.2.3 => ./fork/net
golang.org/x/net => ./fork/net
)
Совместимость с немодульными репозиториями
Чтобы обеспечить плавный переход от GOPATH к модулям, команда go может загружать и собирать пакеты в модульно-ориентированном режиме из репозиториев, которые не перешли на модули, добавив файл go.mod.
Когда команда go загружает модуль заданной версии непосредственно из репозитория, она ищет URL репозитория для пути к модулю, сопоставляет версию с ревизией в репозитории, а затем извлекает архив репозитория с этой ревизией. Если путь модуля равен корневому пути репозитория, а корневой каталог репозитория не содержит файла go.mod, команда go синтезирует в кэше модулей файл go.mod, содержащий директиву модуля и ничего больше. Поскольку синтетические файлы go.mod не содержат директив require для своих зависимостей, другим модулям, зависящим от них, могут потребоваться дополнительные директивы require (с косвенными комментариями //), чтобы гарантировать, что каждая зависимость будет получена в одной и той же версии при каждой сборке.
Когда команда go загружает модуль с прокси, она загружает файл go.mod отдельно от остального содержимого модуля. Предполагается, что прокси-сервер предоставит синтетический файл go.mod, если у исходного модуля его не было.
+incompatible (Несовместимые версии)
Модуль, выпущенный в основной версии 2 или выше, должен иметь соответствующий суффикс основной версии в своем пути модуля. Например, если модуль выпущен в версии v2.0.0, его путь должен иметь суффикс /v2. Это позволяет команде go обрабатывать несколько основных версий проекта как отдельные модули, даже если они разработаны в одном репозитории.
Требование о суффиксе основной версии было введено, когда в команду go была добавлена поддержка модулей, и многие репозитории уже имели теги с основной версией 2 или выше до этого. Для обеспечения совместимости с этими репозиториями команда go добавляет суффикс +incompatible к версиям с основной версией 2 или выше без файла go.mod. +incompatible указывает, что версия является частью того же модуля, что и версии с более низкими номерами основных версий; следовательно, команда go может автоматически обновлять до более высоких версий +incompatible, даже если это может привести к сбою сборки.
Рассмотрим пример требования ниже:
require example.com/m v4.1.2+incompatible
Версия v4.1.2+incompatible относится к семантическому тегу версии v4.1.2 в репозитории, который предоставляет модуль example.com/m. Модуль должен находиться в корневом каталоге репозитория (то есть корневой путь репозитория также должен быть example.com/m), и файл go.mod не должен присутствовать. Модуль может иметь версии с более низкими номерами основных версий, такие как v1.5.2, и команда go может автоматически обновить их до v4.1.2+incompatible (см. минимальный выбор версии (MVS) для получения информации о том, как работают обновления).
Репозиторий, который переходит на модули после того, как версия v2.0.0 была помечена тегом, обычно должен выпустить новую основную версию. В приведенном выше примере автор должен создать модуль с путем example.com/m/v5 и выпустить версию v5.0.0. Автор также должен обновить импорт пакетов в модуле, чтобы использовать префикс example.com/m/v5 вместо example.com/m. Более подробный пример см. в разделе Go Modules: v2 and Beyond.
Обратите внимание, что суффикс +incompatible не должен появляться в теге в репозитории; тег типа v4.1.2+incompatible будет игнорироваться. Суффикс появляется только в версиях, используемых командой go. Подробнее о различиях между версиями и тегами см. в разделе Сопоставление версий с коммитами.
Обратите внимание, что суффикс +incompatible может появляться в псевдоверсиях. Например, v2.0.1-20200722182040-012345abcdef+incompatible может быть действительной псевдоверсией.
Минимальная совместимость модулей
Модуль, выпущенный в основной версии 2 или выше, должен иметь суффикс основной версии в своем пути модуля. Модуль может быть разработан в подкаталоге основной версии в своем репозитории, а может и нет. Это имеет значение для пакетов, которые импортируют пакеты внутри модуля при сборке в режиме GOPATH.
Обычно в режиме GOPATH пакет хранится в каталоге, соответствующем корневому пути его репозитория, соединенному с его каталогом в репозитории. Например, пакет в репозитории с корневым путем example.com/repo в подкаталоге sub будет храниться в $GOPATH/src/example.com/repo/sub и будет импортироваться как example.com/repo/sub.
Для модуля с суффиксом основной версии можно ожидать, что пакет example.com/repo/v2/sub будет находиться в каталоге $GOPATH/src/example.com/repo/v2/sub. Для этого модуль должен быть разработан в подкаталоге v2 своего репозитория. Команда go поддерживает это, но не требует (см. Сопоставление версий с коммитами).
Если модуль не разрабатывается в подкаталоге основной версии, то его каталог в GOPATH не будет содержать суффикс основной версии, и его пакеты могут быть импортированы без суффикса основной версии. В приведенном выше примере пакет будет находиться в каталоге $GOPATH/src/example.com/repo/sub и будет импортирован как example.com/repo/sub.
Это создает проблему для пакетов, предназначенных для сборки как в режиме модуля, так и в режиме GOPATH: режим модуля требует суффикса, а режим GOPATH — нет.
Чтобы исправить это, в Go 1.11 была добавлена минимальная совместимость модулей, которая была обратно портирована в Go 1.9.7 и 1.10.3. Когда путь импорта разрешается в каталог в режиме GOPATH:
-
При разрешении импорта в форме $modpath/$vn/$dir, где:
$modpath — действительный путь к модулю,
$vn — суффикс основной версии,
$dir — возможно пустой подкаталог,
-
Если выполняются все следующие условия:
- Пакет
$modpath/$vn/$dir отсутствует в любом соответствующем каталоге поставщика.
- Файл
go.mod присутствует в том же каталоге, что и импортирующий файл, или в любом родительском каталоге до корня $GOPATH/src.
- Не существует каталога
$GOPATH[i]/src/$modpath/$vn/$suffix (для любого корня $GOPATH[i]),
- Существует файл
$GOPATH[d]/src/$modpath/go.mod (для некоторого корня $GOPATH[d]), который объявляет путь к модулю как $modpath/$vn,
-
Тогда импорт $modpath/$vn/$dir разрешается в каталог $GOPATH[d]/src/$modpath/$dir.
Эти правила позволяют пакетам, которые были перенесены в модули, импортировать другие пакеты, которые были перенесены в модули при сборке в режиме GOPATH, даже если не использовался подкаталог основной версии.
Команды с поддержкой модулей
Большинство команд go могут работать в режиме с поддержкой модулей или в режиме GOPATH. В режиме с поддержкой модулей команда go использует файлы go.mod для поиска версионных зависимостей и обычно загружает пакеты из кэша модулей, скачивая модули, если они отсутствуют. В режиме GOPATH команда go игнорирует модули; она ищет зависимости в каталогах поставщиков и в GOPATH.
Начиная с Go 1.16, режим с поддержкой модулей включен по умолчанию, независимо от наличия файла go.mod. В более ранних версиях режим с поддержкой модулей включался, если файл go.mod присутствовал в текущем каталоге или любом родительском каталоге.
Режим с поддержкой модулей можно контролировать с помощью переменной среды GO111MODULE, которую можно установить в значение on, off или auto.
- Если
GO111MODULE=off, команда go игнорирует файлы go.mod и работает в режиме GOPATH.
- Если
GO111MODULE=on или не установлено, команда go работает в режиме распознавания модулей, даже если файл go.mod отсутствует. Не все команды работают без файла go.mod: см. Команды модулей вне модуля.
- Если
GO111MODULE=auto, команда go работает в режиме с поддержкой модулей, если файл go.mod присутствует в текущем каталоге или любом родительском каталоге. В Go 1.15 и ниже это было поведением по умолчанию. Подкоманды go mod и go install с запросом версии работают в режиме с поддержкой модулей, даже если файл go.mod отсутствует.
В режиме распознавания модулей GOPATH больше не определяет значение импортов во время сборки, но по-прежнему хранит загруженные зависимости (в GOPATH/pkg/mod; см. Кэш модулей) и установленные команды (в GOPATH/bin, если не установлен GOBIN).
(builds) Команды сборки
Все команды, которые загружают информацию о пакетах, учитывают модули. К ним относятся:
go build
go fix
go generate
go install
go list
go run
go test
go vet
При запуске в режиме распознавания модулей эти команды используют файлы go.mod для интерпретации путей импорта, указанных в командной строке или записанных в исходных файлах Go. Эти команды принимают следующие флаги, общие для всех команд модулей.
- Флаг
-mod контролирует, может ли go.mod обновляться автоматически и используется ли каталог vendor.
-mod=mod указывает команде go игнорировать каталог vendor и автоматически обновлять go.mod, например, когда импортируемый пакет не предоставляется ни одним из известных модулей.
-mod=readonly указывает команде go игнорировать каталог vendor и сообщать об ошибке, если go.mod необходимо обновить.
-mod=vendor указывает команде go использовать каталог vendor. В этом режиме команда go не будет использовать сеть или кэш модулей.
- По умолчанию, если версия
go в go.mod равна 1.14 или выше и присутствует каталог vendor, команда go действует так, как если бы был использован флаг -mod=vendor. В противном случае команда go действует так, как если бы был использован флаг -mod=readonly.
go get отклоняет этот флаг, поскольку цель команды — изменить зависимости, что разрешено только с помощью -mod=mod.
- Флаг
-modcacherw указывает команде go создавать новые каталоги в кэше модулей с правами на чтение и запись, а не делать их доступными только для чтения. При постоянном использовании этого флага (обычно путем установки GOFLAGS=-modcacherw в среде или запуска go env -w GOFLAGS=-modcacherw) кэш модулей можно удалить с помощью таких команд, как rm -r, без предварительного изменения прав доступа. Команда go clean -modcache может использоваться для удаления кэша модулей, независимо от того, использовался ли флаг -modcacherw.
- Флаг
-modfile=file.mod указывает команде go читать (и, возможно, записывать) альтернативный файл вместо go.mod в корневом каталоге модулей. Имя файла должно заканчиваться на .mod. Файл с именем go.mod по-прежнему должен присутствовать, чтобы определить корневой каталог модулей, но доступ к нему не осуществляется. Когда указан -modfile, также используется альтернативный файл go.sum: его путь получается из флага -modfile путем удаления расширения .mod и добавления .sum.
Vendoring
При использовании модулей команда go обычно удовлетворяет зависимости, загружая модули из их источников в кэш модулей, а затем загружая пакеты из этих загруженных копий. Vendoring может использоваться для обеспечения взаимодействия со старыми версиями Go или для обеспечения хранения всех файлов, используемых для сборки, в одном дереве файлов.
Команда go mod vendor создает каталог с именем vendor в корневом каталоге главного модуля, содержащий копии всех пакетов, необходимых для сборки и тестирования пакетов в главном модуле. Пакеты, которые импортируются только тестами пакетов вне главного модуля, не включаются. Как и в случае с go mod tidy и другими командами модулей, ограничения сборки, за исключением ignore, не учитываются при создании каталога vendor.
go mod vendor также создает файл vendor/modules.txt, который содержит список пакетов vendor и версии модулей, из которых они были скопированы. Когда vendoring включен, этот манифест используется в качестве источника информации о версии модуля, как сообщается go list -m и go version -m. Когда команда go читает vendor/modules.txt, она проверяет, что версии модулей соответствуют go.mod. Если go.mod изменился с момента создания vendor/modules.txt, команда go сообщит об ошибке. go mod vendor следует запустить снова, чтобы обновить каталог vendor.
Если каталог vendor присутствует в корневом каталоге основного модуля, он будет использоваться автоматически, если версия go в файле go.mod основного модуля равна 1.14 или выше. Чтобы явно включить vendoring, вызовите команду go с флагом -mod=vendor. Чтобы отключить vendoring, используйте флаг -mod=readonly или -mod=mod.
Когда vendoring включен, команды сборки, такие как go build и go test, загружают пакеты из каталога vendor вместо доступа к сети или локальному кэшу модулей. Команда go list -m выводит только информацию о модулях, перечисленных в go.mod. Команды go mod, такие как go mod download и go mod tidy, не работают по-другому, когда vendoring включен, и по-прежнему загружают модули и обращаются к кэшу модулей. go get также не работает по-другому, когда vendoring включен.
В отличие от вендоринга в режиме GOPATH, команда go игнорирует каталоги vendor, расположенные не в корневом каталоге основного модуля. Кроме того, поскольку каталоги vendor в других модулях не используются, команда go не включает каталоги vendor при сборке zip-файлов модулей (но см. известные ошибки #31562 и #37397).
go get
Использование:
go get [-d] [-t] [-u] [build flags] [packages]
Примеры:
# Upgrade a specific module.
$ go get golang.org/x/net
# Upgrade modules that provide packages imported by packages in the main module.
$ go get -u ./...
# Upgrade or downgrade to a specific version of a module.
$ go get golang.org/x/text@v0.3.2
# Update to the commit on the module's master branch.
$ go get golang.org/x/text@master
# Remove a dependency on a module and downgrade modules that require it
# to versions that don't require it.
$ go get golang.org/x/text@none
# Upgrade the minimum required Go version for the main module.
$ go get go
# Upgrade the suggested Go toolchain, leaving the minimum Go version alone.
$ go get toolchain
# Upgrade to the latest patch release of the suggested Go toolchain.
$ go get toolchain@patch
Команда go get обновляет зависимости модулей в файле go.mod для основного модуля, а затем собирает и устанавливает пакеты, указанные в командной строке.
Первый шаг — определить, какие модули необходимо обновить.
go get принимает в качестве аргументов список пакетов, шаблоны пакетов и пути к модулям.
- Если указан аргумент пакета,
go get обновляет модуль, который предоставляет этот пакет.
- Если указан шаблон пакета (например,
all или путь с подстановочным символом …), go get расширяет шаблон до набора пакетов, а затем обновляет модули, которые предоставляют эти пакеты.
- Если аргумент указывает модуль, но не пакет (например, модуль
golang.org/x/net не имеет пакета в своем корневом каталоге), go get обновит модуль, но не скомпилирует пакет.
- Если аргументы не указаны,
go get действует так, как если бы был указан (пакет в текущем каталоге); это можно использовать вместе с флагом -u для обновления модулей, которые предоставляют импортированные пакеты.
Каждый аргумент может включать суффикс запроса версии, указывающий желаемую версию, как в go get golang.org/x/text@v0.3.0. Суффикс запроса версии состоит из символа @, за которым следует запрос версии, который может указывать конкретную версию (v0.3.0), префикс версии (v0.3), имя ветки или тега (master), ревизию (1234abcd) или один из специальных запросов latest, upgrade, patch или none. Если версия не указана, go get использует запрос @upgrade.
После того как go get разрешит свои аргументы для конкретных модулей и версий, go get добавит, изменит или удалит директивы require в файле go.mod основного модуля, чтобы обеспечить сохранение модулей в желаемых версиях в будущем. Обратите внимание, что требуемые версии в файлах go.mod являются минимальными версиями и могут быть автоматически увеличены при добавлении новых зависимостей. См. раздел «Выбор минимальной версии (MVS)» для получения подробной информации о том, как версии выбираются и конфликты разрешаются командами, учитывающими модули.
Другие модули могут быть обновлены, когда модуль, указанный в командной строке, добавляется, обновляется или понижается, если новая версия указанного модуля требует другие модули более высоких версий. Например, предположим, что модуль example.com/a обновлен до версии v1.5.0, и эта версия требует модуль example.com/b версии v1.2.0. Если в настоящее время требуется модуль example.com/b версии v1.1.0, то go get example.com/a@v1.5.0 также обновит example.com/b до v1.2.0.

Другие модули могут быть понижены, когда модуль, названный в командной строке, понижается или удаляется. Продолжая приведенный выше пример, предположим, что модуль example.com/b понижен до версии v1.1.0. Модуль example.com/a также будет понижен до версии, которая требует example.com/b версии v1.1.0 или ниже.

Требование модуля может быть удалено с помощью суффикса версии @none. Это особый вид понижения версии. Модули, которые зависят от удаленного модуля, будут понижены или удалены по мере необходимости. Требование модуля может быть удалено, даже если один или несколько его пакетов импортируются пакетами в основном модуле. В этом случае следующая команда сборки может добавить новое требование модуля.
Если модуль требуется в двух разных версиях (явно указанных в аргументах командной строки или для удовлетворения требований обновлений и понижений версии), go get выдаст ошибку.
После того, как go get выбрал новый набор версий, он проверяет, не были ли отозваны или не стали ли устаревшими какие-либо вновь выбранные версии модулей или какие-либо модули, предоставляющие пакеты, указанные в командной строке. go get выводит предупреждение для каждой найденной отозванной версии или устаревшего модуля. go list -m -u all можно использовать для проверки отзывов и устаревания во всех зависимостях.
После обновления файла go.mod go get собирает пакеты, указанные в командной строке. Исполняемые файлы будут установлены в каталог, указанный переменной окружения GOBIN, которая по умолчанию равна $GOPATH/bin или $HOME/go/bin, если переменная окружения GOPATH не установлена.
go get поддерживает следующие флаги:
- Флаг
-d указывает go get не собирать и не устанавливать пакеты. При использовании -d go get будет только управлять зависимостями в go.mod. Использование go get без -d для сборки и установки пакетов является устаревшим (начиная с Go 1.17). В Go 1.18 -d будет всегда включен.
- Флаг
-u указывает go get обновить модули, предоставляющие пакеты, импортируемые прямо или косвенно пакетами, указанными в командной строке. Каждый модуль, выбранный с помощью -u, будет обновлен до последней версии, если он уже не требуется в более поздней версии (предварительная версия).
- Флаг
-u=patch (не -u patch) также указывает go get обновить зависимости, но go get обновит каждую зависимость до последней патч-версии (аналогично запросу версии @patch).
- Флаг
-t указывает go get учитывать модули, необходимые для сборки тестов пакетов, указанных в командной строке. При совместном использовании -t и -u go get также обновит тестовые зависимости.
- Флаг
-insecure больше не следует использовать. Он позволяет go get разрешать пользовательские пути импорта и загружать из репозиториев и прокси-серверов модулей, используя небезопасные схемы, такие как HTTP. Переменная среды GOINSECURE обеспечивает более тонкий контроль и должна использоваться вместо него.
go get больше ориентирован на управление требованиями в go.mod. Флаг -d является устаревшим, и в Go 1.18 он будет всегда включен.
go install
Начиная с Go 1.16, go install является рекомендуемой командой для сборки и установки программ. При использовании с суффиксом версии (например, @latest или @v1.4.6) go install собирает пакеты в режиме с учетом модулей, игнорируя файл go.mod в текущем каталоге или любом родительском каталоге, если таковой имеется.
Использование:
go install [build flags] [packages]
Примеры:
# Install the latest version of a program,
# ignoring go.mod in the current directory (if any).
$ go install golang.org/x/tools/gopls@latest
# Install a specific version of a program.
$ go install golang.org/x/tools/gopls@v0.6.4
# Install a program at the version selected by the module in the current directory.
$ go install golang.org/x/tools/gopls
# Install all programs in a directory.
$ go install ./cmd/...
Команда go install собирает и устанавливает пакеты, указанные в командной строке. Исполняемые файлы (основные пакеты) устанавливаются в каталог, указанный переменной среды GOBIN, которая по умолчанию равна $GOPATH/bin или $HOME/go/bin, если переменная среды GOPATH не задана. Исполняемые файлы в $GOROOT устанавливаются в $GOROOT/bin или $GOTOOLDIR вместо $GOBIN. Неисполняемые пакеты собираются и кэшируются, но не устанавливаются.
Начиная с Go 1.16, если аргументы имеют суффиксы версии (такие как @latest или @v1.0.0), go install собирает пакеты в режиме с учетом модулей, игнорируя файл go.mod в текущем каталоге или любом родительском каталоге, если таковой имеется. Это полезно для установки исполняемых файлов без влияния на зависимости основного модуля.
Чтобы устранить неоднозначность в отношении того, какие версии модулей используются в сборке, аргументы должны удовлетворять следующим ограничениям:
- Аргументы должны быть путями к пакетам или шаблонами пакетов (с подстановочными знаками
«...»). Они не должны быть стандартными пакетами (такими как fmt), меташаблонами (std, cmd, all, work, tool) или относительными или абсолютными путями к файлам.
- Все аргументы должны иметь одинаковый суффикс версии. Различные запросы не допускаются, даже если они относятся к одной и той же версии.
- Все аргументы должны относиться к пакетам в одном и том же модуле с одной и той же версией.
- Аргументы пути к пакету должны относиться к основным пакетам. Аргументы шаблона будут соответствовать только основным пакетам.
- Ни один модуль не считается основным модулем.
- Если модуль, содержащий пакеты, указанные в командной строке, имеет файл
go.mod, он не должен содержать директивы (replace и exclude), которые привели бы к его интерпретации по-другому, если бы он был основным модулем.
- Модуль не должен требовать более высокой версии самого себя.
- Каталоги поставщиков не используются ни в одном модуле. (Каталоги поставщиков не включаются в zip-файлы модулей, поэтому
go install не загружает их).
См. Запросы версии для поддерживаемого синтаксиса запросов версии. Go 1.15 и ниже не поддерживали использование запросов версии с go install.
Если аргументы не имеют суффиксов версии, go install может работать в режиме с поддержкой модулей или в режиме GOPATH, в зависимости от переменной среды GO111MODULE и наличия файла go.mod. Подробности см. в разделе Команды с поддержкой модулей. Если режим с поддержкой модулей включен, go install работает в контексте основного модуля, который может отличаться от модуля, содержащего устанавливаемый пакет.
go list -m
Использование:
go list -m [-u] [-retracted] [-versions] [list flags] [modules]
Примеры:
$ go list -m all
$ go list -m -versions example.com/m
$ go list -m -json example.com/m@latest
Флаг -m заставляет go list перечислять модули, а не пакеты. В этом режиме аргументами go list могут быть модули, шаблоны модулей (содержащие подстановочный знак ...), запросы версий или специальный шаблон all, который соответствует всем модулям в списке сборки. Если аргументы не указаны, то в списке будет указан главный модуль.
При перечислении модулей флаг -f по-прежнему задает шаблон формата, применяемый к структуре Go, но теперь уже к структуре Module:
type Module struct {
Path string // путь к модулю
Version string // версия модуля
Versions []string // доступные версии модуля
Replace *Module // заменяется этим модулем
Time *time.Time // время создания версии
Update *Module // доступное обновление (с -u)
Main bool // является ли это главным модулем?
Indirect bool // модуль требуется основному модулю только косвенно
Dir string // каталог, содержащий локальную копию файлов, если таковая имеется
GoMod string // путь к файлу go.mod, описывающему модуль, если таковой имеется
GoVersion string // версия go, используемая в модуле
Retracted []string // информация об отзыве, если таковая имеется (с -retracted или -u)
Deprecated string // сообщение об устаревании, если есть (с -u)
Error *ModuleError // ошибка при загрузке модуля
}
type ModuleError struct {
Err string // сама ошибка
}
По умолчанию выводится путь к модулю, а затем информация о версии и замене, если таковая имеется. Например, go list -m all может вывести:
example.com/main/module
golang.org/x/net v0.1.0
golang.org/x/text v0.3.0 => /tmp/text
rsc.io/pdf v0.1.1
В структуре Module есть метод String, который форматирует эту строку вывода, так что формат по умолчанию эквивалентен -f '{{.String}}'.
Обратите внимание, что если модуль был заменен, его поле Replace описывает заменяющий модуль, а его поле Dir устанавливается в исходный код заменяющего модуля, если он присутствует. (То есть, если Replace не имеет значения nil, то Dir устанавливается в Replace.Dir, без доступа к исходному коду замененного модуля).
Флаг -u добавляет информацию о доступных обновлениях. Если последняя версия данного модуля новее текущей, list -u устанавливает в поле Update модуля информацию о более новом модуле.
list -u также выводит, является ли выбранная в данный момент версия отмененной и является ли модуль устаревшим. Метод String модуля указывает на доступное обновление путем форматирования новой версии в скобках после текущей версии.
Например, go list -m -u all может вывести:
example.com/main/module
golang.org/x/old v1.9.9 (deprecated)
golang.org/x/net v0.1.0 (retracted) [v0.2.0]
golang.org/x/text v0.3.0 [v0.4.0] => /tmp/text
rsc.io/pdf v0.1.1 [v0.1.2]
(Для инструментов может быть удобнее использовать команду go list -m -u -json all для анализа.)
Флаг -versions заставляет list установить поле Versions модуля в список всех известных версий этого модуля, упорядоченных в соответствии с семантической версией, от самой низкой к самой высокой. Флаг также изменяет формат вывода по умолчанию, чтобы отобразить путь к модулю, за которым следует список версий, разделенных пробелами. Отмененные версии опускаются из этого списка, если не указан флаг -retracted.
Флаг -retracted указывает list показывать отозванные версии в списке, выведенном с помощью флага -versions, и учитывать отозванные версии при разрешении запросов версий.
Например, go list -m -retracted example.com/m@latest показывает самую последнюю версию или предрелизную версию модуля example.com/m, даже если эта версия отозвана. Директивы retract и deprecations загружаются из файла go.mod в этой версии. Флаг -retracted был добавлен в Go 1.16.
Шаблонная функция module принимает один строковый аргумент, который должен быть путем к модулю или запросом, и возвращает указанный модуль в виде структуры Module. Если происходит ошибка, результатом будет структура Module с полем Error, не равным nil.
go mod download
Использование:
go mod download [-x] [-json] [-reuse=old.json] [modules]
Например:
$ go mod download
$ go mod download golang.org/x/mod@v0.2.0
Команда go mod download загружает именованные модули в кэш модулей. Аргументами могут быть пути к модулям или шаблоны модулей, выбирающие зависимости основного модуля, или запросы версий вида path@version. При отсутствии аргументов загрузка применяется ко всем зависимостям основного модуля.
Команда go будет автоматически загружать модули по мере необходимости во время обычного выполнения. Команда go mod download полезна в основном для предварительного заполнения кэша модулей или для загрузки данных, которые будут обслуживаться прокси-модулем.
По умолчанию команда загрузки ничего не пишет в стандартный вывод. Она печатает сообщения о ходе выполнения и ошибках в стандартную ошибку.
Флаг -json заставляет команду download выводить на стандартный вывод последовательность JSON-объектов, описывающих каждый загруженный модуль (или сбой), соответствующий данной Go-структуре:
type Module struct {
Path string // module path
Query string // version query corresponding to this version
Version string // module version
Error string // error loading module
Info string // absolute path to cached .info file
GoMod string // absolute path to cached .mod file
Zip string // absolute path to cached .zip file
Dir string // absolute path to cached source root directory
Sum string // checksum for path, version (as in go.sum)
GoModSum string // checksum for go.mod (as in go.sum)
Origin any // provenance of module
Reuse bool // reuse of old module info is safe
}
Флаг -x заставляет download выводить команды, выполняемые download, в стандартную ошибку.
Флаг -reuse принимает имя файла, содержащего JSON-вывод предыдущего вызова ‘go mod download -json’. Команда go может использовать этот файл, чтобы определить, что модуль не изменился с момента предыдущего вызова, и избежать его повторной загрузки. Модули, которые не будут перезагружены, будут отмечены в новом выводе установкой поля Reuse в true. Обычно кэш модулей обеспечивает такое повторное использование автоматически; флаг -reuse может быть полезен в системах, не сохраняющих кэш модулей.
go mod edit
Использование:
go mod edit [editing flags] [-fmt|-print|-json] [go.mod]
Например:
# Add a replace directive.
$ go mod edit -replace example.com/a@v1.0.0=./a
# Remove a replace directive.
$ go mod edit -dropreplace example.com/a@v1.0.0
# Set the go version, add a requirement, and print the file
# instead of writing it to disk.
$ go mod edit -go=1.14 -require=example.com/m@v1.0.0 -print
# Format the go.mod file.
$ go mod edit -fmt
# Format and print a different .mod file.
$ go mod edit -print tools.mod
# Print a JSON representation of the go.mod file.
$ go mod edit -json
Команда go mod edit предоставляет интерфейс командной строки для редактирования и форматирования файлов go.mod, предназначенный в первую очередь для использования инструментами и скриптами. go mod edit читает только один файл go.mod; он не ищет информацию о других модулях. По умолчанию go mod edit читает и записывает файл go.mod основного модуля, но после флагов редактирования можно указать другой целевой файл.
Флаги редактирования определяют последовательность операций редактирования.
- Флаг
-module изменяет путь модуля (строку модуля в файле go.mod).
- Флаг
-go=version устанавливает ожидаемую версию языка Go.
- Флаги
-require=path@version и -droprequire=path добавляют и удаляют требование к указанному пути и версии модуля. Обратите внимание, что -require переопределяет все существующие требования к пути. Эти флаги в основном предназначены для инструментов, которые понимают граф модулей. Пользователям следует отдавать предпочтение go get path@version или go get path@none, которые производят другие настройки go.mod по мере необходимости, чтобы удовлетворить ограничения, налагаемые другими модулями. См. go get.
- Флаги
-exclude=path@version и -dropexclude=path@version добавляют и удаляют исключение для указанного пути модуля и версии. Обратите внимание, что -exclude=path@version не выполняется, если такое исключение уже существует.
- Флаг
-replace=old[@v]=new[@v] добавляет замену указанной пары пути модуля и версии. Если @v в old@v опущено, добавляется замена без версии в левой части, которая применяется ко всем версиям старого пути модуля. Если @v в new@v опущено, новый путь должен быть локальным корневым каталогом модуля, а не путем модуля. Обратите внимание, что -replace переопределяет любые избыточные замены для old[@v], поэтому опущение @v удалит замены для определенных версий.
- Флаг
-dropreplace=old[@v] отменяет замену заданного пути модуля и пары версий. Если @v указан, то отменяется замена с заданной версией. Существующая замена без версии в левой части может по-прежнему заменять модуль. Если @v опущен, то отменяется замена без версии.
- Флаги
-retract=version и -dropretract=version добавляют и удаляют отзыв для заданной версии, которая может быть одной версией (например, v1.2.3) или интервалом (например, [v1.1.0,v1.2.0]). Обратите внимание, что флаг -retract не может добавлять комментарий с обоснованием для директивы отзыва. Комментарии с обоснованием рекомендуются и могут отображаться с помощью go list -m -u и других команд.
- Флаги
-tool=path и -droptool=path добавляют и удаляют директиву tool для указанных путей. Обратите внимание, что это не добавит необходимые зависимости в граф сборки. Пользователям рекомендуется использовать go get -tool path для добавления инструмента или go get -tool path@none для его удаления.
Флаги редактирования могут повторяться. Изменения применяются в указанном порядке.
go mod edit имеет дополнительные флаги, которые контролируют его вывод.
- Флаг
-fmt переформатирует файл go.mod без внесения других изменений. Это переформатирование также подразумевается любыми другими изменениями, которые используют или переписывают файл go.mod. Этот флаг нужен только в том случае, если не указаны другие флаги, как в go mod edit -fmt.
- Флаг
-print выводит окончательный файл go.mod в текстовом формате вместо записи обратно на диск.
- Флаг
-json выводит окончательный файл go.mod в формате JSON вместо записи обратно на диск в текстовом формате. Вывод JSON соответствует следующим типам Go:
type Module struct {
Path string
Version string
}
type GoMod struct {
Module ModPath
Go string
Require []Require
Exclude []Module
Replace []Replace
Retract []Retract
}
type ModPath struct {
Path string
Deprecated string
}
type Require struct {
Path string
Version string
Indirect bool
}
type Replace struct {
Old Module
New Module
}
type Retract struct {
Low string
High string
Rationale string
}
type Tool struct {
Path string
}
Обратите внимание, что здесь описывается только сам файл go.mod, а не другие модули, на которые есть косвенные ссылки. Для получения полного набора модулей, доступных для сборки, используйте go list -m -json all. См. go list -m.
Например, инструмент может получить файл go.mod в виде структуры данных, разобрав вывод go mod edit -json, а затем внести изменения, вызвав go mod edit с параметрами -require, -exclude и так далее.
Инструменты также могут использовать пакет golang.org/x/mod/modfile для разбора, редактирования и форматирования файлов go.mod.
go mod graph
Использование:
go mod graph [-go=version]
Команда go mod graph выводит граф требований к модулю (с примененными заменами) в текстовом виде.
Например:
example.com/main example.com/a@v1.1.0
example.com/main example.com/b@v1.2.0
example.com/a@v1.1.0 example.com/b@v1.1.1
example.com/a@v1.1.0 example.com/c@v1.3.0
example.com/b@v1.1.0 example.com/c@v1.1.0
example.com/b@v1.2.0 example.com/c@v1.2.0
Каждая вершина в графе модуля представляет собой определенную версию модуля. Каждое ребро в графе представляет собой требование к минимальной версии зависимости.
go mod graph выводит ребра графа по одному в строке. Каждая строка содержит два поля, разделенных пробелами: версию модуля и одну из его зависимостей. Каждая версия модуля идентифицируется как строка вида path@version. Главный модуль не имеет суффикса @version, так как у него нет версии.
Флаг -go заставляет go mod graph сообщать о том, что граф модуля загружен данной версией Go, а не версией, указанной директивой go в файле go.mod.
Дополнительные сведения о том, как выбираются версии, см. в разделе Минимальный выбор версии (MVS). См. также go list -m для печати выбранных версий и go mod why для понимания того, зачем нужен тот или иной модуль.
go mod init
Использование:
go mod init [module-path]
Пример:
go mod init
go mod init example.com/m
Команда go mod init инициализирует и записывает новый файл go.mod в текущем каталоге, фактически создавая новый модуль, укорененный в текущем каталоге. Файл go.mod не должен уже существовать.
init принимает один необязательный аргумент - путь к модулю для нового модуля. Инструкции по выбору пути к модулю см. в разделе Пути к модулю. Если аргумент путь к модулю опущен, init попытается определить путь к модулю, используя комментарии импорта в файлах .go и текущий каталог (если он есть в GOPATH).
go mod tidy
Использование:
go mod tidy [-e] [-v] [-x] [-diff] [-go=версия] [-compat=версия]
go mod tidy обеспечивает соответствие файла go.mod исходному коду в модуле. Он добавляет все недостающие требования к модулям, необходимые для сборки пакетов и зависимостей текущего модуля, и удаляет требования к модулям, которые не предоставляют никаких соответствующих пакетов. Он также добавляет все недостающие записи в go.sum и удаляет ненужные записи.
- Флаг
-e (добавлен в Go 1.16) заставляет go mod tidy пытаться продолжить работу, несмотря на ошибки, возникшие при загрузке пакетов.
- Флаг
-v заставляет go mod tidy выводить информацию об удаленных модулях в стандартный вывод ошибок.
- Флаг
-x заставляет go mod tidy выводить команды, которые выполняет tidy.
- Флаг
-diff заставляет go mod tidy не изменять go.mod или go.sum, а вместо этого выводить необходимые изменения в виде унифицированного diff. Он завершается с кодом, отличным от нуля, если diff не пустой.
go mod tidy работает, загружая все пакеты в основном модуле, все его инструменты и все пакеты, которые они импортируют, рекурсивно. Сюда входят пакеты, импортированные тестами (включая тесты в других модулях).
go mod tidy действует так, как если бы все теги сборки были включены, поэтому он будет учитывать платформозависимые исходные файлы и файлы, которые требуют настраиваемых тегов сборки, даже если эти исходные файлы обычно не собираются.
Есть одно исключение: тег сборки ignore не включен, поэтому файл с ограничением сборки // +build ignore не будет учитываться.
Обратите внимание, что go mod tidy не будет учитывать пакеты в главном модуле в каталогах с именем testdata или с именами, начинающимися с . или _, если эти пакеты явно не импортируются другими пакетами.
После загрузки этого набора пакетов go mod tidy убеждается, что каждый модуль, предоставляющий один или несколько пакетов, имеет директиву require в файле go.mod основного модуля или — если основной модуль находится на go 1.16 или ниже — требуется другим необходимым модулем.
go mod tidy добавит требование к последней версии каждого отсутствующего модуля (определение последней версии см. в разделе «Запросы версии»).
go mod tidy удалит директивы require для модулей, которые не предоставляют никаких пакетов в описанном выше наборе.
go mod tidy также может добавлять или удалять // косвенные комментарии к директивам require.
// Косвенный комментарий обозначает модуль, который не предоставляет пакет, импортируемый пакетом в основном модуле. (См. директиву require для получения более подробной информации о том, когда добавляются // косвенные зависимости и комментарии).
Если установлен флаг -go, go mod tidy обновит директиву go до указанной версии, включив или отключив обрезку графа модулей и отложенную загрузку модулей (а также добавив или удалив косвенные требования по мере необходимости) в соответствии с этой версией.
По умолчанию go mod tidy проверяет, что выбранные версии модулей не изменяются при загрузке графа модулей версией Go, непосредственно предшествующей версии, указанной в директиве go. Версию, проверяемую на совместимость, также можно явно указать с помощью флага -compat.
go mod vendor
Использование:
go mod vendor [-e] [-v] [-o]
Команда go mod vendor создает в корневом каталоге основного модуля каталог с именем vendor, который содержит копии всех пакетов, необходимых для поддержки сборки и тестирования пакетов в основном модуле. Пакеты, которые импортируются только тестами пакетов вне основного модуля, не включаются. Как и в случае с go mod tidy и другими командами модуля, при создании каталога vendor не учитываются ограничения сборки, за исключением ignore.
Когда vendoring включен, команда go загружает пакеты из каталога vendor вместо того, чтобы загружать модули из их источников в кэш модулей и использовать пакеты из этих загруженных копий. Дополнительную информацию см. в разделе Vendoring.
go mod vendor также создает файл vendor/modules.txt, который содержит список пакетов vendored и версии модулей, из которых они были скопированы.
Когда vendoring включен, этот манифест используется в качестве источника информации о версии модуля, как сообщается go list -m и go version -m. Когда команда go читает vendor/modules.txt, она проверяет, что версии модулей соответствуют go.mod.
Если go.mod изменился с момента создания vendor/modules.txt, go mod vendor следует запустить снова.
Обратите внимание, что go mod vendor удаляет каталог vendor, если он существует, перед его повторным созданием. Не следует вносить локальные изменения в пакеты, включенные в vendor. Команда go не проверяет, не были ли изменены пакеты в каталоге vendor, но можно проверить целостность каталога vendor, запустив go mod vendor и убедившись, что никаких изменений не было.
- Флаг
-e (добавлен в Go 1.16) заставляет go mod vendor пытаться продолжить работу, несмотря на ошибки, возникшие при загрузке пакетов.
- Флаг
-v заставляет go mod vendor выводить имена модулей и пакетов vendor в стандартный поток ошибок.
- Флаг
-o (добавлен в Go 1.18) заставляет go mod vendor выводить дерево vendor в указанном каталоге вместо vendor. Аргументом может быть либо абсолютный путь, либо путь относительно корня модуля.
go mod verify
Использование:
go mod verify проверяет, что зависимости основного модуля, хранящиеся в кэше модулей, не были изменены с момента их загрузки.
Для выполнения этой проверки go mod verify хеширует каждый загруженный файл .zip модуля и извлеченный каталог, а затем сравнивает эти хеши с хешем, записанным при первой загрузке модуля.
go mod verify проверяет каждый модуль в списке сборки (который можно вывести с помощью go list -m all).
Если все модули не изменены, go mod verify выводит сообщение «all modules verified» (все модули проверены). В противном случае он сообщает, какие модули были изменены, и завершает работу с ненулевым статусом.
Обратите внимание, что все команды, поддерживающие модули, проверяют, что хэши в файле go.sum основного модуля совпадают с хэшами, записанными для модулей, загруженных в кэш модулей. Если хеш отсутствует в go.sum (например, потому что модуль используется впервые), команда go проверяет его хеш с помощью базы данных контрольных сумм (если путь к модулю не соответствует GOPRIVATE или GONOSUMDB). Подробности см. в разделе Аутентификация модулей.
В отличие от этого, go mod verify проверяет, что файлы .zip модулей и их извлеченные каталоги имеют хеши, которые соответствуют хешам, записанным в кэше модулей при их первоначальной загрузке.
Это полезно для обнаружения изменений в файлах в кэше модулей после загрузки и проверки модуля.
go mod verify не загружает содержимое для модулей, которые нет в кэше, и не использует файлы go.sum для проверки содержимого модулей. Однако go mod verify может загружать файлы go.mod для выполнения минимального выбора версии. Он будет использовать go.sum для проверки этих файлов и может добавлять записи go.sum для отсутствующих хэшей.
go mod why
Использование:
go mod why [-m] [-vendor] packages...
go mod why показывает кратчайший путь в графе импорта от главного модуля до каждого из перечисленных пакетов.
Вывод представляет собой последовательность строк, по одной для каждого пакета или модуля, указанного в командной строке, разделенных пустыми строками. Каждая строка начинается с комментария, начинающегося с #, в котором указан целевой пакет или модуль.
Последующие строки содержат путь через граф импорта, по одному пакету в строке. Если пакет или модуль не упоминается в основном модуле, в строке будет отображаться одно примечание в скобках, указывающее на этот факт.
Например:
$ go mod why golang.org/x/text/language golang.org/x/text/encoding
# golang.org/x/text/language
rsc.io/quote
rsc.io/sampler
golang.org/x/text/language
# golang.org/x/text/encoding
(главный модуль не нуждается в пакете golang.org/x/text/encoding)
-
Флаг -m заставляет go mod why обрабатывать свои аргументы как список модулей. go mod why выведет путь к любому пакету в каждом из модулей. Обратите внимание, что даже при использовании -m go mod why запрашивает граф пакетов, а не граф модулей, выведенный go mod graph.
-
Флаг -vendor заставляет go mod why игнорировать импорты в тестах пакетов вне основного модуля (как это делает go mod vendor). По умолчанию go mod why рассматривает граф пакетов, соответствующих шаблону all. Этот флаг не имеет эффекта после Go 1.16 в модулях, которые объявляют go 1.16 или выше (используя директиву go в go.mod), поскольку значение all изменилось, чтобы соответствовать набору пакетов, соответствующих go mod vendor.
go version -m
Использование:
go version [-m] [-v] [файл ...]
Пример:
# Вывести версию Go, использованную для сборки go.
$ go version
# Вывести версию Go, использованную для сборки конкретного исполняемого файла.
$ go version ~/go/bin/gopls
# Вывести версию Go и версии модулей, использованные для сборки конкретного исполняемого файла.
$ go version -m ~/go/bin/gopls
# Вывести версию Go и версии модулей, использованные для сборки исполняемых файлов в каталоге.
$ go version -m ~/go/bin/
go version сообщает версию Go, использованную для сборки каждого исполняемого файла, указанного в командной строке.
Если в командной строке не указаны файлы, go version выводит информацию о своей версии.
Если указан каталог, go version рекурсивно просматривает этот каталог, ищет распознаваемые бинарные файлы Go и сообщает их версии.
По умолчанию go version не сообщает о нераспознанных файлах, найденных во время сканирования каталога.
- Флаг
-v заставляет его сообщать о нераспознанных файлах.
- Флаг
-m заставляет go version выводить информацию о версии встроенного модуля каждого исполняемого файла, если она доступна. Для каждого исполняемого файла go version -m выводит таблицу с разделёнными табуляцией столбцами, как показано ниже.
$ go version -m ~/go/bin/goimports
/home/jrgopher/go/bin/goimports: go1.14.3
path golang.org/x/tools/cmd/goimports
mod golang.org/x/tools v0.0.0-20200518203908-8018eb2c26ba h1:0Lcy64USfQQL6GAJma8BdHCgeofcchQj+Z7j0SXYAzU=
dep golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
dep golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
Формат таблицы может измениться в будущем. Ту же информацию можно получить из runtime/debug.ReadBuildInfo.
Значение каждой строки в таблице определяется словом в первом столбце.
- path: путь к основному пакету, использованному для сборки исполняемого файла.
- mod: модуль, содержащий основной пакет. Столбцы соответственно представляют путь к модулю, версию и сумму. Основной модуль имеет версию (
devel) и не имеет суммы.
- dep: модуль, который предоставил один или несколько пакетов, связанных с исполняемым файлом. Тот же формат, что и
mod.
- =>: замена модуля в предыдущей строке. Если замена является локальным каталогом, то указывается только путь к каталогу (без версии и суммы). Если замена является версией модуля, то указываются путь, версия и сумма, как в
mod и dep. Замененный модуль не имеет суммы.
go clean -modcache
Использование:
- Флаг
-modcache заставляет go clean удалить весь кэш модулей, включая распакованный исходный код версионных зависимостей.
Обычно это лучший способ удалить кэш модулей. По умолчанию большинство файлов и каталогов в кэше модулей доступны только для чтения, чтобы тесты и редакторы не могли непреднамеренно изменить файлы после их аутентификации. К сожалению, это приводит к сбою таких команд, как rm -r, поскольку файлы не могут быть удалены без предварительного предоставления права на запись в их родительские каталоги.
- Флаг
-modcacherw (принимаемый go build и другими командами, поддерживающими модули) приводит к тому, что новые каталоги в кэше модулей становятся доступными для записи. Чтобы передать -modcacherw всем командам, поддерживающим модули, добавьте его в переменную GOFLAGS.
GOFLAGS может быть установлена в среде или с помощью go env -w. Например, приведенная ниже команда устанавливает ее навсегда:
go env -w GOFLAGS=-modcacherw
-modcacherw следует использовать с осторожностью; разработчики должны быть осторожны и не вносить изменения в файлы в кэше модулей.
go mod verify можно использовать для проверки соответствия файлов в кэше хэшам в файле go.sum основного модуля.
Запросы версии
Несколько команд позволяют указать версию модуля с помощью запроса версии, который появляется после символа @, следующего за путем к модулю или пакету в командной строке.
Примеры:
go get example.com/m@latest
go mod download example.com/m@master
go list -m -json example.com/m@e3702bed2
Запрос версии может быть одним из следующих:
- Полностью указанная семантическая версия, такая как
v1.2.3, которая выбирает конкретную версию. Синтаксис см. в разделе «Версии».
- Префикс семантической версии, такой как
v1 или v1.2, который выбирает самую высокую доступную версию с этим префиксом.
- Сравнение семантических версий, например
<v1.2.3 или >=v1.5.6, которое выбирает ближайшую доступную версию к объекту сравнения (самую низкую версию для > и >= и самую высокую версию для < и <=).
- Идентификатор ревизии для базового репозитория исходного кода, например префикс хеша коммита, тег ревизии или имя ветки. Если ревизия помечена семантической версией, этот запрос выбирает эту версию. В противном случае этот запрос выбирает псевдоверсию для базового коммита. Обратите внимание, что ветки и теги с именами, совпадающими с другими запросами версий, не могут быть выбраны таким образом. Например, запрос
v2 выбирает последнюю версию, начинающуюся с v2, а не ветку с именем v2.
- Строка
latest, которая выбирает самую последнюю доступную версию релиза. Если версий релиза нет, latest выбирает самую последнюю предрелизную версию. Если нет помеченных версий, latest выбирает псевдоверсию для коммита в конце ветки по умолчанию репозитория.
- Строка
upgrade, которая похожа на latest, за исключением того, что если модуль в настоящее время требуется в более высокой версии, чем та, которую выбрала бы latest (например, предварительная версия), upgrade выберет текущую версию.
- Строка
patch, которая выбирает последнюю доступную версию с теми же номерами основной и второстепенной версий, что и текущая требуемая версия. Если в настоящее время не требуется никакая версия, patch эквивалентна latest. Начиная с Go 1.16, go get требует текущую версию при использовании patch (но флаг -u=patch не имеет этого требования).
За исключением запросов на конкретные именованные версии или ревизии, все запросы учитывают доступные версии, указанные в go list -m -versions (см. go list -m). Этот список содержит только тегированные версии, а не псевдоверсии. Версии модулей, запрещенные директивами exclude в файле go.mod основного модуля, не учитываются. Версии, на которые распространяются директивы retract в файле go.mod из последней версии того же модуля, также игнорируются, за исключением случаев, когда флаг -retracted используется с go list -m и за исключением случаев загрузки директив retract.
Релизные версии предпочтительнее предварительных версий. Например, если доступны версии v1.2.2 и v1.2.3-pre, последний запрос выберет v1.2.2, даже если v1.2.3-pre выше. Запрос <v1.2.4 также выберет v1.2.2, даже если v1.2.3-pre ближе к v1.2.4. Если нет доступных релизных или предварительных версий, запросы latest, upgrade и patch выберут псевдоверсию для коммита в конце ветки по умолчанию репозитория. Другие запросы выдадут ошибку.
Команды модуля вне модуля
Команды Go, поддерживающие модули, обычно запускаются в контексте главного модуля, определенного файлом go.mod в рабочем каталоге или родительском каталоге. Некоторые команды могут запускаться в режиме поддержки модулей без файла go.mod, но большинство команд работают по-разному или выдают ошибку, если файл go.mod отсутствует.
См. Команды, поддерживающие модули для получения информации о включении и отключении режима поддержки модулей.
| Команда |
Поведение |
| go build |
|
| go doc |
|
| go fix |
|
| go fmt |
|
| go generate |
Только пакеты из стандартной библиотеки и пакеты, указанные в командной строке как файлы .go, могут быть загружены, импортированы и скомпилированы. Пакеты из других модулей не могут быть скомпилированы, поскольку нет места для записи требований модулей и обеспечения детерминированной компиляции. |
| go install |
|
| go list |
|
| go run |
|
| go test |
|
| go vet |
|
| go get |
Пакеты и исполняемые файлы могут быть скомпилированы и установлены как обычно. Обратите внимание, что при запуске go get без файла go.mod нет главного модуля, поэтому директивы replace и exclude не применяются. |
| go list -m |
Для большинства аргументов требуются явные запросы версии, за исключением случаев, когда используется флаг -versions. |
| go mod download |
Для большинства аргументов требуются явные запросы версии. |
| go mod edit |
Требуется явный аргумент файла. |
| go mod graph |
|
| go mod tidy |
|
| go mod vendor |
Эти команды требуют файл go.mod и выдают ошибку, если он отсутствует. |
| go mod verify |
|
| go mod why |
|
go work init
Использование:
Init инициализирует и записывает новый файл go.work в текущем каталоге, фактически создавая новую рабочую область в текущем каталоге.
go work init опционально принимает пути к модулям рабочей области в качестве аргументов. Если аргумент опущен, будет создана пустая рабочая область без модулей.
Каждый путь аргумента добавляется в директиву use в файле go.work. Текущая версия go также будет указана в файле go.work.
go work edit
Использование:
go work edit [флаги редактирования] [go.work]
Команда go work edit предоставляет интерфейс командной строки для редактирования go.work, предназначенный в первую очередь для использования инструментами или скриптами.
- Она только читает
go.work;
- она не ищет информацию о вовлеченных модулях.
Если файл не указан, Edit ищет файл go.work в текущем каталоге и его родительских каталогах
Флаги редактирования определяют последовательность операций редактирования.
- Флаг
-fmt переформатирует файл go.work без внесения других изменений. Это переформатирование также подразумевается при любых других изменениях, которые используют или перезаписывают файл go.work. Этот флаг необходим только в том случае, если не указаны другие флаги, как в «go work edit -fmt».
- Флаги
-use=path и -dropuse=path добавляют и удаляют директиву use из набора каталогов модулей файла go.work.
- Флаг
-replace=old[@v]=new[@v] добавляет замену заданной пары пути модуля и версии. Если @v в old@v опущено, добавляется замена без версии в левой части, которая применяется ко всем версиям старого пути модуля. Если @v в new@v опущено, новый путь должен быть локальным корневым каталогом модуля, а не путем модуля. Обратите внимание, что -replace переопределяет любые избыточные замены для old[@v], поэтому опущение @v приведет к удалению существующих замен для определенных версий.
- Флаг
-dropreplace=old[@v] удаляет замену заданного пути модуля и пары версий. Если @v опущено, удаляется замена без версии в левой части.
- Флаг
-go=version устанавливает ожидаемую версию языка Go.
Флаги редактирования могут повторяться. Изменения применяются в указанном порядке.
go work edit имеет дополнительные флаги, которые контролируют его вывод
- Флаг
-print выводит окончательный go.work в текстовом формате вместо записи обратно в go.mod.
- Флаг
-json выводит окончательный файл go.work в формате JSON вместо записи обратно в go.mod. Вывод JSON соответствует следующим типам Go:
type Module struct {
Path string
Version string
}
type GoWork struct {
Go string
Directory []Directory
Replace []Replace
}
type Use struct {
Path string
ModulePath string
}
type Replace struct {
Old Module
New Module
}
go work use
Использование:
go work use [-r] [moddirs]
Команда go work use предоставляет интерфейс командной строки для добавления каталогов, по желанию рекурсивно, в файл go.work.
Директива use будет добавлена в файл go.work для каждого каталога, указанного в командной строке go.work, если он существует на диске, или удалена из файла go.work, если он не существует на диске.
- Флаг
-r рекурсивно ищет модули в указанных каталогах, и команда use работает так, как если бы каждый из каталогов был указан в качестве аргумента.
go work sync
Использование:
Команда go work sync синхронизирует список сборок рабочего пространства с модулями рабочего пространства.
Список сборок рабочего пространства - это набор версий всех (переходных) зависимых модулей, используемых для выполнения сборок в рабочем пространстве. go work sync генерирует этот список сборок, используя алгоритм минимального выбора версий (MVS), а затем синхронизирует эти версии с каждым из модулей, указанных в рабочем пространстве (с помощью директив use).
После того как список сборок рабочего пространства вычислен, файл go.mod для каждого модуля в рабочем пространстве переписывается с обновлением зависимостей, относящихся к этому модулю, чтобы соответствовать списку сборок рабочего пространства. Обратите внимание, что минимальный выбор версий гарантирует, что версия каждого модуля в списке сборки всегда будет такой же или выше, чем в каждом модуле рабочего пространства.
Прокси-модули
Протокол GOPROXY
Прокси-модуль — это HTTP-сервер, который может отвечать на запросы GET для указанных ниже путей. Запросы не содержат параметров запроса и не требуют специальных заголовков, поэтому даже сайт, обслуживающийся из фиксированной файловой системы (включая URL file://), может быть прокси-модулем.
Успешные HTTP-ответы должны иметь статус-код 200 (OK). Перенаправления (3xx) принимаются. Ответы с кодами состояния 4xx и 5xx рассматриваются как ошибки. Коды ошибок 404 (Not Found) и 410 (Gone) указывают, что запрошенный модуль или версия недоступны на прокси, но могут быть найдены в другом месте. Ответы с ошибками должны иметь тип содержимого text/plain с кодировкой utf-8 или us-ascii.
Команда go может быть настроена для связи с прокси-серверами или серверами контроля версий с помощью переменной среды GOPROXY, которая принимает список URL-адресов прокси-серверов.
Список может включать ключевые слова direct или off (подробности см. в разделе «Переменные среды»).
Элементы списка могут быть разделены запятыми (,) или вертикальными чертами (|), которые определяют поведение при ошибке.
Когда за URL следует запятая, команда go переходит к более поздним источникам только после ответа 404 (Not Found) или 410 (Gone).
Когда за URL следует вертикальная черта, команда go переходит к более поздним источникам после любой ошибки, включая не-HTTP ошибки, такие как таймауты.
Такое поведение при обработке ошибок позволяет прокси действовать как шлюз для неизвестных модулей.
Например, прокси может ответить ошибкой 403 (Forbidden) для модулей, не входящих в утвержденный список (см. Частный прокси, обслуживающий частные модули).
В таблице ниже указаны запросы, на которые должен отвечать прокси модуля. Для каждого пути $base — это часть пути URL-адреса прокси, $module — путь модуля, а $version — версия.
Например, если URL-адрес прокси-сервера — https://example.com/mod, а клиент запрашивает файл go.mod для модуля golang.org/x/text версии v0.3.2, клиент отправит запрос GET на https://example.com/mod/golang.org/x/text/@v/v0.3.2.mod.
Чтобы избежать неоднозначности при обслуживании из файловых систем, нечувствительных к регистру, элементы $module и $version кодируются с учетом регистра, заменяя каждую заглавную букву восклицательным знаком, за которым следует соответствующая строчная буква. Это позволяет хранить на диске модули example.com/M и example.com/m, поскольку первый кодируется как example.com/!m.
| Путь |
Описание |
$base/$module/@v/list |
Возвращает список известных версий данного модуля в виде простого текста, по одной на строку. Этот список не должен включать псевдоверсии. Возвращает метаданные в формате JSON о конкретной версии модуля. Ответ должен быть объектом JSON, соответствующим приведенной ниже структуре данных Go: |
type Info struct {
Version string // строка версии
Time time.Time // время фиксации
}
| Путь |
Описание |
$base/$module/@v/$version.info |
Поле Version является обязательным и должно содержать действительную каноническую версию (см. Версии). $version в пути запроса не обязательно должна быть той же версией или даже действительной версией; этот конечный пункт может использоваться для поиска версий для имен ветвей или идентификаторов ревизий. Однако, если $version является канонической версией с основной версией, совместимой с $module, поле Version в успешном ответе должно быть таким же.
Поле Time является необязательным. Если оно присутствует, оно должно быть строкой в формате RFC 3339. Оно указывает время создания версии.
В будущем могут быть добавлены дополнительные поля, поэтому другие имена зарезервированы. |
$base/$module/@v/$version.mod |
Возвращает файл go.mod для определенной версии модуля. Если модуль не имеет файла go.mod для запрошенной версии, должен быть возвращен файл, содержащий только оператор модуля с запрошенным путем к модулю. В противном случае должен быть возвращен исходный, немодифицированный файл go.mod. |
$base/$module/@v/$version.zip |
Возвращает zip-файл, содержащий содержимое определенной версии модуля. См. раздел «Zip-файлы модулей» для получения подробной информации о том, как должен быть отформатирован этот zip-файл. |
$base/$module/@latest |
Возвращает метаданные в формате JSON о последней известной версии модуля в том же формате, что и $base/$module/@v/$version.info. Последняя версия должна быть версией модуля, которую команда go должна использовать, если $base/$module/@v/list пуст или ни одна из перечисленных версий не подходит. Этот конечный пункт является опциональным, и прокси-серверы модулей не обязаны его реализовывать. |
При определении последней версии модуля команда go запрашивает $base/$module/@v/list, затем, если подходящих версий не найдено, $base/$module/@latest.
Команда go предпочитает в порядке убывания: семантически наивысшую релизную версию, семантически наивысшую предрелизную версию и хронологически самую последнюю псевдо-версию. В Go 1.12 и более ранних версиях команда go считала псевдоверсии в $base/$module/@v/list предрелизными версиями, но начиная с Go 1.13 это уже не так.
Прокси модуляь должен всегда предоставлять одно и то же содержимое для успешных ответов на запросы $base/$module/$version.mod и $base/$module/$version.zip. Это содержимое проходит криптографическую проверку подлинности с помощью файлов go.sum и, по умолчанию, базы данных контрольных сумм.
Команда go кэширует большую часть содержимого, загружаемого с прокси модулей, в своем кэше модулей в $GOPATH/pkg/mod/cache/download. Даже при загрузке непосредственно из систем контроля версий команда go синтезирует явные файлы info, mod и zip и сохраняет их в этом каталоге, как если бы она загружала их непосредственно с прокси. Расположение кэша совпадает с пространством URL прокси, поэтому, обслуживая $GOPATH/pkg/mod/cache/download по адресу (или копируя его на) https://example.com/proxy, пользователи смогут получить доступ к кэшированным версиям модулей, установив GOPROXY на https://example.com/proxy.
Взаимодействие с прокси
Команда go может загружать исходный код модулей и метаданные из прокси модулей. Переменная окружения GOPROXY может использоваться для настройки прокси, к которым команда go может подключаться, а также для определения, может ли она напрямую взаимодействовать с системами контроля версий. Загруженные данные модулей сохраняются в кэше модулей. Команда go связывается с прокси только в том случае, если ей требуется информация, которой нет в кэше.
В разделе «Протокол GOPROXY» описаны запросы, которые могут отправляться на сервер GOPROXY. Однако также полезно понимать, когда команда go отправляет эти запросы. Например, go build выполняет следующую процедуру:
- Вычисляет список сборки, читая файлы
go.mod и выполняя минимальный выбор версии (MVS).
- Считывает пакеты, указанные в командной строке, и пакеты, которые они импортируют.
- Если пакет не предоставляется ни одним модулем в списке сборки, находит модуль, который его предоставляет. Добавляет требование модуля в его последней версии в
go.mod и начинает заново.
- Строит пакеты после того, как все загружено.
Когда команда go вычисляет список сборки, она загружает файл go.mod для каждого модуля в графе модулей.
Если файл go.mod отсутствует в кэше, команда go загрузит его из прокси с помощью запроса $module/@v/$version.mod (где $module — путь к модулю, а $version — версия).
Эти запросы можно протестировать с помощью такого инструмента, как curl. Например, приведенная ниже команда загружает файл go.mod для golang.org/x/mod версии v0.2.0:
$ curl https://proxy.golang.org/golang.org/x/mod/@v/v0.2.0.mod
module golang.org/x/mod
go 1.12
require (
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898
)
Чтобы загрузить пакет, команде go нужен исходный код модуля, который его предоставляет. Исходный код модуля распространяется в файлах .zip, которые извлекаются в кэш модулей. Если .zip модуля нет в кэше, команда go загрузит его с помощью запроса $module/@v/$version.zip.
$ curl -O https://proxy.golang.org/golang.org/x/mod/@v/v0.2.0.zip
$ unzip -l v0.2.0.zip | head
Archive: v0.2.0.zip
Length Date Time Name
--------- ---------- ----- ----
1479 00-00-1980 00:00 golang.org/x/mod@v0.2.0/LICENSE
1303 00-00-1980 00:00 golang.org/x/mod@v0.2.0/PATENTS
559 00-00-1980 00:00 golang.org/x/mod@v0.2.0/README
21 00-00-1980 00:00 golang.org/x/mod@v0.2.0/codereview.cfg
214 00-00-1980 00:00 golang.org/x/mod@v0.2.0/go.mod
1476 00-00-1980 00:00 golang.org/x/mod@v0.2.0/go.sum
5224 00-00-1980 00:00 golang.org/x/mod@v0.2.0/gosumcheck/main.go
Обратите внимание, что запросы .mod и .zip являются отдельными, даже если файлы go.mod обычно содержатся в файлах .zip. Команде go может потребоваться загрузить файлы go.mod для многих различных модулей, а файлы .mod намного меньше по размеру, чем файлы .zip. Кроме того, если в проекте Go нет файла go.mod, прокси будет предоставлять синтетический файл go.mod, содержащий только директиву модуля. Синтетические файлы go.mod генерируются командой go при загрузке из системы контроля версий.
Если команде go необходимо загрузить пакет, который не предоставляется ни одним модулем в списке сборки, она попытается найти новый модуль, который его предоставляет. Этот процесс описан в разделе «Преобразование пакета в модуль». Вкратце, команда go запрашивает информацию о последней версии каждого пути модуля, который может содержать пакет.
Например, для пакета golang.org/x/net/html команда go попытается найти последние версии модулей golang.org/x/net/html, golang.org/x/net, golang.org/x/ и golang.org. Только golang.org/x/net действительно существует и предоставляет этот пакет, поэтому команда go использует последнюю версию этого модуля.
Если пакет предоставляется более чем одним модулем, команда go использует модуль с самым длинным путем.
Когда команда go запрашивает последнюю версию модуля, она сначала отправляет запрос на $module/@v/list. Если список пуст или ни одна из возвращенных версий не может быть использована, она отправляет запрос на $module/@latest. После выбора версии команда go отправляет запрос $module/@v/$version.info для получения метаданных. Затем она может отправить запросы $module/@v/$version.mod и $module/@v/$version.zip для загрузки файла go.mod и исходного кода.
$ curl https://proxy.golang.org/golang.org/x/mod/@v/list
v0.1.0
v0.2.0
$ curl https://proxy.golang.org/golang.org/x/mod/@v/v0.2.0.info
{«Version»:«v0.2.0»,„Time“:«2020-01-02T17:33:45Z»}
После загрузки файла .mod или .zip команда go вычисляет криптографический хеш и проверяет, соответствует ли он хешу в файле go.sum основного модуля. Если хеш отсутствует в go.sum, по умолчанию команда go извлекает его из базы данных контрольных сумм.
Если вычисленный хеш не совпадает, команда go сообщает об ошибке безопасности и не устанавливает файл в кэш модулей. Переменные среды GOPRIVATE и GONOSUMDB могут использоваться для отключения запросов к базе данных контрольных сумм для определенных модулей.
Переменная среды GOSUMDB также может быть установлена в значение off, чтобы полностью отключить запросы к базе данных контрольных сумм.
Дополнительные сведения см. в разделе Аутентификация модулей. Обратите внимание, что списки версий и метаданные версий, возвращаемые для запросов .info, не аутентифицируются и могут изменяться со временем.
Обслуживание модулей напрямую из прокси
Большинство модулей разрабатываются и обслуживаются из репозитория системы контроля версий.
В прямом режиме команда go загружает такой модуль с помощью инструмента контроля версий (см. Системы контроля версий). Также можно обслуживать модуль напрямую из прокси модуля. Это полезно для организаций, которые хотят обслуживать модули, не раскрывая свои серверы контроля версий, а также для организаций, которые используют инструменты контроля версий, не поддерживаемые командой go.
Когда команда go загружает модуль в прямом режиме, она сначала ищет URL-адрес сервера модулей с помощью HTTP-запроса GET на основе пути модуля.
Она ищет тег <meta> с именем go-import в HTML-ответе. Содержимое тега должно содержать корневой путь репозитория, систему контроля версий и URL-адрес, разделенные пробелами. Подробнее см. в разделе Поиск репозитория для пути модуля.
Если системой контроля версий является mod, команда go загружает модуль с указанного URL-адреса с помощью протокола GOPROXY.
Например, предположим, что команда go пытается загрузить модуль example.com/gopher версии v1.0.0. Она отправляет запрос на https://example.com/gopher?go-get=1. Сервер отвечает HTML-документом, содержащим тег:
<meta name="go-import" content="example.com/gopher mod https://modproxy.example.com">
На основании этого ответа команда go загружает модуль, отправляя запросы на https://modproxy.example.com/example.com/gopher/@v/v1.0.0.info, v1.0.0.mod и v1.0.0.zip.
Обратите внимание, что модули, обслуживаемые непосредственно с прокси, не могут быть загружены с помощью go get в режиме GOPATH.
Системы контроля версий
Команда go может загружать исходный код модулей и метаданные непосредственно из репозитория системы контроля версий. Загрузка модуля через прокси-сервер обычно происходит быстрее, но прямое подключение к репозиторию необходимо, если прокси-сервер недоступен или если репозиторий модуля недоступен для прокси-сервера (часто это относится к частным репозиториям). Поддерживаются Git, Subversion, Mercurial, Bazaar и Fossil. Чтобы команда go могла использовать инструмент контроля версий, он должен быть установлен в каталоге, включенном в PATH.
Чтобы загружать определенные модули из репозиториев исходного кода, а не через прокси, установите переменные окружения GOPRIVATE или GONOPROXY. Чтобы настроить команду go на загрузку всех модулей напрямую из репозиториев исходного кода, установите GOPROXY в значение direct. Дополнительные сведения см. в разделе Переменные окружения.
Поиск репозитория для пути модуля
Когда команда go загружает модуль в прямом режиме, она сначала находит репозиторий, содержащий модуль.
Если путь к модулю имеет квалификатор VCS (один из .bzr, .fossil, .git, .hg, .svn) в конце компонента пути, команда go будет использовать все до этого квалификатора пути в качестве URL-адреса репозитория. Например, для модуля example.com/foo.git/bar команда go загружает репозиторий по адресу example.com/foo с помощью git, ожидая найти модуль в подкаталоге bar. Команда go угадывает протокол, который следует использовать, на основе протоколов, поддерживаемых инструментом контроля версий.
Если путь модуля не имеет квалификатора, команда go отправляет HTTP-запрос GET на URL, полученный из пути модуля с строкой запроса ?go-get=1. Например, для модуля golang.org/x/mod команда go может отправить следующие запросы:
https://golang.org/x/mod?go-get=1 (preferred)
http://golang.org/x/mod?go-get=1 (fallback, only with GOINSECURE)
Команда go следует за перенаправлениями, но игнорирует коды статуса ответа, поэтому сервер может ответить кодом 404 или любым другим кодом ошибки. Переменная среды GOINSECURE может быть установлена, чтобы разрешить переход на незашифрованный HTTP для определенных модулей.
Сервер должен отвечать HTML-документом, содержащим тег <meta> в <head> документа.
Тег <meta> должен появляться в начале документа, чтобы не сбивать с толку ограниченный парсер команды go. В частности, он должен появляться перед любым необработанным JavaScript или CSS. Тег <meta> должен иметь следующий вид:
<meta name="go-import" content="root-path vcs repo-url [subdirectory]">
- root-path — это корневой путь репозитория, часть пути модуля, которая соответствует корневому каталогу репозитория или подкаталогу, если он присутствует и используется Go 1.25 или более поздней версии (см. раздел о подкаталогах ниже). Он должен быть префиксом или точно соответствовать запрошенному пути модуля. Если он не соответствует точно, делается другой запрос на префикс, чтобы проверить соответствие тегов
<meta>.
- vcs — это система контроля версий. Это должен быть один из инструментов, перечисленных в таблице ниже, или ключевое слово
mod, которое указывает команде go загрузить модуль с указанного URL-адреса с использованием протокола GOPROXY. Подробности см. в разделе «Обслуживание модулей напрямую с прокси».
- repo-url — это URL-адрес репозитория. Если URL не содержит схему (либо потому, что путь к модулю имеет квалификатор
VCS, либо потому, что в теге <meta> отсутствует схема), команда go попробует каждый протокол, поддерживаемый системой контроля версий. Например, с Git команда go попробует https://, а затем git+ssh://. Небезопасные протоколы (такие как http:// и git://) могут использоваться только в том случае, если путь к модулю соответствует переменной среды GOINSECURE.
- Подкаталог, если он присутствует, является подкаталогом репозитория, разделенным косой чертой, которому соответствует root-path, переопределяя по умолчанию корневой каталог репозитория. Мета-теги go-import, предоставляющие подкаталог, распознаются только Go 1.25 и более поздними версиями. Попытки извлечь модули в более ранних версиях Go будут игнорировать мета-тег и приведут к сбою разрешения, если модуль не может быть разрешен в другом месте.
| Name |
Command |
GOVCS default |
Secure schemes |
| Bazaar |
bzr |
Private only |
https, bzr+ssh |
| Fossil |
fossil |
Private only |
https |
| Git |
git |
Public and private |
https, git+ssh, ssh |
| Mercurial |
hg |
Public and private |
https, ssh |
| Subversion |
svn |
Private only |
https, svn+ssh |
В качестве примера снова рассмотрим golang.org/x/mod. Команда go отправляет запрос на https://golang.org/x/mod?go-get=1. Сервер отвечает HTML-документом, содержащим тег:
<meta name="go-import" content="golang.org/x/mod git https://go.googlesource.com/mod">
На основании этого ответа команда go будет использовать репозиторий Git по удаленному URL-адресу https://go.googlesource.com/mod.
GitHub и другие популярные хостинговые сервисы отвечают на запросы ?go-get=1 для всех репозиториев, поэтому обычно для модулей, размещенных на этих сайтах, не требуется настройка сервера.
После нахождения URL-адреса репозитория команда go клонирует репозиторий в кэш модулей. Как правило, команда go старается избегать извлечения ненужных данных из репозитория. Однако фактические используемые команды варьируются в зависимости от системы контроля версий и могут меняться со временем. Для Git команда go может перечислить большинство доступных версий без загрузки коммитов. Обычно она извлекает коммиты без загрузки коммитов предков, но иногда это необходимо.
Сопоставление версий с коммитами
Команда go может проверить модуль в репозитории с определенной канонической версией, такой как v1.2.3, v2.4.0-beta или v3.0.0+incompatible. Каждая версия модуля должна иметь семантический тег версии в репозитории, который указывает, какая ревизия должна быть проверена для данной версии.
Если модуль определен в корневом каталоге репозитория или в подкаталоге основной версии корневого каталога, то каждое имя тега версии равно соответствующей версии. Например, модуль golang.org/x/text определен в корневом каталоге своего репозитория, поэтому версия v0.3.2 имеет тег v0.3.2 в этом репозитории. Это верно для большинства модулей.
Если модуль определен в подкаталоге внутри репозитория, то есть часть пути модуля, относящаяся к подкаталогу модуля, не пуста, то каждое имя тега должно начинаться с префикса подкаталога модуля, за которым следует косая черта. Например, модуль golang.org/x/tools/gopls определен в подкаталоге gopls репозитория с корневым путем golang.org/x/tools. Версия v0.4.0 этого модуля должна иметь тег с именем gopls/v0.4.0 в этом репозитории.
Номер основной версии тега семантической версии должен соответствовать суффиксу основной версии пути модуля (если таковой имеется). Например, тег v1.0.0 может принадлежать модулю example.com/mod, но не example.com/mod/v2, который будет иметь теги типа v2.0.0.
Теги с основной версией v2 или выше могут принадлежать модулю без суффикса основной версии, если файл go.mod отсутствует, а модуль находится в корневом каталоге репозитория. Такая версия обозначается суффиксом +incompatible. Сам тег версии не должен иметь суффикса. См. раздел «Совместимость с репозиториями, не являющимися модулями».
После создания тег не должен удаляться или изменяться на другую ревизию. Версии проходят аутентификацию для обеспечения безопасных и повторяемых сборок. Если тег изменен, клиенты могут увидеть ошибку безопасности при его загрузке. Даже после удаления тега его содержимое может оставаться доступным на прокси-серверах модулей.
Сопоставление псевдоверсий с коммитами
Команда go может проверить модуль в репозитории с определенной ревизией, закодированной как псевдоверсия, например v1.3.2-0.20191109021931-daa7c04131f5.
Последние 12 символов псевдоверсии (daa7c04131f5 в примере выше) указывают на ревизию в репозитории, которую необходимо проверить. Значение этого зависит от системы контроля версий. Для Git и Mercurial это префикс хеша коммита. Для Subversion это номер ревизии с нулевым заполнением.
Перед проверкой коммита команда go проверяет, что временная метка (20191109021931 выше) соответствует дате коммита. Она также проверяет, что базовая версия (v1.3.1, версия перед v1.3.2 в примере выше) соответствует семантическому тегу версии, который является предком коммита. Эти проверки гарантируют, что авторы модулей имеют полный контроль над тем, как псевдоверсии сопоставляются с другими выпущенными версиями.
Дополнительную информацию см. в разделе Псевдоверсии.
Сопоставление веток и коммитов с версиями
Модуль может быть проверен в определенной ветке, теге или ревизии с помощью запроса версии.
go get example.com/mod@master
Команда go преобразует эти имена в канонические версии, которые можно использовать с минимальным выбором версии (MVS). MVS зависит от возможности однозначно упорядочить версии. Имена веток и ревизии не могут быть надежно сопоставлены во времени, поскольку они зависят от структуры репозитория, которая может изменяться.
Если ревизия помечена одним или несколькими семантическими тегами версии, такими как v1.2.3, будет использован тег для самой высокой действительной версии. Команда go учитывает только семантические теги версии, которые могут принадлежать целевому модулю; например, тег v1.5.2 не будет учитываться для example.com/mod/v2, поскольку основная версия не соответствует суффиксу пути модуля.
Если ревизия не помечена действительным тегом семантической версии, команда go сгенерирует псевдоверсию. Если у ревизии есть предки с действительными тегами семантической версии, в качестве базы псевдоверсии будет использована версия самого старшего предка. См. Псевдоверсии.
Каталоги модулей в репозитории
После того, как репозиторий модуля был проверен на определенную ревизию, команда go должна найти каталог, содержащий файл go.mod модуля (корневой каталог модуля).
Напомним, что путь к модулю состоит из трех частей: корневой путь репозитория (соответствующий корневому каталогу репозитория), подкаталог модуля и суффикс основной версии (только для модулей, выпущенных в версии v2 или выше).
Для большинства модулей путь к модулю равен корневому пути репозитория, поэтому корневой каталог модуля является корневым каталогом репозитория.
Иногда модули определяются в подкаталогах репозитория. Обычно это делается для больших репозиториев с несколькими компонентами, которые необходимо выпускать и версионировать независимо друг от друга. Такой модуль должен находиться в подкаталоге, который соответствует части пути к модулю после корневого пути репозитория. Например, предположим, что модуль example.com/monorepo/foo/bar находится в репозитории с корневым путем example.com/monorepo. Его файл go.mod должен находиться в подкаталоге foo/bar.
Если модуль выпускается в основной версии v2 или выше, его путь должен иметь суффикс основной версии. Модуль с суффиксом основной версии может быть определен в одном из двух подкаталогов: одном с суффиксом и одном без него. Например, предположим, что новая версия вышеуказанного модуля выпущена с путем example.com/monorepo/foo/bar/v2. Его файл go.mod может находиться либо в foo/bar, либо в foo/bar/v2.
Подкаталоги с суффиксом основной версии являются подкаталогами основной версии. Они могут использоваться для разработки нескольких основных версий модуля в одной ветке. Это может быть ненужным, когда разработка нескольких основных версий ведется в отдельных ветках. Однако подкаталоги основной версии имеют важное свойство: в режиме GOPATH пути импорта пакетов точно соответствуют каталогам в GOPATH/src. Команда go обеспечивает минимальную совместимость модулей в режиме GOPATH (см. Совместимость с репозиториями, не являющимися модулями), поэтому подкаталоги основной версии не всегда необходимы для совместимости с проектами, созданными в режиме GOPATH. Однако у старых инструментов, которые не поддерживают минимальную совместимость модулей, могут возникнуть проблемы.
Как только команда go находит корневой каталог модуля, она создает файл .zip с содержимым каталога, а затем извлекает файл .zip в кэш модуля. См. Ограничения по пути и размеру файлов для получения подробной информации о том, какие файлы могут быть включены в файл .zip. Содержимое файла .zip проверяется перед извлечением в кэш модуля так же, как если бы файл .zip был загружен с прокси-сервера.
Файлы zip модулей не включают содержимое каталогов поставщиков или вложенных модулей (подкаталогов, содержащих файлы go.mod). Это означает, что модуль не должен ссылаться на файлы, расположенные за пределами своего каталога или в других модулях. Например, шаблоны //go:embed не должны совпадать с файлами во вложенных модулях. Такое поведение может служить полезным обходным путем в ситуациях, когда файлы не должны включаться в модуль. Например, если в репозитории есть большие файлы, зарегистрированные в каталоге testdata, автор модуля может добавить пустой файл go.mod в testdata, чтобы пользователи не должны были загружать эти файлы. Конечно, это может уменьшить охват для пользователей, тестирующих свои зависимости.
Особый случай для файлов LICENSE
Когда команда go создает файл .zip для модуля, который не находится в корневом каталоге репозитория, если в корневом каталоге модуля (рядом с go.mod) нет файла с именем LICENSE, команда go скопирует файл с именем LICENSE из корневого каталога репозитория, если он присутствует в той же ревизии.
Этот особый случай позволяет применять один и тот же файл LICENSE ко всем модулям в репозитории. Это относится только к файлам с именем LICENSE, без расширений, таких как .txt. К сожалению, это нельзя расширить без нарушения криптографических сумм существующих модулей; см. Аутентификация модулей. Другие инструменты и веб-сайты, такие как pkg.go.dev, могут распознавать файлы с другими именами.
Обратите также внимание, что команда go не включает символические ссылки при создании файлов .zip модулей; см. Ограничения по пути и размеру файлов. Следовательно, если в корневом каталоге репозитория нет файла LICENSE, авторы могут вместо этого создать копии своих файлов лицензий в модулях, определенных в подкаталогах, чтобы обеспечить включение этих файлов в файлы .zip модулей.
Управление инструментами контроля версий с помощью GOVCS
Возможность команды go загружать модули с помощью команд контроля версий, таких как git, имеет решающее значение для децентрализованной экосистемы пакетов, в которой код может быть импортирован с любого сервера. Это также может представлять потенциальную угрозу безопасности, если злоумышленный сервер найдет способ заставить вызванную команду контроля версий выполнить непреднамеренный код.
Чтобы сбалансировать функциональность и проблемы безопасности, команда go по умолчанию будет использовать только git и hg для загрузки кода с публичных серверов. Она будет использовать любую известную систему контроля версий для загрузки кода с частных серверов, определенных как те, которые хостят пакеты, соответствующие переменной среды GOPRIVATE. Причина, по которой разрешены только Git и Mercurial, заключается в том, что эти две системы уделяют наибольшее внимание проблемам запуска в качестве клиентов недоверенных серверов. В отличие от них, Bazaar, Fossil и Subversion в основном используются в доверенных, аутентифицированных средах и не подвергаются столь тщательному анализу с точки зрения уязвимостей.
Ограничения команд управления версиями применяются только при использовании прямого доступа к управлению версиями для загрузки кода. При загрузке модулей с прокси-сервера команда go использует протокол GOPROXY, который всегда разрешен. По умолчанию команда go использует зеркало модулей Go (proxy.golang.org) для общедоступных модулей и переключается на управление версиями только для частных модулей или когда зеркало отказывается обслуживать общедоступный пакет (обычно по юридическим причинам). Таким образом, клиенты по-прежнему могут по умолчанию получать доступ к общедоступному коду, обслуживаемому репозиториями Bazaar, Fossil или Subversion, поскольку для этих загрузок используется зеркало модулей Go, которое принимает на себя риск безопасности, связанный с запуском команд системы контроля версий с использованием настраиваемой песочницы.
Переменная GOVCS может использоваться для изменения разрешенных систем контроля версий для определенных модулей. Переменная GOVCS применяется при сборке пакетов как в режиме с поддержкой модулей, так и в режиме GOPATH. При использовании модулей шаблоны сопоставляются с путем модуля. При использовании GOPATH шаблоны сопоставляются с путем импорта, соответствующим корню репозитория системы контроля версий.
Общая форма переменной GOVCS — это разделенный запятыми список правил pattern:vcslist. Pattern — это шаблон glob, который должен соответствовать одному или нескольким начальным элементам пути модуля или импорта. Vcslist — это разделенный вертикальной чертой список разрешенных команд управления версиями, или all, чтобы разрешить использование любой известной команды, или off, чтобы ничего не разрешать.
Обратите внимание, что если модуль соответствует шаблону с vcslist off, он все равно может быть загружен, если исходный сервер использует схему mod, которая указывает команде go загрузить модуль с помощью протокола GOPROXY. Применяется самый ранний соответствующий шаблон в списке, даже если более поздние шаблоны также могут соответствовать.
Рассмотрим, например:
GOVCS=github.com:git,evil.com:off,*:git|hg
С этой настройкой код с модулем или путем импорта, начинающимся с github.com/, может использовать только git; пути на evil.com не могут использовать никакие команды управления версиями, а все остальные пути (* соответствует всему) могут использовать только git или hg.
Специальные шаблоны public и private соответствуют публичным и частным модулям или путям импорта. Путь является частным, если он соответствует переменной GOPRIVATE; в противном случае он является публичным.
Если ни одно из правил в переменной GOVCS не соответствует конкретному модулю или пути импорта, команда go применяет свое правило по умолчанию, которое теперь можно обозначить в нотации GOVCS как public:git|hg,private:all.
Чтобы разрешить неограниченное использование любой системы контроля версий для любого пакета, используйте:
Чтобы отключить все использование контроля версий, используйте:
Команда go env -w может быть использована для установки переменной GOVCS для будущих вызовов команды go.
GOVCS был введен в Go 1.16. Более ранние версии Go могут использовать любой известный инструмент контроля версий для любого модуля.
Zip-файлы модулей
Версии модулей распространяются в виде zip-файлов. Редко возникает необходимость в прямом взаимодействии с этими файлами, поскольку команда go автоматически создает, загружает и извлекает их из прокси-серверов модулей и репозиториев системы контроля версий. Однако знание об этих файлах все же полезно для понимания ограничений кроссплатформенной совместимости или при реализации прокси-сервера модулей.
Команда go mod download загружает zip-файлы для одного или нескольких модулей, а затем извлекает эти файлы в кэш модулей. В зависимости от GOPROXY и других переменных окружения, команда go может либо загружать zip-файлы с прокси, либо клонировать репозитории контроля версий и создавать из них zip-файлы. Флаг -json может использоваться для поиска местоположения загруженных zip-файлов и их извлеченного содержимого в кэше модулей.
Пакет golang.org/x/mod/zip можно использовать для программного создания, извлечения или проверки содержимого zip-файлов.
Ограничения по пути к файлу и размеру
Существует ряд ограничений на содержимое zip-файлов модулей. Эти ограничения гарантируют, что zip-файлы могут быть безопасно и последовательно извлечены на широком спектре платформ.
- Размер
zip-файла модуля не может превышать 500 МБ. Общий размер его файлов в несжатом виде также ограничен 500 МБ. Размер файлов go.mod ограничен 16 МБ. Файлы LICENSE также ограничены 16 МБ. Эти ограничения существуют для смягчения атак типа «отказ в обслуживании» на пользователей, прокси и другие части экосистемы модулей. Репозитории, содержащие более 500 МБ файлов в дереве каталогов модулей, должны помечать версии модулей при фиксации, которые включают только файлы, необходимые для сборки пакетов модуля; видео, модели и другие большие ресурсы обычно не нужны для сборки.
- Каждый файл в
zip-файле модуля должен начинаться с префикса $module@$version/, где $module — это путь к модулю, а $version — версия, например, golang.org/x/mod@v0.3.0/. Путь к модулю должен быть действительным, версия должна быть действительной и канонической, а версия должна соответствовать суффиксу основной версии пути к модулю. Конкретные определения и ограничения см. в разделе «Пути и версии модулей».
- Режимы файлов, временные метки и другие метаданные игнорируются.
- Пустые каталоги (записи с путями, заканчивающимися косой чертой) могут быть включены в
zip-файлы модулей, но не извлекаются. Команда go не включает пустые каталоги в создаваемые ею zip-файлы.
- Символьные ссылки и другие нестандартные файлы игнорируются при создании
zip-файлов, поскольку они не переносимы между операционными системами и файловыми системами, и нет переносимого способа их представления в формате zip-файла.
- Файлы в каталогах с именем vendor игнорируются при создании
zip-файлов, поскольку каталоги vendor вне основного модуля никогда не используются.
- Файлы в каталогах, содержащих файлы
go.mod, кроме корневого каталога модуля, игнорируются при создании zip-файлов, поскольку они не являются частью модуля. Команда go игнорирует подкаталоги, содержащие файлы go.mod, при извлечении zip-файлов.
- Ни два файла в
zip-файле не могут иметь одинаковые пути при сгибании регистра Unicode (см. strings.EqualFold). Это гарантирует, что zip-файлы могут быть извлечены в файловых системах, нечувствительных к регистру, без коллизий.
- Файл
go.mod может присутствовать или отсутствовать в каталоге верхнего уровня ($module@$version/go.mod). Если он присутствует, он должен иметь имя go.mod (все строчные буквы). Файлы с именем go.mod не допускаются в других каталогах.
- Имена файлов и каталогов в модуле могут состоять из букв Unicode, цифр ASCII, символа пробела
ASCII (U+0020) и символов пунктуации ASCII !#$%&()+,-.=@[]^_{}~. Обратите внимание, что пути к пакетам не могут содержать все эти символы. См. module.CheckFilePath и module.CheckImportPath для ознакомления с различиями.
- Имя файла или каталога до первой точки не должно быть зарезервированным именем файла в Windows, независимо от регистра (CON, com1, NuL и т. д.).
Private (Частные модули)
Модули Go часто разрабатываются и распространяются на серверах контроля версий и прокси-серверах модулей, которые недоступны в общедоступном Интернете. Команда go может загружать и собирать модули из частных источников, хотя для этого обычно требуется некоторая настройка.
Для настройки доступа к частным модулям можно использовать перечисленные ниже переменные окружения. Подробности см. в разделе Переменные окружения. См. также раздел Конфиденциальность для получения информации об управлении информацией, отправляемой на общедоступные серверы.
- GOPROXY — список URL-адресов прокси-серверов модулей. Команда
go будет пытаться загрузить модули с каждого сервера по очереди. Ключевое слово direct указывает команде go загружать модули из репозиториев контроля версий, где они разрабатываются, вместо использования прокси-сервера.
- GOPRIVATE — список шаблонов префиксов путей к модулям, которые должны считаться частными. Действует как значение по умолчанию для
GONOPROXY и GONOSUMDB.
- GONOPROXY — список шаблонов
glob префиксов путей модулей, которые не должны загружаться с прокси. Команда go загрузит соответствующие модули из репозиториев контроля версий, где они разрабатываются, независимо от GOPROXY.
- GONOSUMDB — список шаблонов
glob префиксов путей модулей, которые не должны проверяться с помощью общедоступной базы данных контрольных сумм sum.golang.org.
- GOINSECURE — список шаблонов
glob префиксов путей модулей, которые могут быть получены через HTTP и другие небезопасные протоколы.
Эти переменные могут быть установлены в среде разработки (например, в файле .profile) или они могут быть установлены навсегда с помощью go env -w.
В остальной части этого раздела описаны общие шаблоны для предоставления доступа к частным прокси-серверам модулей и репозиториям контроля версий.
Частный прокси-сервер, обслуживающий все модули
Центральный частный прокси-сервер, обслуживающий все модули (публичные и частные), обеспечивает максимальный контроль для администраторов и требует минимальной настройки для отдельных разработчиков.
Чтобы настроить команду go для использования такого сервера, установите следующие переменные среды, заменив https://proxy.corp.example.com на URL вашего прокси и corp.example.com на префикс вашего модуля:
GOPROXY=https://proxy.corp.example.com
GONOSUMDB=corp.example.com
Настройка GOPROXY указывает команде go загружать модули только с https://proxy.corp.example.com; команда go не будет подключаться к другим прокси-серверам или репозиториям контроля версий.
Настройка GONOSUMDB указывает команде go не использовать публичную базу данных контрольных сумм для аутентификации модулей с путями, начинающимися с corp.example.com.
Прокси, работающий в этой конфигурации, скорее всего, потребует доступ на чтение к частным серверам контроля версий. Ему также потребуется доступ к общедоступному Интернету для загрузки новых версий общедоступных модулей.
Существует несколько реализаций серверов GOPROXY, которые можно использовать таким образом. Минимальная реализация будет обслуживать файлы из каталога кэша модулей и использовать go mod download (с соответствующей конфигурацией) для извлечения отсутствующих модулей.
Частный прокси-сервер, обслуживающий частные модули
Частный прокси-сервер может обслуживать частные модули, не обслуживая при этом общедоступные модули. Команду go можно настроить так, чтобы она переключалась на общедоступные источники для модулей, которые недоступны на частном сервере.
Чтобы настроить команду go для работы в таком режиме, установите следующие переменные среды, заменив https://proxy.corp.example.com на URL прокси-сервера, а corp.example.com на префикс модуля:
GOPROXY=https://proxy.corp.example.com,https://proxy.golang.org,direct
GONOSUMDB=corp.example.com
Настройка GOPROXY указывает команде go сначала пытаться загрузить модули с https://proxy.corp.example.com. Если этот сервер отвечает кодом 404 (Not Found) или 410 (Gone), команда go переходит к https://proxy.golang.org, а затем к прямым подключениям к репозиториям.
Настройка GONOSUMDB указывает команде go не использовать общедоступную базу данных контрольных сумм для аутентификации модулей, пути к которым начинаются с corp.example.com.
Обратите внимание, что прокси, используемый в этой конфигурации, может по-прежнему контролировать доступ к общедоступным модулям, даже если он их не обслуживает. Если прокси отвечает на запрос со статусом ошибки, отличным от 404 или 410, команда go не будет переходить к более поздним записям в списке GOPROXY. Например, прокси может ответить 403 (Forbidden) для модуля с неподходящей лицензией или с известными уязвимостями безопасности.
Прямой доступ к частным модулям
Команду go можно настроить так, чтобы она обходила общедоступные прокси-серверы и загружала частные модули напрямую с серверов контроля версий. Это полезно, когда запуск частного прокси-сервера невозможен.
Чтобы настроить команду go для работы в таком режиме, установите GOPRIVATE, заменив corp.example.com префиксом частного модуля:
GOPRIVATE=corp.example.com
В этой ситуации переменную GOPROXY менять не нужно. По умолчанию она имеет значение https://proxy.golang.org,direct, что указывает команде go сначала пытаться загрузить модули с https://proxy.golang.org, а затем перейти к прямому подключению, если этот прокси отвечает 404 (Not Found) или 410 (Gone).
Настройка GOPRIVATE указывает команде go не подключаться к прокси или базе данных контрольных сумм для модулей, начинающихся с corp.example.com.
Для преобразования путей модулей в URL-адреса репозиториев по-прежнему может потребоваться внутренний HTTP-сервер. Например, когда команда go загружает модуль corp.example.com/mod, она отправляет запрос GET на https://corp.example.com/mod?go-get=1 и ищет URL-адрес репозитория в ответе. Чтобы избежать этого требования, убедитесь, что каждый частный путь модуля имеет суффикс VCS (например, .git), обозначающий префикс корня репозитория. Например, когда команда go загружает модуль corp.example.com/repo.git/mod, она клонирует репозиторий Git по адресу https://corp.example.com/repo.git или ssh://corp.example.com/repo.git без необходимости делать дополнительные запросы.
Разработчикам потребуется доступ на чтение к репозиториям, содержащим частные модули. Это можно настроить в глобальных файлах конфигурации VCS, таких как .gitconfig. Лучше всего, если инструменты VCS настроены так, чтобы не требовать интерактивных запросов аутентификации. По умолчанию при вызове Git команда go отключает интерактивные запросы, устанавливая GIT_TERMINAL_PROMPT=0, но она учитывает явные настройки.
Передача учетных данных частным прокси-серверам
Команда go поддерживает базовую аутентификацию HTTP при взаимодействии с прокси-серверами.
Учетные данные могут быть указаны в файле .netrc. Например, файл .netrc, содержащий приведенные ниже строки, настроит команду go на подключение к машине proxy.corp.example.com с указанным именем пользователя и паролем.
machine proxy.corp.example.com
login jrgopher
password hunter2
Расположение файла можно установить с помощью переменной среды NETRC. Если NETRC не установлена, команда go будет читать $HOME/.netrc на UNIX-подобных платформах или %USERPROFILE%\_netrc в Windows.
Поля в .netrc разделены пробелами, табуляциями и символами новой строки. К сожалению, эти символы не могут использоваться в именах пользователей или паролях. Также обратите внимание, что имя машины не может быть полным URL-адресом, поэтому невозможно указать разные имена пользователей и пароли для разных путей на одной машине.
В качестве альтернативы учетные данные можно указать непосредственно в URL-адресах GOPROXY. Например:
GOPROXY=https://jrgopher:hunter2@proxy.corp.example.com
Будьте осторожны при использовании этого подхода: переменные среды могут появляться в истории оболочки и в журналах.
Передача учетных данных в частные репозитории
Команда go может загружать модуль непосредственно из репозитория системы контроля версий. Это необходимо для частных модулей, если не используется частный прокси. См. раздел Прямой доступ к частным модулям для настройки.
Команда go запускает инструменты контроля версий, такие как git, при прямой загрузке модулей. Эти инструменты выполняют собственную аутентификацию, поэтому вам может потребоваться настроить учетные данные в конфигурационном файле, специфичном для инструмента, например .gitconfig.
Чтобы обеспечить бесперебойную работу, убедитесь, что команда go использует правильный URL-адрес репозитория и что инструмент контроля версий не требует ввода пароля в интерактивном режиме. Команда go предпочитает URL-адреса https:// другим схемам, таким как ssh://, если схема не была указана при поиске URL-адреса репозитория. Специально для репозиториев GitHub команда go предполагает использование https://.
Для большинства серверов вы можете настроить клиент на аутентификацию по HTTP. Например, GitHub поддерживает использование личных токенов доступа OAuth в качестве паролей HTTP. Вы можете хранить пароли HTTP в файле .netrc, как при передаче учетных данных частным прокси-серверам.
В качестве альтернативы можно переписать URL-адреса https:// в другую схему. Например, в .gitconfig:
[url «git@github.com:»]
insteadOf = https://github.com/
Для получения дополнительной информации см. Почему «go get» использует HTTPS при клонировании репозитория?
Конфиденциальность
Команда go может загружать модули и метаданные с прокси-серверов модулей и систем контроля версий. Переменная окружения GOPROXY контролирует, какие серверы используются. Переменные окружения GOPRIVATE и GONOPROXY контролируют, какие модули загружаются с прокси-серверов.
Значение по умолчанию для GOPROXY:
https://proxy.golang.org,direct
С этой настройкой, когда команда go загружает модуль или метаданные модуля, она сначала отправляет запрос на proxy.golang.org, общедоступный прокси-сервер модулей, управляемый Google (политика конфиденциальности). См. протокол GOPROXY для получения подробной информации о том, какая информация отправляется в каждом запросе. Команда go не передает личную информацию, но передает полный путь к запрашиваемому модулю. Если прокси отвечает со статусом 404 (Not Found) или 410 (Gone), команда go попытается подключиться напрямую к системе контроля версий, предоставляющей модуль. Подробности см. в разделе Системы контроля версий.
Переменные среды GOPRIVATE или GONOPROXY могут быть установлены в виде списков шаблонов glob, соответствующих префиксам модулей, которые являются частными и не должны запрашиваться у какого-либо прокси. Например:
GOPRIVATE=*.corp.example.com,*.research.example.com
GOPRIVATE просто действует как значение по умолчанию для GONOPROXY и GONOSUMDB, поэтому нет необходимости устанавливать GONOPROXY, если GONOSUMDB не должен иметь другое значение. Когда путь к модулю соответствует GONOPROXY, команда go игнорирует GOPROXY для этого модуля и извлекает его напрямую из репозитория системы контроля версий. Это полезно, когда ни один прокси не обслуживает частные модули. См. Прямой доступ к частным модулям.
Если есть доверенный прокси, обслуживающий все модули, то GONOPROXY не следует устанавливать. Например, если GOPROXY установлен на один источник, команда go не будет загружать модули из других источников. В этой ситуации GONOSUMDB все равно следует установить.
GOPROXY=https://proxy.corp.example.com
GONOSUMDB=*.corp.example.com,*.research.example.com
Если есть доверенный прокси-сервер, обслуживающий только частные модули, GONOPROXY не следует устанавливать, но необходимо убедиться, что прокси-сервер отвечает с правильными кодами статуса. Рассмотрим, например, следующую конфигурацию:
GOPROXY=https://proxy.corp.example.com,https://proxy.golang.org
GONOSUMDB=*.corp.example.com,*.research.example.com
Предположим, что из-за опечатки разработчик пытается загрузить модуль, которого не существует.
go mod download corp.example.com/secret-product/typo@latest
Команда go сначала запрашивает этот модуль у proxy.corp.example.com. Если этот прокси отвечает кодом 404 (Not Found) или 410 (Gone), команда go переходит к proxy.golang.org, передавая путь secret-product в URL-адресе запроса. Если частный прокси отвечает любым другим кодом ошибки, команда go выводит ошибку и не переходит к другим источникам.
В дополнение к прокси, команда go может подключаться к базе данных контрольных сумм для проверки криптографических хэшей модулей, не указанных в go.sum. Переменная среды GOSUMDB устанавливает имя, URL и открытый ключ базы данных контрольных сумм. По умолчанию GOSUMDB имеет значение sum.golang.org, открытая база данных контрольных сумм, управляемая Google (политика конфиденциальности). См. База данных контрольных сумм для получения подробной информации о том, что передается с каждым запросом. Как и в случае с прокси-серверами, команда go не передает личную информацию, но передает полный путь к запрашиваемому модулю, а база данных контрольных сумм не может вычислять контрольные суммы для непубличных модулей.
Переменная среды GONOSUMDB может быть установлена на шаблоны, указывающие, какие модули являются частными и не должны запрашиваться из базы данных контрольных сумм. GOPRIVATE действует как значение по умолчанию для GONOSUMDB и GONOPROXY, поэтому нет необходимости устанавливать GONOSUMDB, если GONOPROXY не должен иметь другое значение.
Прокси может зеркалировать базу данных контрольных сумм. Если прокси в GOPROXY делает это, команда go не будет подключаться к базе данных контрольных сумм напрямую.
GOSUMDB может быть установлена в off, чтобы полностью отключить использование базы данных контрольных сумм. С этой настройкой команда go не будет аутентифицировать загруженные модули, если они уже не находятся в go.sum. См. Аутентификация модулей.
Кэш модулей
Кэш модулей — это каталог, в котором команда go хранит загруженные файлы модулей. Кэш модулей отличается от кэша сборки, который содержит скомпилированные пакеты и другие артефакты сборки.
По умолчанию кэш модулей находится в каталоге $GOPATH/pkg/mod. Чтобы использовать другое местоположение, установите переменную среды GOMODCACHE.
Кэш модулей не имеет максимального размера, и команда go не удаляет его содержимое автоматически.
Кэш может использоваться совместно несколькими проектами Go, разрабатываемыми на одном компьютере. Команда go будет использовать один и тот же кэш независимо от местоположения главного модуля. Несколько экземпляров команды go могут безопасно одновременно обращаться к одному и тому же кэшу модулей.
Команда go создает исходные файлы модулей и каталоги в кэше с правами только для чтения, чтобы предотвратить случайные изменения модулей после их загрузки. Это имеет нежелательный побочный эффект, затрудняющий удаление кэша с помощью таких команд, как rm -rf. Вместо этого кэш можно удалить с помощью go clean -modcache.
В качестве альтернативы, при использовании флага -modcacherw команда go создаст новые каталоги с правами на чтение и запись. Это увеличивает риск изменения файлов в кэше модулей редакторами, тестами и другими программами.
Команда go mod verify может использоваться для обнаружения изменений в зависимостях основного модуля. Она сканирует извлеченное содержимое каждой зависимости модуля и подтверждает, что оно соответствует ожидаемому хешу в go.sum.
В таблице ниже объясняется назначение большинства файлов в кэше модулей. Некоторые временные файлы (файлы блокировки, временные каталоги) опущены. Для каждого пути $module — это путь к модулю, а $version — версия. Пути, заканчивающиеся косыми чертами (/), являются каталогами. Заглавные буквы в путях к модулям и версиях экранируются с помощью восклицательных знаков (Azure экранируется как !azure), чтобы избежать конфликтов в файловых системах, нечувствительных к регистру.
| Путь |
Описание |
| $module@$version/ |
Каталог, содержащий извлеченное содержимое файла .zip модуля. Он служит корневым каталогом для загруженного модуля. Он не будет содержать файл go.mod, если исходный модуль не имел такого файла. |
| cache/download/ |
Каталог, содержащий файлы, загруженные с прокси-серверов модулей, и файлы, полученные из систем контроля версий. Структура этого каталога соответствует протоколу GOPROXY, поэтому этот каталог может использоваться в качестве прокси-сервера при обслуживании HTTP-файловым сервером или при обращении по URL-адресу file://. |
| cache/download/$module/@v/list |
Список известных версий (см. протокол GOPROXY). Он может меняться со временем, поэтому команда go обычно загружает новую копию вместо повторного использования этого файла. |
| cache/download/$module/@v/$version.info |
JSON-метаданные о версии. (см. протокол GOPROXY). Это может измениться со временем, поэтому команда go обычно загружает новую копию вместо повторного использования этого файла. |
| cache/download/$module/@v/$version.mod |
Файл go.mod для этой версии (см. протокол GOPROXY). Если исходный модуль не имел файла go.mod, это синтезированный файл без требований. |
| cache/download/$module/@v/$version.zip |
Сжатое содержимое модуля (см. протокол GOPROXY и zip-файлы модулей). |
| cache/download/$module/@v/$version.ziphash |
Криптографический хеш файлов в файле .zip. Обратите внимание, что сам файл .zip не хешируется, поэтому порядок файлов, сжатие, выравнивание и метаданные не влияют на хеш. При использовании модуля команда go проверяет, что этот хеш соответствует соответствующей строке в go.sum. Команда go mod verify проверяет, что хеши файлов .zip модуля и извлеченных каталогов соответствуют этим файлам. |
| cache/download/sumdb/ |
Каталог, содержащий файлы, загруженные из базы данных контрольных сумм (обычно sum.golang.org). |
| cache/vcs/ |
Содержит клонированные репозитории контроля версий для модулей, полученных непосредственно из их источников. Имена каталогов представляют собой хэши в шестнадцатеричном кодировании, полученные из типа репозитория и URL. Репозитории оптимизированы по размеру на диске. Например, клонированные репозитории Git являются «голыми» и «неглубокими», когда это возможно. |
Аутентификация модулей
Когда команда go загружает zip-файл модуля или файл go.mod в кэш модулей, она вычисляет криптографический хеш и сравнивает его с известным значением, чтобы убедиться, что файл не изменился с момента его первой загрузки. Команда go сообщает об ошибке безопасности, если загруженный файл не имеет правильного хеша.
Для файлов go.mod команда go вычисляет хеш из содержимого файла. Для zip-файлов модулей команда go вычисляет хеш из имен и содержимого файлов в архиве в детерминированном порядке. На хеш не влияют порядок файлов, сжатие, выравнивание и другие метаданные. См. golang.org/x/mod/sumdb/dirhash для получения подробной информации о реализации хеша.
Команда go сравнивает каждый хеш с соответствующей строкой в файле go.sum основного модуля. Если хеш отличается от хеша в go.sum, команда go сообщает об ошибке безопасности и удаляет загруженный файл, не добавляя его в кэш модуля.
Если файл go.sum отсутствует или не содержит хэш для загруженного файла, команда go может проверить хэш с помощью базы данных контрольных сумм, глобального источника хэшей для общедоступных модулей. После проверки хеша команда go добавляет его в go.sum и добавляет загруженный файл в кэш модулей. Если модуль является частным (соответствует переменным окружения GOPRIVATE или GONOSUMDB) или если база данных контрольных сумм отключена (путем установки GOSUMDB=off), команда go принимает хеш и добавляет файл в кэш модулей без проверки.
Кэш модулей обычно используется всеми проектами Go в системе, и каждый модуль может иметь свой собственный файл go.sum с потенциально разными хэшами. Чтобы избежать необходимости доверять другим модулям, команда go проверяет хэши с помощью go.sum основного модуля при каждом доступе к файлу в кэше модулей. Вычисление хэшей zip-файлов требует больших затрат, поэтому команда go проверяет заранее вычисленные хэши, хранящиеся вместе с zip-файлами, вместо повторного хеширования файлов. Команда go mod verify может использоваться для проверки того, что zip-файлы и извлеченные каталоги не были изменены с момента их добавления в кэш модулей.
Файлы go.sum
Модуль может иметь текстовый файл с именем go.sum в своем корневом каталоге, наряду с файлом go.mod. Файл go.sum содержит криптографические хэши прямых и косвенных зависимостей модуля. Когда команда go загружает файл .mod или .zip модуля в кэш модулей, она вычисляет хеш и проверяет, что он совпадает с соответствующим хешем в файле go.sum основного модуля. go.sum может быть пустым или отсутствовать, если модуль не имеет зависимостей или если все зависимости заменены локальными каталогами с помощью директив replace.
Каждая строка в go.sum имеет три поля, разделенных пробелами: путь к модулю, версия (возможно, заканчивающаяся на /go.mod) и хеш.
- Путь к модулю — это имя модуля, к которому принадлежит хеш.
- Версия — это версия модуля, к которому принадлежит хеш. Если версия заканчивается на
/go.mod, хеш относится только к файлу go.mod модуля; в противном случае хеш относится к файлам в файле .zip модуля.
- Столбец хеша состоит из имени алгоритма (например, h1) и криптографического хеша, закодированного в base64, разделенных двоеточием (:). В настоящее время SHA-256 (h1) является единственным поддерживаемым алгоритмом хеширования. Если в будущем будет обнаружена уязвимость в SHA-256, будет добавлена поддержка другого алгоритма (с именем h2 и так далее).
Файл go.sum может содержать хэши для нескольких версий модуля. Команде go может потребоваться загрузить файлы go.mod из нескольких версий зависимости, чтобы выполнить выбор минимальной версии. go.sum также может содержать хэши для версий модулей, которые больше не нужны (например, после обновления). go mod tidy добавит отсутствующие хэши и удалит ненужные хэши из go.sum.
База данных контрольных сумм
База данных контрольных сумм является глобальным источником строк go.sum. Команда go может использовать ее во многих ситуациях для обнаружения некорректного поведения прокси-серверов или исходных серверов.
База данных контрольных сумм обеспечивает глобальную согласованность и надежность всех общедоступных версий модулей. Она делает возможным использование ненадежных прокси-серверов, поскольку они не могут предоставлять неверный код, который останется незамеченным. Она также гарантирует, что биты, связанные с конкретной версией, не изменятся со дня на день, даже если автор модуля впоследствии изменит теги в своем репозитории.
База данных контрольных сумм обслуживается sum.golang.org, который управляется Google. Это прозрачный журнал (или «дерево Меркла») хэшей строк go.sum, который поддерживается Trillian. Главное преимущество дерева Меркла заключается в том, что независимые аудиторы могут проверить, что оно не было подделано, поэтому оно более надежно, чем простая база данных.
Команда go взаимодействует с базой данных контрольных сумм, используя протокол, первоначально описанный в предложении: «Обеспечение безопасности экосистемы публичных модулей Go».
В таблице ниже указаны запросы, на которые должна отвечать база данных контрольных сумм. Для каждого пути $base — это часть URL-адреса базы данных контрольных сумм, $module — путь к модулю, а $version — версия. Например, если URL базы данных контрольных сумм — https://sum.golang.org, а клиент запрашивает запись для модуля golang.org/x/text версии v0.3.2, клиент отправит запрос GET на https://sum.golang.org/lookup/golang.org/x/text@v0.3.2.
Чтобы избежать неоднозначности при обслуживании из файловых систем, нечувствительных к регистру, элементы $module и $version кодируются с учетом регистра, заменяя каждую заглавную букву восклицательным знаком, за которым следует соответствующая строчная буква. Это позволяет хранить на диске модули example.com/M и example.com/m, поскольку первый кодируется как example.com/!m.
Части пути, заключенные в квадратные скобки, такие как [.p/$W], обозначают необязательные значения.
| Путь |
Описание |
| $base/latest |
Возвращает подписанное, закодированное описание дерева для последнего журнала. Это подписанное описание имеет форму примечания, которое представляет собой текст, подписанный одним или несколькими ключами сервера и который можно проверить с помощью открытого ключа сервера. Описание дерева содержит размер дерева и хэш вершины дерева при данном размере. Это кодирование описано в golang.org/x/mod/sumdb/tlog#FormatTree. |
| $base/lookup/$module@$version |
Возвращает номер записи журнала для записи о $module в $version, за которым следуют данные для записи (то есть строки go.sum для $module в $version) и подписанное, закодированное описание дерева, которое содержит запись. |
| $base/tile/$H/$L/$K[.p/$W] |
Возвращает блоку журнала, который представляет собой набор хешей, составляющих раздел журнала. Каждый блок определяется в двумерных координатах на уровне блока $L, $K-м слева, с высотой блока $H. Необязательный суффикс .p/$W указывает на частичную плитку журнала, содержащую только $W хешей. Клиенты должны перейти к извлечению полной плитки, если частичная плитка не найдена. |
| $base/tile/$H/data/$K[.p/$W] |
Возвращает данные записи для листьевых хешей в /tile/$H/0/$K[.p/$W] (с буквальным элементом пути данных). |
Если команда go обращается к базе данных контрольных сумм, то первым шагом является извлечение данных записи через конечную точку /lookup. Если версия модуля еще не записана в журнале, база данных контрольных сумм попытается получить ее с исходного сервера перед ответом. Эти данные /lookup предоставляют сумму для этой версии модуля, а также ее положение в журнале, что информирует клиента о том, какие плитки следует загрузить для выполнения проверок. Команда go выполняет проверки «включения» (наличие определенной записи в журнале) и «согласованности» (отсутствие подделки дерева) перед добавлением новых строк go.sum в файл go.sum основного модуля. Важно, чтобы данные из /lookup никогда не использовались без предварительной аутентификации по подписанному хешу дерева и аутентификации подписанного хеша дерева по временной шкале подписанных хешей дерева клиента.
Подписанные хеши дерева и новые плитки, обслуживаемые базой данных контрольных сумм, хранятся в кэше модуля, поэтому команде go нужно загружать только отсутствующие плитки.
Команде go не нужно напрямую подключаться к базе данных контрольных сумм. Она может запрашивать суммы модулей через прокси-сервер модулей, который зеркалирует базу данных контрольных сумм и поддерживает вышеуказанный протокол. Это может быть особенно полезно для частных корпоративных прокси-серверов, которые блокируют запросы за пределами организации.
Переменная среды GOSUMDB определяет имя базы данных контрольных сумм, которую следует использовать, и, опционально, ее открытый ключ и URL, как в примере:
GOSUMDB="sum.golang.org"
GOSUMDB="sum.golang.org+<publickey>"
GOSUMDB="sum.golang.org+<publickey> https://sum.golang.org"
Команда go знает открытый ключ sum.golang.org, а также то, что имя sum.golang.google.cn (доступно на территории материкового Китая) соединяет с базой данных контрольных сумм sum.golang.org; использование любой другой базы данных требует явного указания открытого ключа. URL по умолчанию https://, за которым следует имя базы данных.
GOSUMDB по умолчанию указывает на sum.golang.org, базу данных контрольных сумм Go, управляемую Google. Политику конфиденциальности сервиса смотрите на https://sum.golang.org/privacy.
Если для GOSUMDB установлено значение off, или если go get вызывается с флагом -insecure, база данных контрольных сумм не проверяется, и все нераспознанные модули принимаются, ценой отказа от гарантии безопасности проверенных повторяющихся загрузок для всех модулей. Лучший способ обойти базу данных контрольных сумм для определенных модулей - использовать переменные окружения GOPRIVATE или GONOSUMDB. Подробности см. в разделе Приватные модули.
Команда go env -w может быть использована для установки этих переменных для последующих вызовов команды go.
Переменные окружения
Поведение модуля в команде go может быть настроено с помощью переменных окружения, перечисленных ниже. В этот список включены только переменные окружения, связанные с модулем. Список всех переменных окружения, распознаваемых командой go, см. в go help environment.
- GO111MODULE
-
Управляет тем, будет ли команда go работать в режиме с поддержкой модулей или в режиме GOPATH. Поддерживаются три значения:
- off: команда go игнорирует файлы
go.mod и работает в режиме GOPATH.
- on (или unset): команда
go работает в режиме с поддержкой модулей, даже если файл go.mod отсутствует.
- auto: команда
go работает в режиме с поддержкой модулей, если файл go.mod присутствует в текущем каталоге или любом родительском каталоге. В Go 1.15 и ниже это было значением по умолчанию.
Дополнительные сведения см. в разделе Команды с поддержкой модулей.
- GOMODCACHE
-
Каталог, в котором команда go будет хранить загруженные модули и связанные файлы. Подробные сведения о структуре этого каталога см. в разделе Кэш модулей.
Если GOMODCACHE не установлен, по умолчанию используется $GOPATH/pkg/mod.
- GOINSECURE
-
Разделенный запятыми список шаблонов glob (в синтаксисе path.Match Go) префиксов путей модулей, которые всегда могут быть загружены небезопасным способом. Применяется только к зависимостям, которые загружаются напрямую.
В отличие от флага -insecure в go get, GOINSECURE не отключает проверку базы данных контрольных сумм модулей. Для этого можно использовать GOPRIVATE или GONOSUMDB.
- GONOPROXY
-
Разделенный запятыми список шаблонов glob (в синтаксисе path.Match Go) префиксов путей модулей, которые всегда должны загружаться напрямую из репозиториев контроля версий, а не из прокси-серверов модулей.
Если GONOPROXY не установлен, по умолчанию используется GOPRIVATE. См. раздел «Конфиденциальность».
- GONOSUMDB
-
Разделенный запятыми список шаблонов glob (в синтаксисе path.Match Go) префиксов путей модулей, для которых go не должен проверять контрольные суммы с помощью базы данных контрольных сумм.
Если GONOSUMDB не установлен, по умолчанию используется GOPRIVATE. См. раздел «Конфиденциальность».
- GOPATH
-
В режиме GOPATH переменная GOPATH представляет собой список каталогов, которые могут содержать код Go.
В режиме с поддержкой модулей кэш модулей хранится в подкаталоге pkg/mod первого каталога GOPATH. Исходный код модулей, не входящих в кэш, может храниться в любом каталоге.
Если GOPATH не задан, по умолчанию используется подкаталог go в домашнем каталоге пользователя.
- GOPRIVATE
-
Разделенный запятыми список шаблонов
glob (в синтаксисе path.Match Go) префиксов путей модулей, которые должны считаться частными. GOPRIVATE является значением по умолчанию для GONOPROXY и GONOSUMDB. См. раздел «Конфиденциальность». GOPRIVATE также определяет, считается ли модуль частным для GOVCS.
- GOPROXY
-
Список URL-адресов прокси-серверов модулей, разделенных запятыми (,) или вертикальными чертами (|). Когда команда go ищет информацию о модуле, она обращается к каждому прокси-серверу в списке по порядку, пока не получит успешный ответ или терминальную ошибку. Прокси-сервер может ответить статусом 404 (Not Found) или 410 (Gone), чтобы указать, что модуль недоступен на этом сервере.
Поведение команды go при ошибке определяется разделителями между URL-адресами. Если за URL-адресом прокси следует запятая, команда go переходит к следующему URL-адресу после ошибки 404 или 410; все другие ошибки считаются терминальными. Если за URL-адресом прокси следует вертикальная черта, команда go переходит к следующему источнику после любой ошибки, включая ошибки, не связанные с HTTP, такие как таймауты.
URL-адреса GOPROXY могут иметь схемы https, http или file. Если URL-адрес не имеет схемы, предполагается, что он имеет схему https. Кэш модулей может использоваться непосредственно в качестве прокси-файла:
GOPROXY=file://$(go env GOMODCACHE)/cache/download
Вместо URL-адресов прокси могут использоваться два ключевых слова:
- off: запрещает загрузку модулей из любого источника.
- direct: загрузка напрямую из репозиториев контроля версий вместо использования прокси-сервера модулей.
По умолчанию GOPROXY имеет значение https://proxy.golang.org,direct. При такой конфигурации команда go сначала связывается с зеркалом модулей Go, управляемым Google, а затем переходит к прямому подключению, если зеркало не имеет модуля. Политику конфиденциальности зеркала см. на странице https://proxy.golang.org/privacy. Переменные среды GOPRIVATE и GONOPROXY могут быть установлены, чтобы предотвратить загрузку определенных модулей с помощью прокси. См. раздел «Конфиденциальность» для получения информации о настройке частного прокси.
См. разделы «Прокси модулей» и «Преобразование пакета в модуль» для получения дополнительной информации об использовании прокси.
- GOSUMDB
-
Определяет имя базы данных контрольных сумм, которую следует использовать, а также, по желанию, ее открытый ключ и URL. Например:
GOSUMDB="sum.golang.org"
GOSUMDB="sum.golang.org+<publickey>"
GOSUMDB="sum.golang.org+<publickey> https://sum.golang.org"
Команда go знает открытый ключ sum.golang.org, а также то, что имя sum.golang.google.cn (доступное на территории Китая) подключается к базе данных sum.golang.org; для использования любой другой базы данных необходимо явно указать открытый ключ. По умолчанию URL-адрес имеет вид https://, за которым следует имя базы данных.
По умолчанию GOSUMDB имеет значение sum.golang.org, базу данных контрольных сумм Go, управляемую Google. См. https://sum.golang.org/privacy для ознакомления с политикой конфиденциальности службы.
Если GOSUMDB установлен в положение «off» или если go get вызывается с флагом -insecure, база данных контрольных сумм не используется, и все нераспознанные модули принимаются, что приводит к потере гарантии безопасности проверенных повторяемых загрузок для всех модулей. Лучший способ обойти базу данных контрольных сумм для определенных модулей — использовать переменные среды GOPRIVATE или GONOSUMDB.
Дополнительную информацию см. в разделах «Аутентификация модулей» и «Конфиденциальность».
- GOVCS
-
Управляет набором инструментов контроля версий, которые команда go может использовать для загрузки общедоступных и частных модулей (определяемых по тому, соответствуют ли их пути шаблону в GOPRIVATE) или других модулей, соответствующих шаблону glob.
Если GOVCS не установлен или если модуль не соответствует ни одному шаблону в GOVCS, команда go может использовать git и hg для публичного модуля или любой известный инструмент контроля версий для частного модуля. Конкретно, команда go действует так, как если бы GOVCS был установлен на:
public:git|hg,private:all
См. Управление инструментами контроля версий с помощью GOVCS для полного объяснения.
- GOWORK
-
Переменная окружения
GOWORK указывает команде go перейти в режим рабочего пространства, используя предоставленный файл go.work для определения рабочего пространства. Если GOWORK установлен в off, режим рабочего пространства отключается. Это можно использовать для запуска команды go в режиме одного модуля: например, GOWORK=off go build . собирает пакет . в режиме одного модуля. Если GOWORK пуста, команда go будет искать файл go.work, как описано в разделе Рабочие пространства.
Глоссарий
Ограничение сборки: условие, которое определяет, будет ли использоваться исходный файл Go при компиляции пакета. Ограничения сборки могут быть выражены с помощью суффиксов имен файлов (например, foo_linux_amd64.go) или с помощью комментариев об ограничениях сборки (например, // +build linux,amd64). См. Ограничения сборки.
Список сборки: список версий модулей, которые будут использоваться для команды сборки, такой как go build, go list или go test. Список сборки определяется из файла go.mod основного модуля и файлов go.mod в транзитивно требуемых модулях с использованием минимального выбора версии. Список сборки содержит версии для всех модулей в графе модулей, а не только для тех, которые относятся к конкретной команде.
каноническая версия: правильно отформатированная версия без суффикса метаданных сборки, кроме +incompatible. Например, v1.2.3 является канонической версией, а v1.2.3+meta — нет.
текущий модуль: синоним основного модуля.
Устаревший модуль: модуль, который больше не поддерживается его авторами (хотя основные версии считаются отдельными модулями для этой цели). Устаревший модуль помечается комментарием об устаревании в последней версии его файла go.mod.
Прямая зависимость: пакет, путь к которому появляется в декларации импорта в исходном файле .go для пакета или теста в основном модуле, или модуль, содержащий такой пакет. (Сравните с косвенной зависимостью.)
Прямой режим: настройка переменных среды, которая заставляет команду go загружать модуль напрямую из системы контроля версий, а не через прокси-сервер модулей. GOPROXY=direct делает это для всех модулей. GOPRIVATE и GONOPROXY делают это для модулей, соответствующих списку шаблонов.
Файл go.mod: файл, который определяет путь к модулю, требования и другие метаданные. Появляется в корневом каталоге модуля. См. раздел о файлах go.mod.
Файл go.work: файл, который определяет набор модулей, которые будут использоваться в рабочей области. См. раздел о файлах go.work.
Путь импорта: строка, используемая для импорта пакета в исходном файле Go. Синоним пути к пакету.
Косвенная зависимость: пакет, транзитивно импортируемый пакетом или тестом в главном модуле, но путь к которому не появляется ни в одной декларации импорта в главном модуле; или модуль, который появляется в графе модулей, но не предоставляет никаких пакетов, напрямую импортируемых главным модулем. (Сравните с прямой зависимостью.)
отложенная загрузка модулей: изменение в Go 1.17, которое позволяет избежать загрузки графа модулей для команд, которые в этом не нуждаются, в модулях, которые указывают go 1.17 или выше. См. Отложенная загрузка модулей.
главный модуль: модуль, в котором вызывается команда go. Главный модуль определяется файлом go.mod в текущем каталоге или родительском каталоге. См. Модули, пакеты и версии.
Основная версия: первое число в семантической версии (1 в v1.2.3). В выпуске с несовместимыми изменениями основная версия должна быть увеличена, а второстепенная версия и патч-версия должны быть установлены на 0. Семантические версии с основной версией 0 считаются нестабильными.
подкаталог основной версии: подкаталог в репозитории контроля версий, соответствующий суффиксу основной версии модуля, в котором может быть определен модуль. Например, модуль example.com/mod/v2 в репозитории с корневым путем example.com/mod может быть определен в корневом каталоге репозитория или в подкаталоге основной версии v2. См. Каталоги модулей в репозитории.
суффикс основной версии: суффикс пути модуля, соответствующий номеру основной версии. Например, /v2 в example.com/mod/v2. Суффиксы основной версии требуются в v2.0.0 и более поздних версиях и не допускаются в более ранних версиях. См. раздел «Суффиксы основной версии».
Выбор минимальной версии (MVS): алгоритм, используемый для определения версий всех модулей, которые будут использоваться в сборке. Подробности см. в разделе «Выбор минимальной версии».
Второстепенная версия: второе число в семантической версии (2 в v1.2.3). В выпуске с новой, обратно совместимой функциональностью второстепенная версия должна быть увеличена, а патч-версия должна быть установлена на 0.
Модуль: набор пакетов, которые выпускаются, версионируются и распространяются вместе.
Кэш модулей: локальный каталог, в котором хранятся загруженные модули, расположенный в GOPATH/pkg/mod. См. Кэш модулей.
Граф модулей: ориентированный граф требований модулей, корнем которого является главный модуль. Каждая вершина в графе — это модуль; каждая ребро — это версия из оператора require в файле go.mod (с учетом операторов replace и exclude в файле go.mod главного модуля).
Обрезка графа модулей: изменение в Go 1.17, которое уменьшает размер графа модулей за счет исключения транзитивных зависимостей модулей, которые указывают go 1.17 или выше. См. Обрезка графа модулей.
Путь модуля: путь, который идентифицирует модуль и действует как префикс для путей импорта пакетов внутри модуля. Например, «golang.org/x/net».
Прокси модуля: веб-сервер, который реализует протокол GOPROXY. Команда go загружает информацию о версии, файлы go.mod и zip-файлы модулей с прокси модулей.
Корневой каталог модуля: каталог, который содержит файл go.mod, определяющий модуль.
Подкаталог модуля: часть пути к модулю после корневого пути репозитория, которая указывает подкаталог, в котором определен модуль. Если подкаталог модуля не пустой, он также является префиксом для семантических тегов версии. Подкаталог модуля не включает суффикс основной версии, если таковой имеется, даже если модуль находится в подкаталоге основной версии. См. «Пути к модулям».
Пакет: набор исходных файлов в одном каталоге, которые компилируются вместе. См. раздел «Пакеты» в спецификации языка Go.
Путь к пакету: путь, который однозначно идентифицирует пакет. Путь к пакету — это путь к модулю, соединенный с подкаталогом внутри модуля. Например, «golang.org/x/net/html» — это путь к пакету в модуле «golang.org/x/net» в подкаталоге «html». Синоним пути импорта.
версия патча: третье число в семантической версии (3 в v1.2.3). В выпуске без изменений в публичном интерфейсе модуля версия патча должна быть увеличена.
Предварительная версия: версия с тире, за которым следует ряд идентификаторов, разделенных точками, непосредственно после патч-версии, например, v1.2.3-beta4. Предварительные версии считаются нестабильными и не предполагают совместимости с другими версиями. Предварительная версия сортируется перед соответствующей релизной версией: v1.2.3-pre идет перед v1.2.3. См. также релизная версия.
Псевдоверсия: версия, которая кодирует идентификатор ревизии (например, хэш Git commit) и временную метку из системы контроля версий. Например, v0.0.0-20191109021931-daa7c04131f5. Используется для совместимости с репозиториями, не являющимися модулями, и в других ситуациях, когда версия с тегом недоступна.
релизная версия: версия без суффикса предварительной версии. Например, v1.2.3, а не v1.2.3-pre. См. также предварительная версия.
корневой путь репозитория: часть пути модуля, которая соответствует корневому каталогу репозитория системы контроля версий. См. Пути модулей.
Отзывная версия: версия, на которую не следует полагаться, либо потому что она была опубликована преждевременно, либо потому что после ее публикации была обнаружена серьезная проблема. См. директиву retract.
Семантическая метка версии: метка в репозитории контроля версий, которая сопоставляет версию с конкретной ревизией. См. Сопоставление версий с коммитами.
выбранная версия: версия данного модуля, выбранная с помощью минимального выбора версии. Выбранная версия — это самая высокая версия для пути модуля, найденная в графе модулей.
каталог поставщика: каталог с именем vendor, содержащий пакеты из других модулей, необходимые для сборки пакетов в основном модуле. Обслуживается с помощью go mod vendor. См. раздел «Поставщики».
версия: идентификатор неизменяемого моментального снимка модуля, записываемый в виде буквы v, за которой следует семантическая версия. См. раздел «Версии».
рабочая область: набор модулей на диске, которые используются в качестве основных модулей при выполнении минимального выбора версии (MVS). См. раздел «Рабочие области».
9 - Изучение языка Go
Различные учебники и туториалс по языку Go
9.1 - Alex Edwards Let's Go часть 1
edwardsLetsGo учебный материал по написанию приложения на Go с авторизацией пользователя. Краткий конспект с основными мыслями (главы с 1 по 8)
Основы Web приложения
package main
import (
"log" //пакет для логирования
"net/http" //пакет для создания web приложений
)
func home(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from Snippetbox"))
}
//основная функция, `home` которая получает на вход указатель кто слушает канал:
//w http.ResponseWriter чтобы выводить значения в web
//r *http.Request - структура, которую получаем на вход
func main() {
mux := http.NewServeMux()
//указатель на web сервер
mux.HandleFunc("/", home)
//обработчик запросов
log.Println("Starting server on :4000")
//логируем подключение
err := http.ListenAndServe(":4000", mux)
//слушаем порт
log.Fatal(err)
}
type ResponseWriter interface {
// Header возвращает карту заголовков, которая будет отправлена
// [ResponseWriter.WriteHeader].
Header() Header
// Write записывает данные в соединение как часть HTTP-ответа.
Write([]byte) (int, error)
// WriteHeader отправляет заголовок ответа HTTP с указанным
// кодом состояния.
WriteHeader(statusCode int)
}
type Request struct {
// Method задает метод HTTP (GET, POST, PUT и т.д.).
Method string
// Для клиентских запросов URL's Host указывает сервер, к которому нужно // подключиться.
URL *url.URL
// Версия протокола для входящих запросов сервера.
Proto string // «HTTP/1.0»
ProtoMajor int // 1
ProtoMinor int // 0
// Заголовок содержит поля заголовка запроса, которые либо получены
// сервером, либо должны быть отправлены клиентом.
//
// Host: example.com
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// fOO: Bar
// foo: two
Request Header
// Body - это тело запроса.
Body io.ReadCloser
// GetBody определяет необязательную функцию для возврата новой копии Body.
GetBody func() (io.ReadCloser, error)
// ContentLength записывает длину связанного содержимого.
ContentLength int64
// TransferEncoding перечисляет кодировки передачи данных от крайних к внутренней.
TransferEncoding []string
Close bool
// Для серверных запросов поле Host указывает хост, на котором URL-адрес.
Host String
// Form содержит разобранные данные формы
Form url.Values
// PostForm содержит разобранные данные формы из параметров тела PATCH, POST
// или параметров тела PUT.
PostForm url.Values
// MultipartForm - это разобранная многочастная форма, включая загрузку файлов.
MultipartForm *multipart.Form
// Trailer определяет дополнительные заголовки, которые отправляются после запроса
// body.
Trailer Header
// RemoteAddr позволяет HTTP-серверам и другому программному обеспечению записывать сетевой адрес, отправивший запрос
RemoteAddr String
// RequestURI - это немодифицированный запрос-цель в
RequestURI String
// TLS позволяет HTTP-серверам и другому программному обеспечению записывать информацию о TLS-соединении
TLS *tls.ConnectionState
// Cancel - необязательный канал, закрытие которого указывает на то, что клиентский
// запрос следует считать отмененным.
Cancel <-chan struct{}
// Response - это ответ перенаправления, который вызвал этот запрос.
Response *Response
// Pattern - шаблон [ServeMux], который соответствует запросу.
Pattern string
// содержит отфильтрованные или неотфильтрованные поля
}
Расширение обработчиков
package main
import (
"log"
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) { w
.Write([]byte("Hello from Snippetbox"))
} создаем функции обработчики каждого адреса
func snippetView(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Display a specific snippet..."))
} // для теста просто выводим что-нибудь
func snippetCreate(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Create a new snippet...")) }
func main() {
// Register the two new handler functions and corresponding URL patterns with // the servemux, in exactly the same way that we did before.
mux := http.NewServeMux()
mux.HandleFunc("/", home)
mux.HandleFunc("/snippet/view", snippetView)
mux.HandleFunc("/snippet/create", snippetCreate)
// обрабатываем каждый адрес из браузера и запускаем функцию обработчик
log.Println("Starting server on :4000")
err := http.ListenAndServe(":4000", mux)
log.Fatal(err)
}
Типы адресов в обработчике
/snippet/view — фиксированный адрес
/snippet/view/ — не фиксированный адрес и под ним обрабатывается поддерево
- Для корня проверка если указано что-то отличающееся от
/
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Write([]byte("Hello from Snippetbox"))
DefaultServeMux
var DefaultServeMux = NewServeMux()
это неявная переменная, которая хранит указатель на сервер и используется глобально. Поэтому сервера нужно создавать локально внутри main
У маршрутизатора Go могут быть проблемы с обработкой переменных
Но это по всей видимости в старых версиях. Новые могут больше и есть дополнительные модули, которые полноценно обрабатывают строку запроса
Отправка статуса
func snippetCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" { // просто покажет каким методом отправили запрос
w.Header().Set("Allow", "POST")` //- эта настройка подскажет в ответе, какой метод разрешен
w.WriteHeader(405) // отправим код статуса в заголовке ответа
w.Write([]byte("Method Not Allowed")) // и просто напишем сообщение в случае неправильного кода
return
}
w.Write([]byte("Create a new snippet...")) //это сообщение успеха
}
curl -i -X POST http://localhost:4000/snippet/create // это для тестирования из командной строки
w.Header().Set("Allow", "POST") - эта настройка подскажет в ответе, какой метод разрешен
w.Write([]byte("Method Not Allowed")) у этой команды есть альтернатива более приемлемая: http.Error(w, "Method Not Allowed", 405)
и еще есть специальные константы с указанием статусов: http.StatusMethodNotAllowed вместо 405
Статусы
const (
StatusContinue = 100 // RFC 9110, 15.2.1
StatusSwitchingProtocols = 101 // RFC 9110, 15.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusEarlyHints = 103 // RFC 8297
StatusOK = 200 // RFC 9110, 15.3.1
StatusCreated = 201 // RFC 9110, 15.3.2
StatusAccepted = 202 // RFC 9110, 15.3.3
StatusNonAuthoritativeInfo = 203 // RFC 9110, 15.3.4
StatusNoContent = 204 // RFC 9110, 15.3.5
StatusResetContent = 205 // RFC 9110, 15.3.6
StatusPartialContent = 206 // RFC 9110, 15.3.7
StatusMultiStatus = 207 // RFC 4918, 11.1
StatusAlreadyReported = 208 // RFC 5842, 7.1
StatusIMUsed = 226 // RFC 3229, 10.4.1
StatusMultipleChoices = 300 // RFC 9110, 15.4.1
StatusMovedPermanently = 301 // RFC 9110, 15.4.2
StatusFound = 302 // RFC 9110, 15.4.3
StatusSeeOther = 303 // RFC 9110, 15.4.4
StatusNotModified = 304 // RFC 9110, 15.4.5
StatusUseProxy = 305 // RFC 9110, 15.4.6
StatusTemporaryRedirect = 307 // RFC 9110, 15.4.8
StatusPermanentRedirect = 308 // RFC 9110, 15.4.9
StatusBadRequest = 400 // RFC 9110, 15.5.1
StatusUnauthorized = 401 // RFC 9110, 15.5.2
StatusPaymentRequired = 402 // RFC 9110, 15.5.3
StatusForbidden = 403 // RFC 9110, 15.5.4
StatusNotFound = 404 // RFC 9110, 15.5.5
StatusMethodNotAllowed = 405 // RFC 9110, 15.5.6
StatusNotAcceptable = 406 // RFC 9110, 15.5.7
StatusProxyAuthRequired = 407 // RFC 9110, 15.5.8
StatusRequestTimeout = 408 // RFC 9110, 15.5.9
StatusConflict = 409 // RFC 9110, 15.5.10
StatusGone = 410 // RFC 9110, 15.5.11
StatusLengthRequired = 411 // RFC 9110, 15.5.12
StatusPreconditionFailed = 412 // RFC 9110, 15.5.13
StatusRequestEntityTooLarge = 413 // RFC 9110, 15.5.14
StatusRequestURITooLong = 414 // RFC 9110, 15.5.15
StatusUnsupportedMediaType = 415 // RFC 9110, 15.5.16
StatusRequestedRangeNotSatisfiable = 416 // RFC 9110, 15.5.17
StatusExpectationFailed = 417 // RFC 9110, 15.5.18
StatusTeapot = 418 // RFC 9110, 15.5.19 (Unused)
StatusMisdirectedRequest = 421 // RFC 9110, 15.5.20
StatusUnprocessableEntity = 422 // RFC 9110, 15.5.21
StatusLocked = 423 // RFC 4918, 11.3
StatusFailedDependency = 424 // RFC 4918, 11.4
StatusTooEarly = 425 // RFC 8470, 5.2.
StatusUpgradeRequired = 426 // RFC 9110, 15.5.22
StatusPreconditionRequired = 428 // RFC 6585, 3
StatusTooManyRequests = 429 // RFC 6585, 4
StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5
StatusUnavailableForLegalReasons = 451 // RFC 7725, 3
StatusInternalServerError = 500 // RFC 9110, 15.6.1
StatusNotImplemented = 501 // RFC 9110, 15.6.2
StatusBadGateway = 502 // RFC 9110, 15.6.3
StatusServiceUnavailable = 503 // RFC 9110, 15.6.4
StatusGatewayTimeout = 504 // RFC 9110, 15.6.5
StatusHTTPVersionNotSupported = 505 // RFC 9110, 15.6.6
StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1
StatusInsufficientStorage = 507 // RFC 4918, 11.5
StatusLoopDetected = 508 // RFC 5842, 7.2
StatusNotExtended = 510 // RFC 2774, 7
StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
)
Заголовок запроса
Автоматически устанавливаются три переменные запроса:
Date и Content-Length и Content-Type.
Content-Length определяется автоматически, но лучше установить явно:
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"name":"Alex"}`))
Методы управления заголовком
- w.Header().Set()
- w.Header().Add()
- w.Header().Del()
- w.Header().Get()
- w.Header().Values()
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Add("Cache-Control", "public")
w.Header().Add("Cache-Control", "max-age=31536000")
w.Header().Del("Cache-Control")
w.Header().Get("Cache-Control")
w.Header().Values("Cache-Control")
Канонические имена в заголовке
Все имена в заголовке преобразуются в канонические: Первая буква с заглавной
Если нужно отменить преобразование:
w.Header()["X-XSS-Protection"] = []string{"1; mode=block"}
Подавление системных заголовков
потому-что DEL не удаляет их
Query
/snippet/view?id=1 // обработка строки запроса
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil || id < 1 {
http.NotFound(w, r)
return
}
Структура проекта
- Main оставить в одном файле
- Handler вынести в отдельный файл с такой же структурой
internal
создадим директорию для зависимых пакетов для проекта
html шаблоны
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>Home - Snippetbox</title>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<main>
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
</body>
</html>
html/template - управляет шаблонами html из GO
ts, err := template.ParseFiles("./ui/html/pages/home.tmpl")
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
return
}
загружаем и парсим шаблон. Теперь он в переменной ts
err = ts.Execute(w, nil) //отправляет шаблон в поток w
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
}
Шаблоны отдельно
Делаем составные шаблоны
- Базовый шаблон base.tmpl
Задает основную структуру страницы:
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
</body>
</html>
{{end}}
{{define "base"}} - определяет основной блок, внутри вся структура
{{template "title" .}} - заголовок указанный в переменной title, которую потом подставим
{{template "main" .}} - название другого блока с именем main
чтобы вставить template их нужно объявить как define
{{define "title"}}Home{{end}}
{{define "main"}}
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
{{end}}
Создание слайса с шаблонами
files := []string{
"./ui/html/base.tmpl",
"./ui/html/pages/home.tmpl",
}
что примечательного в этом коде: здесь нужно перечислить все шаблоны с их путями, которые будут собираться для вывода страницы
и немного поменялся парсер, теперь ему передадим не один шаблон, а слайсер с шаблонами
ts, err := template.ParseFiles(files...)
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
return
}
и поменяется метод для сборки всех шаблонов в одну страницу:
err = ts.ExecuteTemplate(w, "base", nil)
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
}
ts.ExecuteTemplate(w, "base", nil) - указываем главный шаблон base, в который будут собираться остальные
в итоге получили такой шаблон:
Итоговый шаблон
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<!-- Invoke the navigation template -->
{{template "nav" .}}
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
</body>
</html>
{{end}}
Block
Это похоже на template, только является необязательным
{{define "base"}}
<h1>An example template</h1>
{{block "sidebar" .}}
<p>My default sidebar content</p>
{{end}}
{{end}}
Еще есть функционал для встраивания файлов прямо в код с пакетом: embed
Статические файлы
http.FileServer - встроенный обработчик статическийх файлов
fileServer := http.FileServer(http.Dir("./ui/static/"))
для подключения обработчика статических файлов необходимо:
mux.Handle("/static/", http.StripPrefix("/static", fileServer)) //http.StripPrefix позволяет правильно работать в адресной строке с именами файлов
Что можно положить в статику:
Пример
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
<!-- Link to the CSS stylesheet and favicon -->
<link rel='stylesheet' href='/static/css/main.css'>
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
<!-- Also link to some fonts hosted by Google -->
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
{{template "nav" .}}
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
<!-- And include the JavaScript file -->
<script src="/static/js/main.js" type="text/javascript"></script>
</body>
</html>
{{end}}
<script src="/static/js/main.js" type="text/javascript"></script> - загрузит статический файл с JS
<link rel='stylesheet' href='/static/css/main.css'> - загрузит CSS на страницу
Hundle что делает
Отступление с объяснением Hundle
В пакете net/http языка Go есть два способа зарегистрировать обработчик (handler) для определённого пути (path):
func (*ServeMux) Handle – это метод структуры ServeMux.
func Handle – это функция уровня пакета, которая использует дефолтный ServeMux.
Разница между ними:
| Критерий |
(*ServeMux).Handle |
http.Handle |
| Тип |
Метод (ServeMux.Handle) |
Функция (http.Handle) |
| Принадлежность |
Работает с конкретным ServeMux (роутером) |
Работает с дефолтным ServeMux |
| Где используется |
Когда нужно кастомное мультиплексирование |
Для простых случаев (использует DefaultServeMux) |
| Пример использования |
mux := http.NewServeMux(); mux.Handle("/", handler) |
http.Handle("/", handler) |
Примеры:
1. Использование (*ServeMux).Handle (кастомный ServeMux)
mux := http.NewServeMux() // создаём свой мультиплексор
mux.Handle("/path", myHandler) // регистрируем обработчик
http.ListenAndServe(":8080", mux) // запускаем сервер с этим мультиплексором
2. Использование http.Handle (дефолтный ServeMux)
http.Handle("/path", myHandler) // регистрируем обработчик в DefaultServeMux
http.ListenAndServe(":8080", nil) // nil означает использование DefaultServeMux
Важно:
- Если вы используете
http.Handle, то обработчик регистрируется в DefaultServeMux.
- Если вы хотите кастомный роутер (например, для middleware, изоляции путей и т. д.), нужно использовать
NewServeMux() и (*ServeMux).Handle.
- В продакшене часто избегают
DefaultServeMux из-за возможных конфликтов (например, если несколько библиотек тоже регистрируют пути в нём).
Вывод:
(*ServeMux).Handle – для кастомных роутеров.
http.Handle – для быстрого использования дефолтного роутера.
Hundler для чего нужен
Функция (*ServeMux).Handler в пакете net/http используется для поиска обработчика (http.Handler) и шаблона пути (pattern), который соответствует переданному HTTP-запросу (*http.Request).
📌 Сигнатура функции:
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
r *Request – входящий HTTP-запрос.
- Возвращает:
h Handler – обработчик, который должен обработать запрос.
pattern string – шаблон пути, который совпал с URL запроса (например, "/users/").
🔹 Когда используется?
- Когда нужно вручную определить обработчик для запроса (например, для кастомной логики маршрутизации).
- В middleware, чтобы проверить, какой обработчик будет вызван для запроса.
- Для логирования или отладки (чтобы узнать, какой
pattern соответствует запросу).
🔹 Примеры использования
1️⃣ Пример 1: Получение обработчика для запроса
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
})
// Создаём тестовый запрос
req, _ := http.NewRequest("GET", "http://example.com/hello", nil)
// Получаем обработчик и шаблон пути
handler, pattern := mux.Handler(req)
fmt.Println("Handler:", handler != nil) // true
fmt.Println("Pattern:", pattern) // "/hello"
}
Вывод:
Handler: true
Pattern: /hello
2️⃣ Пример 2: Использование в middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Получаем обработчик и шаблон до вызова next
handler, pattern := http.DefaultServeMux.Handler(r)
log.Printf("Request: %s %s → Handler: %T, Pattern: %s", r.Method, r.URL.Path, handler, pattern)
next.ServeHTTP(w, r)
})
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home"))
})
http.ListenAndServe(":8080", loggingMiddleware(http.DefaultServeMux))
}
При запросе GET / в логах появится:
Request: GET / → Handler: http.HandlerFunc, Pattern: /
🔹 Как работает внутри?
- Проверяет
Host (если задан в ServeMux).
- Ищет точное совпадение пути (например,
/users).
- Ищет самое длинное совпадение по префиксу (если путь зарегистрирован с
/users/, то запрос /users/123 тоже сработает).
- Если ничего не найдено – возвращает
NotFoundHandler (который отвечает с 404).
🔹 Отличие от http.Handler()
(*ServeMux).Handler – метод конкретного ServeMux.
http.Handler() – работает с DefaultServeMux.
Пример:
// С кастомным ServeMux
mux := http.NewServeMux()
handler, pattern := mux.Handler(req)
// С DefaultServeMux
handler, pattern := http.DefaultServeMux.Handler(req)
// Или просто:
handler, pattern := http.Handler(req) // работает с DefaultServeMux
🔹 Вывод
(*ServeMux).Handler полезен, когда нужно динамически определить обработчик для запроса.
- Используется в middleware, кастомных роутерах, логировании.
- Возвращает как сам обработчик, так и шаблон пути, что помогает в отладке.
ServeHTTP объяснение
Функция (*ServeMux).ServeHTTP — это основной метод обработки HTTP-запросов в ServeMux, который реализует интерфейс http.Handler. Она отвечает за:
- Поиск подходящего обработчика (на основе URL пути)
- Вызов этого обработчика (или возврат
404 Not Found)
🔍 Развёрнутое объяснение
📌 Сигнатура
func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request)
w ResponseWriter — интерфейс для записи HTTP-ответа.
r *Request — входящий HTTP-запрос.
🔧 Как работает? (Mermaid-схема)
flowchart TD
A[Запрос поступает в ServeMux.ServeHTTP] --> B{Есть Host в URL?}
B -->|Да| C[Ищем обработчик по host + path]
B -->|Нет| D[Ищем обработчик только по path]
C --> E{Найден обработчик?}
D --> E
E -->|Да| F[Вызываем handler.ServeHTTP(w, r)]
E -->|Нет| G[Возвращаем 404 Not Found]
Пошаговый алгоритм:
- Проверяет
Host (если в ServeMux есть пути с указанием хоста, например "example.com").
- Ищет точное совпадение пути (например,
/users).
- Ищет самое длинное совпадение по префиксу (если путь зарегистрирован как
/users/, то /users/123 тоже сработает).
- Если обработчик не найден — вызывает
NotFoundHandler (отправляет 404).
📂 Пример кода
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
// Регистрируем обработчики
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/about", aboutHandler)
// Запускаем сервер
http.ListenAndServe(":8080", mux)
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home Page"))
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("About Page"))
}
🛠 Что происходит при запросе?
-
Запрос GET /
ServeHTTP находит точное совпадение с / → вызывает homeHandler.
-
Запрос GET /about
- Совпадение с
/about → вызывает aboutHandler.
-
Запрос GET /unknown
- Совпадений нет → возвращает
404.
⚙️ Кастомизация ServeMux
Можно переопределить ServeHTTP для своей логики (например, для middleware):
type CustomMux struct {
*http.ServeMux
}
func (m *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Логирование перед вызовом обработчика
log.Println("Request:", r.URL.Path)
m.ServeMux.ServeHTTP(w, r) // Вызов оригинального ServeHTTP
}
func main() {
mux := &CustomMux{http.NewServeMux()}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Custom Mux!"))
})
http.ListenAndServe(":8080", mux)
}
🔥 Ключевые особенности
-
Иерархия путей:
/images/ совпадёт с /images/logo.png.
/images (без слеша) — только с точным совпадением.
-
Host-specific пути:
mux.Handle("example.com/", handler) // Сработает только для example.com
-
Дефолтный обработчик 404:
Можно заменить через mux.NotFound = customHandler.
🎯 Вывод
ServeHTTP — ядро роутинга в net/http.
- Автоматически обрабатывает префиксы, хосты и 404.
- Может быть переопределён для кастомной логики.
Что нужно сделать,чтобы получить Handle
- Создать структуру
type home struct {} - или любой объект
- Создать функцию обработчик с вызовом
ServeHTTP
func (h *home) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("This is my home page")) }
- Используем в коде
mux := http.NewServeMux()
mux.Handle("/", &home{})
ВСЁ!!!
А это синтетический сахар для обертывания обычных функций в Handle
mux := http.NewServeMux()
mux.HandleFunc("/", home) // home здесь обычная функция
http.ListenAndServe()
Это собственно сам сервер у которого тоже есть такой же метод ServeHTTP и который отправляет на него запросы по цепочке
Работаем с флагами командной строки
Пакет: flag
addr := flag.String("addr", ":4000", "HTTP network address") // прочитали адрес
flag.Parse()
...
log.Printf("Starting server on %s", *addr) //вывели на печать
err := http.ListenAndServe(*addr, mux) //слушаем порт, который получили из адресной строки
log.Fatal(err)
Флаги могут преобразовываться сразу в нужный тип:
- flag.String()
- flag.Int(),
- flag.Bool()
- flag.Float64().
help автоматически
для команды help автоматически сформируется все параметры вместе с подсказками
os.Getenv
Прочитает значение из переменной среды:
addr := os.Getenv("SNIPPETBOX_ADDR")
Структура флагов
type config struct {
addr string
staticDir string
}
создаем структуру для флагов и определяем переменные из структуры:
var cfg config
flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")
flag.Parse()
для этого используем функции:
- flag.StringVar(),
- flag.IntVar(),
- flag.BoolVar()
Логирование
- Стандартный лог выводит в терминал
log.Printf("Starting server on %s", *addr) // Information message
err := http.ListenAndServe(*addr, mux)
log.Fatal(err) // Error message
Продвинутый лог
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) // создали макет лога для сообщений
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) //макет лога для ошибок
... //вывод лога
infoLog.Printf("Starting server on %s", *addr)
err := http.ListenAndServe(*addr, mux)
errorLog.Fatal(err)
$ go run ./cmd/web
INFO 2022/01/29 16:00:50 Starting server on :4000
ERROR 2022/01/29 16:00:50 main.go:37: listen tcp :4000: bind: address already in use
exit status 1
log.Llongfile, log.Lshortfile — эти флаги определяют полноту выводимых данных
перенаправим вывод журнала в файлы
$ go run ./cmd/web >>/tmp/info.log 2>>/tmp/error.log
Логирование работы сервера
- Создать структуру для параметров сервера HTTP
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
srv := &http.Server{
Addr: *addr,
ErrorLog: errorLog,
Handler: mux,
}
// включаем сервер с обработчиком ошибок
err := srv.ListenAndServe()
Логирование в файл
f, err := os.OpenFile("/tmp/info.log", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatal(err)
}
defer f.Close()
infoLog := log.New(f, "INFO\t", log.Ldate|log.Ltime)
Логирование во всех зависимостях приложения
Создать структуру приложения с его переменными
type application struct {
errorLog *log.Logger
infoLog *log.Logger
}
в main.go задать значения:
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
app := &application{
errorLog: errorLog,
infoLog: infoLog,
}
и использовать во всех зависимостях
так в обработчиках добавляем зависимость от структуры:
func (app *application) home(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
Вариант использования пакета config
func main() {
app := &config.Application{
ErrorLog: log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
}
mux.Handle("/", examplePackage.ExampleHandler(app))
}
func ExampleHandler(app *config.Application) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
...
ts, err := template.ParseFiles(files...)
if err != nil {
app.ErrorLog.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
return
}
...
}
}
пакет позволяет применять конфигурацию для всех зависимостей
Динамические данные в шаблоне
Объявление внутреннего модуля в проекте
в проекте может быть несколько внутренних и не только внутренних модулей. В нашем случае внутренний модуль лежит в папке /internal/models. Учитывая, что основной проект называется: snippetbox.alexedwards.net, то полный путь к модулю будет: "snippetbox.alexedwards.net/internal/models"
package main
import (
"errors"
"fmt"
"html/template" // Uncomment import
"net/http"
"strconv"
"snippetbox.alexedwards.net/internal/models"
)
Для работы динамических шаблонов нужно:
- Создать шаблоны
- Выстроить иерархию внутри них (каждый блок и раздел лучше делать в отдельном шаблоне и соединять в один, например
base)
- Создать массив строк с наименованием шаблонов и путей к ним
files := []string{
"./ui/html/base.tmpl",
"./ui/html/partials/nav.tmpl",
"./ui/html/pages/view.tmpl",
}
- Пропарсить шаблоны
ts, err := template.ParseFiles(files...)
if err != nil {
app.serverError(w, err)
return
}
- Собрать html из шаблонов
err = ts.ExecuteTemplate(w, "base", snippet) //собираем в шаблоне base и передаем в него структуру данных snippet
if err != nil {
app.serverError(w, err)
}
Базовые понятия работы с шаблонами html/template
Для работы с сущностями создадим структуру в дополнительном модуле.
type Snippet struct {
ID int
Title string
Content string
Created time.Time
Expires time.Time
}
Эта структура будет работать с базой данных и шаблонами
. — весь контекст переданный в этот блок
- Передать структуру в шаблон:
err = ts.ExecuteTemplate(w, "base", snippet)
- Использовать поля структуры в шаблоне
{{.Title}} и т.д.
{{define "title"}}Snippet #{{.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
<div class='metadata'>
<strong>{{.Title}}</strong>
<span>#{{.ID}}</span>
</div>
<pre><code>{{.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Created}}</time>
<time>Expires: {{.Expires}}</time>
</div>
</div>
{{end}}
Особенности пакета template
В структуру шаблонов можно передавать только одну переменную. Поэтому для передачи нескольких переменных их нужно собрать всех в одну структуру.
- Создать структуру
type templateData struct {
Snippet *models.Snippet
}
- Выполнить все действия, описанные выше по парсингу шаблонов
- Создать экземпляр структуры данных
data := &templateData{
Snippet: snippet,
}
- Передать данные в генератор html кода
err = ts.ExecuteTemplate(w, "base", data)
if err != nil {
app.serverError(w, err)
}
- В шаблоне значения полей тогда будут вызываться с указанием полного пути поля:
{{.Snippet.ID}} или {{.Snippet.Title}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
<div class='metadata'>
<strong>{{.Snippet.Title}}</strong>
<span>#{{.Snippet.ID}}</span>
</div>
<pre><code>{{.Snippet.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Snippet.Created}}</time>
<time>Expires: {{.Snippet.Expires}}</time>
</div>
</div>
{{end}}
Вложенные структуры в шаблонах
Объявили шаблон
{{define "base"}}
...
{{end}}
Использование шаблона
В основном шаблоне можно указывать другие шаблоны, которые определены в других файлах. В других файлах может быть определено несколько шаблонов в каждом файле.
{{define "title"}}Home{{end}}
{{define "main"}}
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
{{end}}
Вставка встроенного шаблона в точку основного шаблона
{{template "title" .}}
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
</body>
</html>
{{end}}
{{block}}…{{end}}
{{define "base"}}
<h1>An example template</h1>
{{block "sidebar" .}}
<p>My default sidebar content</p>
{{end}}
{{end}}
этот параметр можем использовать для передачи значений по умолчанию. Если нет в этом блоке данных, то он и не выведется, в отличии от {{template}}, который будет требовать обязательного вывода контента.
Вызов методов к полям
{{.Snippet.Created.Weekday}} к нашему полю .Snippet.Created вызовем метод Weekday
или {{.Snippet.Created.AddDate 0 6 0}}
{{if .Foo}} C1 {{else}} C2 {{end}}
{{with .Foo}} C1 {{else}} C2 {{end}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
{{with .Snippet}}
<div class='snippet'>
<div class='metadata'>
<strong>{{.Title}}</strong>
<span>#{{.ID}}</span>
</div>
<pre><code>{{.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Created}}</time>
<time>Expires: {{.Expires}}</time>
</div>
</div>
{{end}}
{{end}}
собственно метод with позволяет далее в этом блоке работать с полями указанной структуры без указания ее полного пути
{{range .Foo}} C1 {{else}} C2 {{end}}
range пройдется по всем элементам .Foo как цикл и выполнит все действия как делает with с одним элементом
Операторы сравнения и логики
{{eq .Foo .Bar}} {{ne .Foo .Bar}} {{not .Foo}} {{or .Foo .Bar}}
{{index .Foo i}}
возвращает текущий индекс массива
{{printf “%s-%s” .Foo .Bar}}
форматированная печать значений
{{len .Foo}}
{{$bar := len .Foo}} — в этом варианте присвоит переменной длину .Foo
{{break}} {{continue}}
используется в if для разветвленной логики
Кэширование
Объяснение создания кэша
Подробный разбор map[string]*template.Template
1. Базовый анализ структуры
map[string]*template.Template
Это декларация типа в Go, представляющая:
- map - ассоциативный массив/словарь
- string - тип ключа (строка)
- *template.Template - тип значения (указатель на объект Template)
2. Компоненты:
- template.Template - тип из пакета
html/template или text/template, представляющий:
- Разобранный шаблон
- Методы для выполнения шаблонов
- Ассоциированные именованные шаблоны
- Указатель (*) - используется для:
- Работы с одним экземпляром шаблона
- Избегания копирования больших структур
- Модификации шаблона после создания
3. Типичное использование:
templates := make(map[string]*template.Template)
// Добавление шаблона
templates["home"] = template.Must(template.ParseFiles("home.html"))
templates["about"] = template.Must(template.ParseFiles("about.html"))
// Использование
err := templates["home"].Execute(w, data)
4. Преимущества такого подхода:
-
Организация шаблонов:
- Группировка по функциональности (страницы, компоненты)
- Быстрый доступ по имени
-
Производительность:
- Парсинг шаблонов один раз при инициализации
- Кэширование готовых шаблонов
-
Гибкость:
- Динамический выбор шаблона
- Возможность горячей перезагрузки
5. Пример расширенного использования:
// Инициализация кэша шаблонов
var templateCache = map[string]*template.Template{
"home": parseTemplate("home.html", "layout.html"),
"about": parseTemplate("about.html", "layout.html"),
}
func parseTemplate(files ...string) *template.Template {
ts, err := template.ParseFiles(files...)
if err != nil {
log.Fatal(err)
}
return ts
}
// Обработчик HTTP
func homeHandler(w http.ResponseWriter, r *http.Request) {
data := struct{ Title string }{Title: "Главная"}
err := templateCache["home"].ExecuteTemplate(w, "layout", data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
6. Особенности работы:
- Потокобезопасность: Шаблоны безопасны для конкурентного использования после создания
- Жизненный цикл: Обычно создаются при старте приложения
- Ошибки: Проверка должна быть при:
- Создании мапы
- Парсинге шаблонов
- Выполнении шаблонов
7. Альтернативные подходы:
-
sync.Map - для конкурентного доступа
var templates sync.Map
templates.Store("home", template.Must(template.ParseFiles("home.html")))
-
Вложенные мапы - для сложных структур:
map[string]map[string]*template.Template
Такой подход широко используется в веб-фреймворках и приложениях для эффективного управления шаблонами.
создали функцию, которая будет кэшировать шаблоны в памяти map
package main
import (
"html/template" // New import
"path/filepath" // New import
"snippetbox.alexedwards.net/internal/models"
)
...
func newTemplateCache() (map[string]*template.Template, error) {
// Initialize a new map to act as the cache.
cache := map[string]*template.Template{}
// Use the filepath.Glob() function to get a slice of all filepaths that
// match the pattern "./ui/html/pages/*.tmpl". This will essentially gives
// us a slice of all the filepaths for our application 'page' templates
// like: [ui/html/pages/home.tmpl ui/html/pages/view.tmpl]
pages, err := filepath.Glob("./ui/html/pages/*.tmpl")
if err != nil {
return nil, err
}
// Loop through the page filepaths one-by-one.
for _, page := range pages {
// Extract the file name (like 'home.tmpl') from the full filepath
// and assign it to the name variable.
name := filepath.Base(page)
// Create a slice containing the filepaths for our base template, any
// partials and the page.
files := []string{
"./ui/html/base.tmpl",
"./ui/html/partials/nav.tmpl",
page,
}
// Parse the files into a template set.
ts, err := template.ParseFiles(files...)
if err != nil {
return nil, err
}
// Add the template set to the map, using the name of the page
// (like 'home.tmpl') as the key.
cache[name] = ts
}
// Return the map.
return cache, nil
}
в структуру приложения добавим поле
type application struct {
errorLog *log.Logger
infoLog *log.Logger
snippets *models.SnippetModel
templateCache map[string]*template.Template //это кэш
}
инициализация кэш
// Initialize a new template cache...
templateCache, err := newTemplateCache() //вызываем функцию и создаем кэш
if err != nil {
errorLog.Fatal(err)
}
создаем экземпляр приложения app с кэшем
app := &application{
errorLog: errorLog,
infoLog: infoLog,
snippets: &models.SnippetModel{DB: db},
templateCache: templateCache,
}
выдача кэшированных страниц
создадим функцию render
package main
...
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
// Retrieve the appropriate template set from the cache based on the page
// name (like 'home.tmpl'). If no entry exists in the cache with the
// provided name, then create a new error and call the serverError() helper
// method that we made earlier and return.
ts, ok := app.templateCache[page]
if !ok {
err := fmt.Errorf("the template %s does not exist", page)
app.serverError(w, err)
return
}
// Write out the provided HTTP status code ('200 OK', '400 Bad Request'
// etc).
w.WriteHeader(status)
// Execute the template set and write the response body. Again, if there
// is any error we call the the serverError() helper.
err := ts.ExecuteTemplate(w, "base", data)
if err != nil {
app.serverError(w, err)
}
}
в эту функцию передаем название страницы и получаем готовый html код, только теперь уже из кэша
пример функции обращения к кэшу
func (app *application) home(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
app.notFound(w)
return
}
snippets, err := app.snippets.Latest()
if err != nil {
app.serverError(w, err)
return
}
// Use the new render helper.
app.render(w, http.StatusOK, "home.tmpl", &templateData{
Snippets: snippets,
})
}
полный код создания кэша
package main
...
func newTemplateCache() (map[string]*template.Template, error) {
cache := map[string]*template.Template{}
pages, err := filepath.Glob("./ui/html/pages/*.tmpl")
if err != nil {
return nil, err
}
for _, page := range pages {
name := filepath.Base(page)
// Parse the base template file into a template set.
ts, err := template.ParseFiles("./ui/html/base.tmpl")
if err != nil {
return nil, err
}
// Call ParseGlob() *on this template set* to add any partials.
ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl")
if err != nil {
return nil, err
}
// Call ParseFiles() *on this template set* to add the page template.
ts, err = ts.ParseFiles(page)
if err != nil {
return nil, err
}
// Add the template set to the map as normal...
cache[name] = ts
}
return cache, nil
}
Объяснение парсинга шаблонов в кэш
Развернутое объяснение функции newTemplateCache()
Эта функция создает кэш шаблонов для веб-приложения, загружая и компилируя все шаблоны страниц вместе с базовым шаблоном и частичными шаблонами (partials). Рассмотрим ее построчно:
1. Объявление функции
func newTemplateCache() (map[string]*template.Template, error)
- Назначение: Создает и возвращает кэш шаблонов
- Возвращаемые значения:
map[string]*template.Template - кэш шаблонов, где ключ - имя файла шаблона
error - ошибка, если что-то пошло не так
2. Инициализация кэша
cache := map[string]*template.Template{}
- Создается пустая мапа для хранения шаблонов
3. Поиск файлов страниц
pages, err := filepath.Glob("./ui/html/pages/*.tmpl")
- Использует
filepath.Glob для поиска всех файлов с расширением .tmpl в директории ./ui/html/pages/
- Возвращает список путей к файлам или ошибку
4. Обработка ошибок поиска
if err != nil {
return nil, err
}
- Если при поиске файлов произошла ошибка, возвращает ее
5. Цикл по найденным страницам
for _, page := range pages {
- Перебирает все найденные файлы шаблонов страниц
6. Получение имени файла
name := filepath.Base(page)
- Извлекает базовое имя файла (без пути) с помощью
filepath.Base()
- Например, для “./ui/html/pages/home.tmpl” вернет “home.tmpl”
7. Парсинг базового шаблона
ts, err := template.ParseFiles("./ui/html/base.tmpl")
- Создает новый набор шаблонов (
*template.Template), загружая базовый шаблон
- Базовый шаблон обычно содержит общую структуру HTML (doctype, head, общие скрипты/стили)
8. Обработка ошибок парсинга
if err != nil {
return nil, err
}
- Если не удалось загрузить базовый шаблон, возвращает ошибку
9. Добавление частичных шаблонов
ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl")
- Добавляет в набор шаблонов все файлы из директории partials
- Частичные шаблоны - это повторно используемые компоненты (header, footer и т.д.)
10. Обработка ошибок добавления partials
if err != nil {
return nil, err
}
- Возвращает ошибку, если не удалось загрузить partials
11. Добавление шаблона страницы
ts, err = ts.ParseFiles(page)
- Загружает конкретный шаблон страницы в тот же набор шаблонов
- Это позволяет странице использовать базовый шаблон и partials
12. Обработка ошибок добавления страницы
if err != nil {
return nil, err
}
- Возвращает ошибку, если не удалось загрузить шаблон страницы
13. Сохранение в кэш
- Сохраняет готовый набор шаблонов в кэш под именем файла страницы
14. Возврат результата
- После обработки всех страниц возвращает заполненный кэш и nil в качестве ошибки
Особенности работы:
-
Иерархия шаблонов:
- Базовый шаблон (base.tmpl)
- Частичные шаблоны (partials/*.tmpl)
- Шаблоны страниц (pages/*.tmpl)
-
Преимущества:
- Все шаблоны парсятся один раз при старте приложения
- Готовые к использованию шаблоны хранятся в памяти
- Быстрый доступ к шаблонам по имени
-
Использование в обработчиках:
// Где-то в обработчике:
err := cache["home.tmpl"].ExecuteTemplate(w, "base", data)
-
Расширения:
- Можно добавить горячую перезагрузку в dev-режиме
- Можно кэшировать не только по имени файла, но и по логическому имени страницы
Эта реализация обеспечивает удобное управление шаблонами в веб-приложении, следуя принципам DRY (Don’t Repeat Yourself) и обеспечивая хорошую производительность.
Как избежать ошибок при рендеринге динамического кода
- Делать рендер кода
- Записывать результат в буфер
- Проверять на отсутствие ошибок
- делать выдачу в
http.ResponseWriter.
Обновленный код обработчика с промежуточной записью результата в буфер
package main
import (
"bytes" // New import
"fmt"
"net/http"
"runtime/debug"
)
...
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
ts, ok := app.templateCache[page]
if !ok {
err := fmt.Errorf("the template %s does not exist", page)
app.serverError(w, err)
return
}
// инициализация буфера
buf := new(bytes.Buffer)
// записываем результат рендеринга в буфер
err := ts.ExecuteTemplate(buf, "base", data)
if err != nil {
app.serverError(w, err)
return
}
// запишем статус в заголовок ответа
w.WriteHeader(status)
// возвращаем результат успешного рендеринга
buf.WriteTo(w)
}
Динамические данные в шаблонах
- Создадим специальную структуру для передачи динамических данных в шаблон
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
}
- Создадим функцию helpers для возврата указателя на структуру с данными и расчетом динамических данных
func (app *application) newTemplateData(r *http.Request) *templateData {//получаем на вход указатель с данными запроса
return &templateData{ //и возвращаем указатель с данными ответа
CurrentYear: time.Now().Year(),// по дороге посчитаем какой текущий год
}
}
- Делаем вызов функции
render и передаем ей на вход структуру с данными templateData
//загружаем из базы данные в переменную snippets
snippets, err := app.snippets.Latest()
if err != nil {
app.serverError(w, err)
return
}
data := app.newTemplateData(r) //создаем переменную типа templateData
data.Snippets = snippets //записываем в структуру переменной значения из базы данных
//вызываем функцию render и передаем ей структуру с изменяемыми данными
app.render(w, http.StatusOK, "home.tmpl", data)
- Повторяем такие вызовы для всех обработчиков
- В шаблонах обрабатываем данные из структуры
templateData как {{.Snippets}} или {{.Snippet}} и текущий год как {{.CurrentYear}}
Пользовательские функции в шаблонах
- Создаем функцию, которую планируем добавить в шаблон
func humanDate(t time.Time) string {
return t.Format("02 Jan 2006 at 15:04")
}
- Создаем объект
template.FuncMap и в нем объявляем свою функцию (например humanDate()). Фактически в переменной function создается карта: ключ - значение, где значение, это название функции, которое мы передаем в шаблон.
var functions = template.FuncMap{
"humanDate": humanDate,
}
- Метод должен быть зарегистрирован до парсинга шаблона
for _, page := range pages {
name := filepath.Base(page)
//перед парсером файла с шаблоном, передаем переменную functions с объявленными функциями
//функций может быть несколько
ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.tmpl")
if err != nil {
return nil, err
}
ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl")
if err != nil {
return nil, err
}
ts, err = ts.ParseFiles(page)
if err != nil {
return nil, err
}
cache[name] = ts
}
- Используем функцию в шаблоне как и другие функции
<td>{{humanDate .Created}}</td>
или
<time>Created: {{.Created | humanDate}}</time>
или
<time>{{.Created | humanDate | printf "Created: %s"}}</time>
Объяснение порядка добавления функций в шаблон
Подробный разбор команды ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.tmpl")
Эта строка кода создает и настраивает новый шаблон в Go. Разберем ее по цепочке вызовов:
1. template.New(name)
- Назначение: Создает новый пустой шаблон
- Параметр:
name (string) - уникальное имя шаблона
- Возвращает: Указатель на
*template.Template
- Особенности:
- Имя используется для вызова шаблона через
{{template "name"}}
- Если шаблон с таким именем уже существует, он будет заменен
2. .Funcs(functions)
- Назначение: Регистрирует пользовательские функции для использования в шаблоне
- Параметр:
functions (тип template.FuncMap) - мапа функций вида map[string]interface{}
- Пример:
functions := template.FuncMap{
"uppercase": strings.ToUpper,
"add": func(a, b int) int { return a + b },
}
- Важно:
- Должен вызываться до парсинга шаблона
- Функции должны возвращать 1 или 2 значения (второе - error)
3. .ParseFiles("./ui/html/base.tmpl")
- Назначение: Парсит указанные файлы как тело шаблона
- Параметр:
- Путь к файлу шаблона (может принимать несколько файлов)
- Особенности:
- Первый файл становится “ассоциированным” с этим шаблоном
- Может содержать определения других шаблонов через
{{define}}
Возвращаемые значения:
ts (*template.Template) - созданный шаблон
err (error) - ошибка, если что-то пошло не так
Полный пример использования:
funcs := template.FuncMap{
"formatDate": func(t time.Time) string { return t.Format("2006-01-02") },
}
ts, err := template.New("base").Funcs(funcs).ParseFiles("./ui/html/base.tmpl")
if err != nil {
log.Fatal(err)
}
// Использование в обработчике
err = ts.Execute(w, data)
Что происходит под капотом:
- Создается новый именованный шаблон
- Регистрируются пользовательские функции
- Читается и парсится файл шаблона
- Все определения шаблонов из файла добавляются в набор
Особенности безопасности при использовании html/template:
- Автоматическое экранирование в зависимости от контекста
- Защита от XSS-атак
- Контекстно-зависимое экранирование для HTML, JS, CSS
Типичные ошибки:
- Регистрация функций после парсинга
- Использование несуществующих путей к файлам
- Попытка использовать недопустимые имена функций (например, с точками)
Эта цепочка вызовов является идиоматическим способом создания шаблонов в Go и часто используется при инициализации веб-приложений.
Middleware
Стандартный паттерн middlware
func myMiddleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// TODO: Execute our middleware logic here...
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
или использование анонимной функции
func myMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: Execute our middleware logic here...
next.ServeHTTP(w, r)
})
}
Объяснение middleware
Middleware в Go: кратко и понятно
Middleware — это промежуточный слой между HTTP-запросом и вашим обработчиком. Он позволяет выполнять код до или после основного обработчика (например, логирование, проверку авторизации, сжатие данных).
1. Глобальный Middleware (для всех запросов)
Если middleware добавлен до servemux (роутера), то он будет выполняться для всех запросов.
Схема:
Запрос → Middleware → Servemux → Обработчик
Пример:
func logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Запрос: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r) // Передаем запрос дальше
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
// Middleware применяется ко ВСЕМ запросам
app := logRequest(mux)
http.ListenAndServe(":8080", app)
}
Когда использовать:
✅ Логирование
✅ Сжатие ответов (gzip)
✅ Добавление заголовков CORS
2. Локальный Middleware (для конкретных маршрутов)
Если middleware добавлен после servemux (оборачивает конкретный обработчик), то он работает только для определенных маршрутов.
Схема:
Запрос → Servemux → Middleware → Обработчик
Пример:
func checkAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isAuthenticated(r) {
http.Error(w, "Доступ запрещен", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r) // Пропускаем авторизованных
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("/admin", checkAuth(adminHandler)) // Только для /admin
mux.HandleFunc("/", homeHandler) // Без middleware
http.ListenAndServe(":8080", mux)
}
Когда использовать:
✅ Проверка авторизации
✅ Валидация данных
✅ Лимитирование запросов (rate limiting)
Вывод:
- До
servemux → Middleware на все запросы.
- После
servemux → Middleware только для выбранных маршрутов.
Это дает гибкость в управлении логикой приложения без дублирования кода. 🚀
Создадим обработчик заголовков для всех страниц
Будем возвращать шаблоны html и к каждому будем записывать правильный заголовок в ответе
Content-Security-Policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com
Referrer-Policy: origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-XSS-Protection: 0
Объяснение HEADERS
Краткое описание HTTP-заголовков безопасности
Эти заголовки защищают веб-приложение от распространённых атак (XSS, clickjacking, MIME-sniffing и др.).
1. Content-Security-Policy (CSP)
Назначение: Ограничивает источники загрузки ресурсов (JS, CSS, шрифты, изображения).
Пример:
Content-Security-Policy: default-src 'self'; font-src fonts.gstatic.com; style-src fonts.googleapis.com 'self'
Эффект:
- Блокирует inline-JS (
unsafe-inline)
- Разрешает шрифты только с
fonts.gstatic.com
- Стили — с
fonts.googleapis.com и своего домена
Другие варианты:
script-src 'self' https://trusted.cdn.com — разрешает скрипты только с указанных источников
img-src * — разрешает изображения с любых доменов (небезопасно!)
2. Referrer-Policy
Назначение: Контролирует, какие данные отправляются в заголовке Referer при переходе на другой сайт.
Пример:
Referrer-Policy: origin-when-cross-origin
Эффект:
- При переходе внутри сайта (
same-origin) передаётся полный URL.
- При переходе на другой домен — только источник (без пути и параметров).
Другие варианты:
no-referrer — вообще не отправлять Referer
strict-origin — всегда отправлять только домен (без протокола и пути)
3. X-Content-Type-Options: nosniff
Назначение: Запрещает браузеру “угадывать” тип контента (MIME-sniffing).
Пример:
X-Content-Type-Options: nosniff
Эффект:
- Браузер строго следует указанному
Content-Type.
- Защищает от атак, когда вредоносный файл маскируется под изображение/JS.
4. X-Frame-Options: deny
Назначение: Блокирует встраивание страницы в <iframe> (защита от clickjacking).
Пример:
Альтернативы:
sameorigin — разрешает iframe только с текущего домена
- Устарел в пользу CSP с
frame-ancestors 'none'
5. X-XSS-Protection: 0
Назначение: Отключает встроенный XSS-фильтр браузера (устаревший механизм).
Почему 0?
- Современные браузеры используют CSP для защиты от XSS.
- Старые версии
X-XSS-Protection иногда сами вызывали уязвимости.
Другие важные заголовки безопасности
-
Strict-Transport-Security (HSTS)
- Принудительное использование HTTPS:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
-
Permissions-Policy
- Отключает нежелательные API (камера, геолокация и др.):
Permissions-Policy: geolocation=(), microphone=()
-
Cross-Origin-Opener-Policy (COOP)
- Защищает от атак типа Spectre:
Cross-Origin-Opener-Policy: same-origin
-
Cross-Origin-Embedder-Policy (COEP)
- Блокирует загрузку кросс-доменных ресурсов без CORS:
Cross-Origin-Embedder-Policy: require-corp
Итог
| Заголовок |
Защита от |
Современная альтернатива |
CSP |
XSS, data injection |
— |
Referrer-Policy |
Утечка URL |
— |
X-Content-Type-Options |
MIME-sniffing |
— |
X-Frame-Options |
Clickjacking |
CSP: frame-ancestors 'none' |
X-XSS-Protection |
Устаревший XSS-фильтр |
CSP |
Оптимальный набор:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; frame-ancestors 'none'
Strict-Transport-Security: max-age=63072000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
- Создадим функцию для создания заголовков
package main
import (
"net/http"
)
func secureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Note: This is split across multiple lines for readability. You don't
// need to do this in your own code.
w.Header().Set("Content-Security-Policy",
"default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com")
w.Header().Set("Referrer-Policy", "origin-when-cross-origin")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protection", "0")
next.ServeHTTP(w, r)
})
}
- Создадим функцию обработчик маршрутов
package main
import "net/http"
// Update the signature for the routes() method so that it returns a
// http.Handler instead of *http.ServeMux.
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
fileServer := http.FileServer(http.Dir("./ui/static/"))
mux.Handle("/static/", http.StripPrefix("/static", fileServer))
mux.HandleFunc("/", app.home)
mux.HandleFunc("/snippet/view", app.snippetView)
mux.HandleFunc("/snippet/create", app.snippetCreate)
// Pass the servemux as the 'next' parameter to the secureHeaders middleware.
// Because secureHeaders is just a function, and the function returns a
// http.Handler we don't need to do anything else.
return secureHeaders(mux)
}
- После обработки маршрута для каждой страницы будут создан заголовок
Функция логирования запросов
func (app *application) logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.infoLog.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI())
next.ServeHTTP(w, r)
})
}
и для нашего случая выстроим цепочку middleware запросов
return app.logRequest(secureHeaders(mux))
полный код routes.go
package main
import "net/http"
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
fileServer := http.FileServer(http.Dir("./ui/static/"))
mux.Handle("/static/", http.StripPrefix("/static", fileServer))
mux.HandleFunc("/", app.home)
mux.HandleFunc("/snippet/view", app.snippetView)
mux.HandleFunc("/snippet/create", app.snippetCreate)
// Wrap the existing chain with the logRequest middleware.
return app.logRequest(secureHeaders(mux))
}
Обработка паники в HTTP сервере
func (app *application) recoverPanic(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create a deferred function (which will always be run in the event
// of a panic as Go unwinds the stack).
defer func() {
// Use the builtin recover function to check if there has been a
// panic or not. If there has...
if err := recover(); err != nil {
// Set a "Connection: close" header on the response.
w.Header().Set("Connection", "close")
// Call the app.serverError helper method to return a 500
// Internal Server response.
app.serverError(w, fmt.Errorf("%s", err))
}
}()
next.ServeHTTP(w, r)
})
}
Разбор HTTP-заголовка Connection: Close и работы recover() в Go
1. Заголовок Connection: Close
Назначение:
- Принудительно закрывает HTTP-соединение после отправки ответа.
- Сообщает клиенту, что сервер разорвёт соединение.
Особенности:
- В HTTP/1.x заголовок
Connection: Close корректен и заставляет сервер закрыть соединение.
- В HTTP/2 Go автоматически удаляет этот заголовок (чтобы не нарушать спецификацию) и вместо этого отправляет фрейм
GOAWAY для graceful-закрытия.
Пример использования:
w.Header().Set("Connection", "Close") // Закрыть соединение после ответа
Когда применять:
- Для серверов с высокой нагрузкой, чтобы уменьшить число висящих соединений.
- В тестовых средах для принудительного сброса соединений.
2. Функция recover() и обработка паник
Как работает recover():
- Возвращает значение, переданное в
panic() (тип any).
- Это значение может быть:
string, error, или любым другим типом.
justinas/alice
Пример:
func handlePanic() {
if r := recover(); r != nil {
// Нормализация в error
err := fmt.Errorf("panic: %v", r)
app.serverError(err) // Обработка ошибки
}
}
Что происходит:
- Если вызвана
panic("oops! something went wrong"), recover() вернёт строку "oops! something went wrong".
fmt.Errorf() преобразует это значение в error.
- Ошибка передаётся в метод
app.serverError() для логирования/отправки клиенту.
Зачем это нужно:
- Чтобы избежать аварийного завершения программы при панике.
- Логировать ошибки и отправлять клиенту понятный ответ (например,
500 Internal Server Error).
Важно:
recover() работает только внутри defer.
- Всегда проверяйте, что
recover() вернул не nil.
Заключительный вариант вызова middleware
return app.recoverPanic(app.logRequest(secureHeaders(mux)))
Делается обработка для всех запускаемых горутин.
Автор рекомендует пакет justinas/alice
Для удобного и читабельного управления middleware можно использовать пакет justinas/alice
Особенности:
- вместо
return myMiddleware1(myMiddleware2(myMiddleware3(myHandler))) будем писать return alice.New(myMiddleware1, myMiddleware2, myMiddleware3).Then(myHandler)
- есть методы Append и Then
myChain := alice.New(myMiddlewareOne, myMiddlewareTwo)
myOtherChain := myChain.Append(myMiddleware3)
return myOtherChain.Then(myHandler)
для нашего примера будет выглядеть:
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
// Return the 'standard' middleware chain followed by the servemux.
return standard.Then(mux)
Расширенные обработчики маршрутов
Автор предлагает вместо стандартной библиотеки Go использовать сторонних разработчиков:
- julienschmidt/httprouter,
- go-chi/chi,
- gorilla/mux.
сделав анализ современного развития этих пакетов можно прийти к выводу:
Сравнение пакетов
Вот сравнительный анализ трёх популярных HTTP-роутеров для Go (julienschmidt/httprouter, go-chi/chi, gorilla/mux) в сравнении со стандартным net/http, а также рекомендации по их применению:
1. julienschmidt/httprouter
Отличия от net/http:
✅ Высокая производительность – один из самых быстрых роутеров благодаря оптимизированному алгоритму сопоставления путей.
✅ Поддержка параметров в пути (например, /user/:id) с синтаксисом :param и *catch-all.
✅ Четкое разделение методов HTTP (GET, POST и т. д.), что предотвращает коллизии.
❌ Нет встроенного middleware-стека (но можно подключить вручную).
❌ Менее гибкий, чем chi и gorilla/mux, из-за строгого подхода к роутингу.
Рекомендации:
✔ Используйте, если нужна максимальная скорость (например, в high-load API).
✔ Хорош для RESTful-сервисов с четкой структурой эндпоинтов.
✔ Не подходит, если нужна сложная логика маршрутизации или встроенная поддержка middleware.
2. go-chi/chi
Отличия от net/http:
✅ Полная совместимость с net/http – можно использовать стандартные http.Handler и http.HandlerFunc.
✅ Гибкость и простота – удобный синтаксис для группировки роутов и middleware.
✅ Встроенная поддержка middleware (например, CORS, логирование, аутентификация).
✅ Поддержка параметров в URL (/posts/{id}) и regex-ограничений.
❌ Чуть медленнее, чем httprouter, но разница незначительна в большинстве сценариев.
Рекомендации:
✔ Идеален для REST API и веб-приложений с middleware (аутентификация, логирование и т. д.).
✔ Подходит, если важна простота и совместимость со стандартной библиотекой.
✔ Лучший выбор для проектов, где баланс между скоростью и функциональностью критичен.
3. gorilla/mux
Отличия от net/http:
✅ Богатые возможности маршрутизации – поддержка regex, условий на методы HTTP, query-параметров.
✅ Гибкость – можно задавать сложные правила для роутов (например, r.Host("example.com").Methods("GET")).
✅ Поддержка middleware (через gorilla/handlers или совместимость с http.Handler).
❌ Медленнее, чем httprouter и chi.
❌ Сложнее в настройке из-за обилия возможностей.
❌ Проект в архиве (maintenance mode), но стабилен и используется в многих проектах.
Рекомендации:
✔ Хорош для legacy-проектов или сложных маршрутов с regex и условиями.
✔ Подходит, если нужен максимальный контроль над роутингом (например, поддомены, строгие условия).
✔ Лучше избегать в новых проектах (из-за статуса “архивный”), но старые проекты могут продолжать его использовать.
Сравнение со стандартным net/http
| Роутер |
Скорость |
Гибкость |
Middleware |
Простота |
Поддержка |
net/http |
⚡️ Быстрый |
❌ Низкая |
❌ Нет |
✅ Простой |
✅ Активен |
httprouter |
⚡️⚡️ Очень быстрый |
❌ Жёсткий |
❌ Требует костылей |
✅ Простой |
✅ Активен |
chi |
⚡️ Быстрый |
✅ Высокая |
✅ Встроенный |
✅ Простой |
✅ Активен |
gorilla/mux |
🐢 Умеренный |
✅ Очень высокая |
✅ Через handlers |
❌ Сложный |
❌ Архивный |
Итоговые рекомендации:
- Для высоконагруженных API →
httprouter.
- Для баланса скорости и функциональности →
chi.
- Для сложных условий маршрутизации →
gorilla/mux (но лучше мигрировать на chi).
- Для минимализма и обучения → стандартный
net/http.
Если нужна максимальная скорость – httprouter.
Если нужен удобный и современный роутер – chi.
Если проект уже использует gorilla/mux – можно оставить, но для новых проектов лучше chi.
Чистые URL и маршрутизация на основе методов
Установка пакета julienschmidt/httprouter,
go get github.com/julienschmidt/httprouter@v1
Синтаксис пакета:
package main
import (
"net/http"
"github.com/julienschmidt/httprouter" // New import
"github.com/justinas/alice"
)
func (app *application) routes() http.Handler {
// Initialize the router.
router := httprouter.New()
// Update the pattern for the route for the static files.
fileServer := http.FileServer(http.Dir("./ui/static/"))
router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
// And then create the routes using the appropriate methods, patterns and
// handlers.
router.HandlerFunc(http.MethodGet, "/", app.home)
router.HandlerFunc(http.MethodGet, "/snippet/view/:id", app.snippetView)
router.HandlerFunc(http.MethodGet, "/snippet/create", app.snippetCreate)
router.HandlerFunc(http.MethodPost, "/snippet/create", app.snippetCreatePost)
// Create the middleware chain as normal.
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
// Wrap the router with the middleware and return it as normal.
return standard.Then(router)
}
Особенности передачи параметров в пакете httprouter
//данные передаются в контексте типа Params
func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) //получаем данные запроса
id, err := strconv.Atoi(params.ByName("id")) //считываем по ключу id значение переданного id
if err != nil || id < 1 {
app.notFound(w)
return
}
//далее продолжаем обработку
поменяли обработку запроса в шаблоне
<td><a href='/snippet/view/{{.ID}}'>{{.Title}}</a></td>
Обработка ошибок
в пакете есть специальный метод app.notFound()
настройка в начале файла routes.go
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.notFound(w)
})
позволит несуществующие пути обрабатывать ошибкой 404
Обработка форм
- Выполнить запрос GET к серверу для запроса формы
- Заполнение формы и отправка на сервер результата методом POST
- Обработка формы и валидация handler-ом
snippetCreatePost
- После проверки и добавление редирект на
/snippet/view/:id для просмотра результата
Шаблон формы create.tmpl
{{define "title"}}Create a New Snippet{{end}}
{{define "main"}}
<form action='/snippet/create' method='POST'>
<div>
<label>Title:</label>
<input type='text' name='title'>
</div>
<div>
<label>Content:</label>
<textarea name='content'></textarea>
</div>
<div>
<label>Delete in:</label>
<input type='radio' name='expires' value='365' checked> One Year
<input type='radio' name='expires' value='7'> One Week
<input type='radio' name='expires' value='1'> One Day
</div>
<div>
<input type='submit' value='Publish snippet'>
</div>
</form>
{{end}}
Ссылка на вызов формы nav.html
в меню добавим пункт с вызовом формы
{{define "nav"}}
<nav>
<a href='/'>Home</a>
<!-- Add a link to the new form -->
<a href='/snippet/create'>Create snippet</a>
</nav>
{{end}}
Обработчик формы handler
func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
app.render(w, http.StatusOK, "create.tmpl", data)
}
Парсинг формы
метод парсинга и проверки формы. Парсит форму и сохраняет значения в map r.ParseForm
Для получения значений из полей формы, используем r.PostForm.Get() метод. Например: r.PostForm.Get("title")
Функция обработчик формы:
func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
//парсинг формы и проверка ошибок
err := r.ParseForm()
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
//получение значений title и content
title := r.PostForm.Get("title")
content := r.PostForm.Get("content")
//преобразование в целое число, так как все значения приходят в формате string
expires, err := strconv.Atoi(r.PostForm.Get("expires"))
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
//выполнение запроса к базе данных
id, err := app.snippets.Insert(title, content, expires)
if err != nil {
app.serverError(w, err)
return
}
http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}
Для справки
r.PostForm map популярен только для POST, PATCH и PUT запросов. Но еще есть карта r.Form которая сохраняет не только значение тела запроса но и из строки /snippet/create?foo=bar
Стандартные методы r.FormValue() и r.PostFormValue() автор не рекомендует использовать из-за сложностей в обработке ошибок.
Обработка множественных значений
r.PostForm.Get() метод Get не работает с множественными значениями и вернет только первый
например флажки чекбокса
В этом случае вам нужно будет работать непосредственно с картой r.PostForm. Основным типом карты r.PostForm является url.Values, который, в свою очередь, имеет базовый тип map[string][]string. Таким образом, для полей с несколькими значениями вы можете пройтись по базовой карте, чтобы получить к ним доступ следующим образом:
for i, item := range r.PostForm["items"] {
fmt.Fprintf(w, "%d: Item %s\n", i, item)
}
Ограничение размера формы
Если вы не отправляете составные данные (т.е. ваша форма имеет атрибут enctype="multipart/form-data"), то тела запросов POST, PUT и PATCH ограничены 10 МБ. Если это значение будет превышено, то r.ParseForm() вернет ошибку.
Если вы хотите изменить это ограничение, вы можете использовать http.MaxBytesReader() функцию.
// Limit the request body size to 4096 bytes
r.Body = http.MaxBytesReader(w, r.Body, 4096)
err := r.ParseForm()
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
Кроме того, при достижении лимита MaxBytesReader устанавливает флаг на http.ResponseWriter, который указывает серверу закрыть базовое TCP-соединение.
Валидация формы
// создадим переменную для валидации
fieldErrors := make(map[string]string)
// проверка, что title не пусто и не больше 100 знаков. Результат проблем записываем в нашу map с ключом именем поля
if strings.TrimSpace(title) == "" {
fieldErrors["title"] = "This field cannot be blank"
} else if utf8.RuneCountInString(title) > 100 {
fieldErrors["title"] = "This field cannot be more than 100 characters long"
}
// проверяем на пустоту content и записываем в map если есть проблемы
if strings.TrimSpace(content) == "" {
fieldErrors["content"] = "This field cannot be blank"
}
// проверяем что поле равно 1 7 или 365
if expires != 1 && expires != 7 && expires != 365 {
fieldErrors["expires"] = "This field must equal 1, 7 or 365"
}
// а теперь простая проверка всего map. Если map не пустой, значит есть ошибки и возвращаем ошибку. Точнее возвращаем весь map.
if len(fieldErrors) > 0 {
fmt.Fprint(w, fieldErrors)
return
}
Отображение ошибок на форме
- В обработчик шаблонов templates.go добавим поле Form
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
Form any
}
поле Form необходимо для отправки результатов валидации формы
- В handlers.go добавим map с результатами валидации в структуру snippetCreateForm
type snippetCreateForm struct {
Title string
Content string
Expires int
FieldErrors map[string]string
}
- Заполним структуру значениями
form := snippetCreateForm{
Title: r.PostForm.Get("title"),
Content: r.PostForm.Get("content"),
Expires: expires,
FieldErrors: map[string]string{},
}
- Выполним валидацию
- Если есть ошибки то возвращаем ответ с ошибками
if len(form.FieldErrors) > 0 {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data)
return
}
т.е. вызываем эту же форму, только с заполненным map с ошибками, но если ошибок нет, то этот шаг не выполнится и пойдет обращение к базе данных
Изменяем шаблон для отображения ошибок
- Обновим create.tmpl шаблон формы
- Для повторного отображения введенных значений, используем значения поля Form
{{.Form.Title}} и {{.Form.Content}}
- Для отображения ошибок валидации используем значения map:
{{.Form.FieldErrors.title}} и другие
- Шаблон стал теперь таким
{{define "title"}}Create a New Snippet{{end}}
{{define "main"}}
<form action='/snippet/create' method='POST'>
<div>
<label>Title:</label>
<!-- Use the `with` action to render the value of .Form.FieldErrors.title
if it is not empty. -->
{{with .Form.FieldErrors.title}}
<label class='error'>{{.}}</label>
{{end}}
<!-- Re-populate the title data by setting the `value` attribute. -->
<input type='text' name='title' value='{{.Form.Title}}'>
</div>
<div>
<label>Content:</label>
<!-- Likewise render the value of .Form.FieldErrors.content if it is not
empty. -->
{{with .Form.FieldErrors.content}}
<label class='error'>{{.}}</label>
{{end}}
<!-- Re-populate the content data as the inner HTML of the textarea. -->
<textarea name='content'>{{.Form.Content}}</textarea>
</div>
<div>
<label>Delete in:</label>
<!-- And render the value of .Form.FieldErrors.expires if it is not empty. -->
{{with .Form.FieldErrors.expires}}
<label class='error'>{{.}}</label>
{{end}}
<!-- Here we use the `if` action to check if the value of the re-populated
expires field equals 365. If it does, then we render the `checked`
attribute so that the radio input is re-selected. -->
<input type='radio' name='expires' value='365' {{if (eq .Form.Expires 365)}}checked{{end}}> One Year
<!-- And we do the same for the other possible values too... -->
<input type='radio' name='expires' value='7' {{if (eq .Form.Expires 7)}}checked{{end}}> One Week
<input type='radio' name='expires' value='1' {{if (eq .Form.Expires 1)}}checked{{end}}> One Day
</div>
<div>
<input type='submit' value='Publish snippet'>
</div>
</form>
{{end}}
- В обработчик handlers.go нужно обязательно добавить инициализацию Form для пустых значений, чтобы не было ошибки
func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data.Form = snippetCreateForm{
Expires: 365,
}
app.render(w, http.StatusOK, "create.tmpl", data)
}
Создание хелперов валидации
Просто выносим всю валидацию из кода в отдельный модуль internal/validator
mkdir internal/validator
touch internal/validator/validator.go
создаем файл validator.go
File: internal/validator/validator.go
package validator
import (
"strings"
"unicode/utf8"
)
// Define a new Validator type which contains a map of validation errors for our
// form fields.
type Validator struct {
FieldErrors map[string]string
}
// Valid() returns true if the FieldErrors map doesn't contain any entries.
func (v *Validator) Valid() bool {
return len(v.FieldErrors) == 0
}
// AddFieldError() adds an error message to the FieldErrors map (so long as no
// entry already exists for the given key).
func (v *Validator) AddFieldError(key, message string) {
// Note: We need to initialize the map first, if it isn't already
// initialized.
if v.FieldErrors == nil {
v.FieldErrors = make(map[string]string)
}
if _, exists := v.FieldErrors[key]; !exists {
v.FieldErrors[key] = message
}
}
// CheckField() adds an error message to the FieldErrors map only if a
// validation check is not 'ok'.
func (v *Validator) CheckField(ok bool, key, message string) {
if !ok {
v.AddFieldError(key, message)
}
}
// NotBlank() returns true if a value is not an empty string.
func NotBlank(value string) bool {
return strings.TrimSpace(value) != ""
}
// MaxChars() returns true if a value contains no more than n characters.
func MaxChars(value string, n int) bool {
return utf8.RuneCountInString(value) <= n
}
// PermittedInt() returns true if a value is in a list of permitted integers.
func PermittedInt(value int, permittedValues ...int) bool {
for i := range permittedValues {
if value == permittedValues[i] {
return true
}
}
return false
}
Корректируем handlers.go
для использования валидатора внесем изменения
добавим импорт:
"snippetbox.alexedwards.net/internal/validator" // New import
- Убрали map и поставили validator.Validator
type snippetCreateForm struct {
Title string
Content string
Expires int
validator.Validator
}
основное преимущество validator.Validator в том, что он содержит все поля map и методы обработки
- При заполнении структуры Form, теперь не нужно заполонять map с ошибками
form := snippetCreateForm{
Title: r.PostForm.Get("title"),
Content: r.PostForm.Get("content"),
Expires: expires,
// Remove the FieldErrors assignment from here.
}
- Вызов проверок валидации теперь доступны прямо на переменной form
form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
form.CheckField(validator.PermittedInt(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")
- Проверяем валидность формы
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data)
return
}
Автоматический парсинг форм
Автор предлагает использовать сторонний пакет go-playground/form который по сути делает тоже самое, но более гибко и универсально
9.2 - Alex Edwards Let's Go часть 2
edwardsLetsGo учебный материал по написанию приложения на Go с авторизацией пользователя. Краткий конспект с основными мыслями (главы с 9 по 12)
Stateful HTTP
Отслеживание состояний в HTTP
Будем выводить для начала сообщение по результатам добавления новой формы
Выбор менеджера сессий
Сравнение и анализ менеджеров сессий
1. Сравнение gorilla/sessions и alexedwards/scs
| Характеристика |
gorilla/sessions |
alexedwards/scs |
| Тип хранилища |
Cookie, файлы, Redis, Memcached, другие (через сторонние адаптеры) |
Cookie, Redis, PostgreSQL, MySQL, SQLite, in-memory |
| Безопасность |
Поддержка подписанных кук, но требует ручной настройки |
Встроенная защита от подделки (secure cookies), автоматическое обновление токенов |
| Производительность |
Зависит от бэкенда (Redis быстрее, чем куки) |
Оптимизирован для работы с разными хранилищами, меньше накладных расходов |
| Гибкость |
Гибкий API, можно настраивать под разные сценарии |
Более структурированный API, но менее гибкий |
| Поддержка контекста |
Нет встроенной поддержки context.Context |
Полная интеграция с context.Context |
| Автоматическое продление |
Нет (нужно реализовывать вручную) |
Есть (автоматически обновляет сессии) |
| Зависимости |
Часть Gorilla Toolkit (можно использовать отдельно) |
Независимая библиотека |
| Активность разработки |
Заморожен (Gorilla Toolkit больше не поддерживается) |
Активно развивается |
| Использование в продакшене |
Широко использовался, но сейчас менее предпочтителен |
Рекомендуется для новых проектов |
Вывод:
- Gorilla/sessions — устаревающий, но проверенный вариант. Подходит, если уже используется Gorilla Toolkit.
- SCS (alexedwards/scs) — современный, безопасный и более удобный для новых проектов.
2. Другие популярные менеджеры сессий в Go
a) Gin Sessions (для фреймворка Gin)
- Особенности: Интеграция с Gin, поддержка Redis, Cookie, Memcached.
- Плюсы: Простота использования в Gin-приложениях.
- Минусы: Зависит от Gin, не подходит для других фреймворков.
- Ссылка: github.com/gin-contrib/sessions
b) Beego Session
- Особенности: Встроен в фреймворк Beego, поддерживает файлы, Redis, Memcached, MySQL, PostgreSQL.
- Плюсы: Готовое решение для Beego.
- Минусы: Привязан к Beego.
- Ссылка: beego.me
c) Go-chi/session (от создателей Chi)
- Особенности: Минималистичный, работает с
context.Context, поддерживает Redis и куки.
- Плюсы: Легковесный, хорош для микрофреймворков.
- Минусы: Меньше возможностей, чем у SCS.
- Ссылка: github.com/go-chi/session
d) Iriris Sessions
- Особенности: Интеграция с фреймворком Iris, поддержка Redis, BoltDB, Badger, файлы.
- Плюсы: Высокая производительность, удобство в Iris.
- Минусы: Привязан к Iris.
- Ссылка: github.com/kataras/iris/sessions
3. Какой выбрать?
- Для новых проектов → alexedwards/scs (безопасность, контекст, активная разработка).
- Для Gin → gin-contrib/sessions.
- Для микрофреймворков (Chi, Echo) → go-chi/session или scs.
- Legacy-проекты на Gorilla → gorilla/sessions (но лучше мигрировать на scs).
Установка alexedwards/scs
go get github.com/alexedwards/scs/v2@v2
go get github.com/alexedwards/scs/mysqlstore
Создать таблицу в базе данных для хранения сессий
Поддерживает много различных баз данных о чем говорит в документации: https://github.com/alexedwards/scs
USE snippetbox;
CREATE TABLE sessions (
token CHAR(43) PRIMARY KEY,
data BLOB NOT NULL,
expiry TIMESTAMP(6) NOT NULL
);
CREATE INDEX sessions_expiry_idx ON sessions (expiry);
этот пример для нашей базы snippetbox
- Поле токена (tocken) будет содержать уникальный, случайно сгенерированный идентификатор для каждого сеанса.
- Поле данных (data) будет содержать фактические данные сеанса, которые вы хотите использовать совместно с HTTP-запросами. Они хранятся в виде двоичных данных типа BLOB (двоичный большой объект).
- Поле истечения срока действия (expiry) будет содержать время истечения срока действия сеанса.
Пакет scs автоматически удалит истекшие сеансы из таблицы сеансов, чтобы она не стала слишком большой.
Изменения в main.go
- Добавим импорты
"github.com/alexedwards/scs/mysqlstore" // New import
"github.com/alexedwards/scs/v2" // New import
- Добавим поле сессии в структуру приложения
type application struct {
errorLog *log.Logger
infoLog *log.Logger
snippets *models.SnippetModel
templateCache map[string]*template.Template
formDecoder *form.Decoder
sessionManager *scs.SessionManager
}
- Настройка менеджера сессий
sessionManager := scs.New() // Используем scs.New() для инициализации нового менеджера сессий.
sessionManager.Store = mysqlstore.New(db) //настраиваем его на использование нашей базы данных MySQL в качестве хранилища сессий
sessionManager.Lifetime = 12 * time.Hour //устанавливаем время жизни 12 часов
- Добавить менеджер сессий в структуру приложения
app := &application{
errorLog: errorLog,
infoLog: infoLog,
snippets: &models.SnippetModel{DB: db},
templateCache: templateCache,
formDecoder: formDecoder,
sessionManager: sessionManager,
}
Обернуть обработчики запросов
Чтобы сеансы работали, нам также необходимо обернуть маршруты наших приложений в промежуточное ПО, предоставляемое методом SessionManager.LoadAndSave(). Это промежуточное ПО автоматически загружает и сохраняет данные сеанса при каждом HTTP-запросе и ответе.
/static/*filepath - оборачивать не будем
Создадим ноувую цепочку для динамических данных сайта
Корректируем файл: routes.go
- Создаем переменную для учета сессий
dynamic := alice.New(app.sessionManager.LoadAndSave)
- Обертываем обработчики
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
router.Handler(http.MethodGet, "/snippet/create", dynamic.ThenFunc(app.snippetCreate))
router.Handler(http.MethodPost, "/snippet/create", dynamic.ThenFunc(app.snippetCreatePost))
- Остальное оставляем без изменений
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
return standard.Then(router)
Если не используется пакет alice
синтаксис вызова будет другой
router := httprouter.New()
router.Handler(http.MethodGet, "/", app.sessionManager.LoadAndSave(http.HandlerFunc(app.home)))
router.Handler(http.MethodGet, "/snippet/view/:id", app.sessionManager.LoadAndSave(http.HandlerFunc(app.snippetView)))
// ... etc
Работа с данными сессии
Добавление сообщения об успешном добавлении сниппета
- Внесем изменения в файл handlers.go
Используйте метод Put() для добавления строкового значения (“Сниппет успешно создан!”) и соответствующего ключа (“flash”) к данным сессии.
func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
var form snippetCreateForm
...
app.sessionManager.Put(r.Context(), "flash", "Snippet successfully created!")
- Первый параметр, который мы передаем в app.sessionManager.Put(), — это текущий контекст запроса (это место, где менеджер сеансов временно хранит информацию, пока ваши обработчики работают с запросом.)
- Второй параметр (в нашем случае строка “flash”) является ключом для конкретного сообщения, которое мы добавляем в данные сессии. Впоследствии мы получим сообщение из данных сеанса также с помощью этого ключа.
Если для текущего пользователя нет существующего сеанса (или срок его сеанса истек), то новый пустой сеанс для него будет автоматически создан промежуточным программным обеспечением сеанса.
- Далее мы хотим, чтобы наш обработчик snippetView извлек флэш-сообщение (если оно существует в сессии для текущего пользователя) и передал его в HTML-шаблон для последующего отображения.
Поскольку мы хотим отобразить флэш-сообщение только один раз, на самом деле мы хотим извлечь и удалить сообщение из данных сеанса. Мы можем выполнить обе эти операции одновременно с помощью метода PopString().
func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
...
flash := app.sessionManager.PopString(r.Context(), "flash")
data := app.newTemplateData(r)
data.Snippet = snippet
// Pass the flash message to the template.
data.Flash = flash
app.render(w, http.StatusOK, "view.tmpl", data)
}
Информация
Информация: Если вы хотите получить значение только из данных сеанса (и оставить его там), вы можете использовать метод GetString(). Пакет scs также предоставляет методы для получения других распространенных типов данных, включая GetInt(), GetBool(), GetBytes() и GetTime().
- Добавить поле Flash в структуру данных templateData (в файле templates.go)
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
Form any
Flash string // Add a Flash field to the templateData struct.
}
- Внесем изменения в шаблон base.tmpl
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
<link rel='stylesheet' href='/static/css/main.css'>
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
{{template "nav" .}}
<main>
<!-- Display the flash message if one exists -->
{{with .Flash}}
<div class='flash'>{{.}}</div>
{{end}}
{{template "main" .}}
</main>
<footer>
Powered by <a href='https://golang.org/'>Go</a> in {{.CurrentYear}}
</footer>
<script src="/static/js/main.js" type="text/javascript"></script>
</body>
</html>
{{end}}
Помните
{{with .Flash}} будет выполнен только в том случае, если значение .Flash не является пустой строкой. Таким образом, если в сеансе текущего пользователя нет ключа «flash», то в результате часть новой разметки просто не будет отображаться.
- Модернизируем функцию newTemplateData в файле helpers.go для вывода других сообщений при совершении действий на сайте.
func (app *application) newTemplateData(r *http.Request) *templateData {
return &templateData{
CurrentYear: time.Now().Year(),
// Add the flash message to the template data, if one exists.
Flash: app.sessionManager.PopString(r.Context(), "flash"),
}
}
поэтому теперь можно убрать строки из файла handlers.go
flash := app.sessionManager.PopString(r.Context(), "flash") //убрать
...
data.Flash = flash //убрать
Безопасность данных
Создание самозаверяющего сертификата TLS
HTTPS — это, по сути, HTTP, отправляемый через соединение TLS (Transport Layer Security).
Поскольку данные отправляются через соединение TLS, они шифруются и подписываются, что помогает обеспечить их конфиденциальность и целостность во время передачи.
Прежде чем наш сервер сможет начать использовать HTTPS, нам необходимо сгенерировать сертификат TLS.
Удобно отметить, что пакет crypto/tls в стандартной библиотеке Go включает в себя инструмент generate_cert.go, который мы можем использовать для простого создания собственного самоподписанного сертификата.
Создание своего самозаписывающего сертификата
cd $HOME/code/snippetbox
mkdir tls
cd tls
Чтобы запустить инструмент generate_cert.go, вам нужно знать место на вашем компьютере, где установлен исходный код стандартной библиотеки Go.
Если вы используете Linux, macOS или FreeBSD и следуете официальным инструкциям по установке, то файл generate_cert.go должен находиться в папке /usr/local/go/src/crypto/tls.
Запуск генерации сертификата
$ go run /usr/local/go/src/crypto/tls/generate_cert.go --rsa-bits=2048 --host=localhost
на manjaro оказался на /usr/lib/go/src/crypto/tls/generate_cert.go
для поиска можно воспользоваться командой:
find /usr -name 'generate_cert.go'
За кулисами инструмент generate_cert.go работает в два этапа:
- сначала он генерирует 2048-битную пару ключей RSA, которая является криптографически защищенным открытым ключом и закрытым ключом.
- Затем он сохраняет закрытый ключ в файле key.pem и генерирует самоподписанный сертификат TLS для хоста localhost, содержащий открытый ключ, который он хранит в файле cert.pem.
Как закрытый ключ, так и сертификат закодированы в PEM, что является стандартным форматом, используемым в большинстве реализаций TLS.
Запуск HTTPS сервера
- Заменить в файле main.go srv.ListenAndServe() на srv.ListenAndServeTLS()
err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem")
errorLog.Fatal(err)
- Запустить приложение
- Открыть сайт https://localhost:4000/ по протоколу https
Важно
Важно отметить, что пользователь, которого вы используете для запуска приложения Go, должен иметь разрешения на чтение файлов cert.pem и key.pem, в противном случае ListenAndServeTLS() вернет ошибку отказа в разрешении.
Занесите каталог tls в .gitignore
нежелательно пушить сертификаты в репозиторий
Настройка параметров HTTPS
Go имеет хорошие настройки по умолчанию для своего HTTPS-сервера, но можно оптимизировать и настроить поведение сервера.
tls.CurveP256 and tls.X25519 - рекомендуется использовать эти протоколы
Добавим настройки в файл main.go
- Добавим импорт
import (
"crypto/tls" // New import
- Добавим tlsConfig
tlsConfig := &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
}
- Добавим tls настройки в структуру srv
srv := &http.Server{
Addr: *addr,
ErrorLog: errorLog,
Handler: app.routes(),
TLSConfig: tlsConfig,
}
Настройка версий TLS
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
}
Ограничение наборов шифров Полный набор наборов шифров, поддерживаемых Go,
tlsConfig := &tls.Config{
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,
},
}
Тайм-ауты подключения
Давайте уделим немного времени, чтобы повысить отказоустойчивость нашего сервера, добавив некоторые настройки тайм-аута, например:
srv := &http.Server{
Addr: *addr,
ErrorLog: errorLog,
Handler: app.routes(),
TLSConfig: tlsConfig,
// Add Idle, Read and Write timeouts to the server.
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
добавляем в структуру srv необходимые поля для настройки таймаута
Все эти три тайм-аута — IdleTimeout, ReadTimeout и WriteTimeout — являются общесерверными настройками, которые действуют на базовое соединение и применяются ко всем запросам независимо от их обработчика или URL-адреса.
Параметр IdleTimeout
По умолчанию Go включает проверку активности для всех принятых подключений. Это помогает уменьшить задержку (особенно для подключений HTTPS), поскольку клиент может повторно использовать одно и то же соединение для нескольких запросов без необходимости повторять рукопожатие.
В нашем случае мы установили IdleTimeout на 1 минуту, что означает, что все keep-alive соединения будут автоматически закрыты после 1 минуты бездействия.
Параметр ReadTimeout
В нашем коде мы также установили параметр ReadTimeout равным 5 секундам. Это означает, что если заголовки или тело запроса все еще считываются через 5 секунд после первого принятия запроса, то Go закроет базовое соединение. Поскольку это “жесткое” замыкание соединения, пользователь не получит никакого ответа HTTP(S). Установка короткого периода ReadTimeout помогает снизить риск атак на медленных клиентов, таких как Slowloris, которые в противном случае могли бы держать соединение открытым на неопределенный срок, отправляя частичные, неполные HTTP(S) запросы.
Параметр WriteTimeout
Параметр WriteTimeout закроет базовое соединение, если наш сервер попытается записать данные в соединение через определенный период времени (в нашем коде это 10 секунд).
Но это ведет себя немного по-разному в зависимости от используемого протокола.
Для HTTP-соединений, если некоторые данные записываются в соединение более чем через 10 секунд после завершения чтения заголовка запроса, Go закроет базовое соединение, а не запишет данные.
Для соединений HTTPS, если некоторые данные записываются в соединение более чем через 10 секунд после первого принятия запроса, Go закроет базовое соединение вместо записи данных.
Это означает, что если вы используете HTTPS (как мы), имеет смысл установить значение WriteTimeout больше, чем ReadTimeout.
Важно помнить, что записи, выполненные обработчиком, буферизуются и записываются в соединение как единое целое при возврате обработчика. Таким образом, идея WriteTimeout обычно заключается не в том, чтобы предотвратить длительную работу обработчиков, а в том, чтобы предотвратить слишком долгую запись данных, возвращаемых обработчиком.
Функция http.Server также предоставляет параметр ReadHeaderTimeout, который мы не использовали в нашем приложении. Это работает аналогично ReadTimeout, за исключением того, что применяется только к чтению заголовков HTTP(S).
Это может быть полезно, если вы хотите применить ограничение на уровне сервера для чтения заголовков запросов, но хотите реализовать разные тайм-ауты на разных маршрутах, когда дело доходит до чтения тела запроса (возможно, с использованием http.TimeoutHandler() промежуточное ПО).
Для нашего веб-приложения Snippetbox у нас нет никаких действий, которые гарантируют тайм-ауты чтения для каждого маршрута — чтение заголовков и тел запросов для всех наших маршрутов должно быть комфортно завершено за 5 секунд, поэтому мы будем использовать ReadTimeout.
http.Server также предоставляет поле MaxHeaderBytes, которое можно использовать для управления максимальным количеством байтов, которое сервер будет считывать при анализе заголовков запросов. По умолчанию Go разрешает максимальную длину заголовка 1 МБ.
Например, если вы хотите ограничить максимальную длину заголовка до 0,5 МБ, вы должны написать:
srv := &http.Server{
Addr: *addr,
MaxHeaderBytes: 524288,
...
}
Аутентификация пользователей
- Пользователь должен зарегистрироваться, посетив форму по адресу /user/signup и введя свое имя, адрес электронной почты и пароль. Мы сохраним эту информацию в новой таблице базы данных пользователей.
- Пользователь войдет в систему, посетив форму по адресу /user/login и введя свой адрес электронной почты и пароль.
- Затем мы проверим базу данных, чтобы увидеть, совпадают ли введенные ими адрес электронной почты и пароль с одним из пользователей в таблице пользователей. Если совпадение найдено, пользователь успешно прошел проверку подлинности, и мы добавляем соответствующее значение id для пользователя в данные его сеанса с помощью ключа “authenticatedUserID”.
- Когда мы получаем какие-либо последующие запросы, мы можем проверить данные сеанса пользователя на наличие значения “authenticatedUserID”. Если она существует, мы знаем, что пользователь уже успешно вошел в систему. Мы можем продолжать проверять это до тех пор, пока сессия не истечет, когда пользователю потребуется снова войти в систему. Если в сеансе отсутствует “authenticatedUserID”, мы знаем, что пользователь не вошел в систему.
Подготовительные мероприятия для создания инфраструктуры данных
Добавим новые маршруты для регистрации пользователей
| HTTP Method |
Route |
Handler |
Description |
| GET |
/user/signup |
userSignup |
Display a HTML form for signing up a new user |
| POST |
/user/signup |
userSignupPost |
Create a new user |
| GET |
/user/login |
userLogin |
Display a HTML form for logging in a user |
| POST |
/user/login |
userLoginPost |
Authenticate and login the user |
| POST |
/user/logout |
userLogoutPost |
Logout the user |
Пропишем в файле routes.go данные маршруты:
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
router.Handler(http.MethodGet, "/user/login", dynamic.ThenFunc(app.userLogin))
router.Handler(http.MethodPost, "/user/login", dynamic.ThenFunc(app.userLoginPost))
router.Handler(http.MethodPost, "/user/logout", dynamic.ThenFunc(app.userLogoutPost))
Создание обработчиков для маршрутов handlers.go
func (app *application) userSignup(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Display a HTML form for signing up a new user...")
}
func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Create a new user...")
}
func (app *application) userLogin(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Display a HTML form for logging in a user...")
}
func (app *application) userLoginPost(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Authenticate and login the user...")
}
func (app *application) userLogoutPost(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Logout the user...")
}
Создать ссылки на экране для вызова функций nav.tmpl
{{define "nav"}}
<nav>
<div>
<a href='/'>Home</a>
<a href='/snippet/create'>Create snippet</a>
</div>
<div>
<a href='/user/signup'>Signup</a>
<a href='/user/login'>Login</a>
<form action='/user/logout' method='POST'>
<button>Logout</button>
</form>
</div>
</nav>
{{end}}
Создать таблицу для пользователей
USE snippetbox;
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
hashed_password CHAR(60) NOT NULL,
created DATETIME NOT NULL
);
ALTER TABLE users ADD CONSTRAINT users_uc_email UNIQUE (email);
Мы также добавили ограничение UNIQUE в столбец электронной почты и назвали его users_uc_email. Это ограничение гарантирует, что у нас не будет двух пользователей с одинаковым адресом электронной почты. Если мы попытаемся вставить запись в эту таблицу с дубликатом электронной почты, MySQL выдаст ошибку 1062: ER_DUP_ENTRY ошибку.
Создать аналогичные методы и структуры для работы с данными пользователей
Добавить необходимые типы ошибок для обработки пользователей в файл errors.go
package models
import (
"errors"
)
var (
ErrNoRecord = errors.New("models: no matching record found")
// Add a new ErrInvalidCredentials error. We'll use this later if a user
// tries to login with an incorrect email address or password.
ErrInvalidCredentials = errors.New("models: invalid credentials")
// Add a new ErrDuplicateEmail error. We'll use this later if a user
// tries to signup with an email address that's already in use.
ErrDuplicateEmail = errors.New("models: duplicate email")
)
Создаем отдельный файл для обработки пользователей users.go
cd $HOME/code/snippetbox
touch internal/models/users.go
package models
import (
"database/sql"
"time"
)
// Define a new User type. Notice how the field names and types align
// with the columns in the database "users" table?
type User struct {
ID int
Name string
Email string
HashedPassword []byte
Created time.Time
}
// Define a new UserModel type which wraps a database connection pool.
type UserModel struct {
DB *sql.DB
}
// We'll use the Insert method to add a new record to the "users" table.
func (m *UserModel) Insert(name, email, password string) error {
return nil
}
// We'll use the Authenticate method to verify whether a user exists with
// the provided email address and password. This will return the relevant
// user ID if they do.
func (m *UserModel) Authenticate(email, password string) (int, error) {
return 0, nil
}
// We'll use the Exists method to check if a user exists with a specific ID.
func (m *UserModel) Exists(id int) (bool, error) {
return false, nil
}
Добавить новое поле в структуру приложения main.go
добавим поле users
type application struct {
errorLog *log.Logger
infoLog *log.Logger
snippets *models.SnippetModel
users *models.UserModel
templateCache map[string]*template.Template
formDecoder *form.Decoder
sessionManager *scs.SessionManager
}
func main() {
...
// Initialize a models.UserModel instance and add it to the application
// dependencies.
app := &application{
errorLog: errorLog,
infoLog: infoLog,
snippets: &models.SnippetModel{DB: db},
users: &models.UserModel{DB: db},
templateCache: templateCache,
formDecoder: formDecoder,
sessionManager: sessionManager,
}
...
Регистрация пользователей
Создадим форму регистрации ui/html/pages/signup.tmpl
$ cd $HOME/code/snippetbox
$ touch ui/html/pages/signup.tmpl
{{define "title"}}Signup{{end}}
{{define "main"}}
<form action='/user/signup' method='POST' novalidate>
<div>
<label>Name:</label>
{{with .Form.FieldErrors.name}}
<label class='error'>{{.}}</label>
{{end}}
<input type='text' name='name' value='{{.Form.Name}}'>
</div>
<div>
<label>Email:</label>
{{with .Form.FieldErrors.email}}
<label class='error'>{{.}}</label>
{{end}}
<input type='email' name='email' value='{{.Form.Email}}'>
</div>
<div>
<label>Password:</label>
{{with .Form.FieldErrors.password}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='password'>
</div>
<div>
<input type='submit' value='Signup'>
</div>
</form>
{{end}}
Создадим новую структуру данных для регистрации пользователя
В файле handlers.go:
package main
...
// Create a new userSignupForm struct.
type userSignupForm struct {
Name string `form:"name"`
Email string `form:"email"`
Password string `form:"password"`
validator.Validator `form:"-"`
}
// Update the handler so it displays the signup page.
func (app *application) userSignup(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data.Form = userSignupForm{}
app.render(w, http.StatusOK, "signup.tmpl", data)
}
...
Валидация формы регистрации
Добавим новые методы валидации в файле: internal/validator/validator.go
package validator
import (
"regexp" // New import
"strings"
"unicode/utf8"
)
// Use the regexp.MustCompile() function to parse a regular expression pattern
// for sanity checking the format of an email address. This returns a pointer to
// a 'compiled' regexp.Regexp type, or panics in the event of an error. Parsing
// this pattern once at startup and storing the compiled *regexp.Regexp in a
// variable is more performant than re-parsing the pattern each time we need it.
var EmailRX = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
...
// MinChars() returns true if a value contains at least n characters.
func MinChars(value string, n int) bool {
return utf8.RuneCountInString(value) >= n
}
// Matches() returns true if a value matches a provided compiled regular
// expression pattern.
func Matches(value string, rx *regexp.Regexp) bool {
return rx.MatchString(value)
}
Добавим обработчик формы в handlers.go
func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) {
// Declare an zero-valued instance of our userSignupForm struct.
var form userSignupForm
// Parse the form data into the userSignupForm struct.
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
// Validate the form contents using our helper functions.
form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank")
form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
form.CheckField(validator.MinChars(form.Password, 8), "password", "This field must be at least 8 characters long")
// If there are any errors, redisplay the signup form along with a 422
// status code.
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "signup.tmpl", data)
return
}
// Otherwise send the placeholder response (for now!).
fmt.Fprintln(w, "Create a new user...")
}
Краткое введение в bcrypt
- Хорошей практикой — является хранение одностороннего хеша пароля, полученного с помощью ресурсоемкой функции извлечения ключа, такой как Argon2, scrypt или bcrypt. Go имеет реализации всех 3 алгоритмов в пакете golang.org/x/crypto.
- Тем не менее, плюсом реализации bcrypt является то, что она включает в себя вспомогательные функции, специально разработанные для хеширования и проверки паролей, и именно их мы будем использовать здесь.
- Установка bcrypt
go get golang.org/x/crypto/bcrypt@latest
- Создать хэш пароля
hash, err := bcrypt.GenerateFromPassword([]byte("my plain text password"), 12)
- Эта функция вернет хеш длиной 60 символов, который выглядит примерно так:
$2a$12$NuTjWXm3KKntReFwyBVHyuf/to.HEwTy.eS206TNfkGfr6HzGJSWG
Второй параметр мы передаем в bcrypt.GenerateFromPassword() указывает стоимость, которая представлена целым числом от 4 до 31. 12 означает, что для генерации хэша пароля будет использовано 4096 (2^12) итераций bcrypt.
- Проверка пароля: функция
bcrypt.CompareHashAndPassword()
hash := []byte("$2a$12$NuTjWXm3KKntReFwyBVHyuf/to.HEwTy.eS206TNfkGfr6GzGJSWG")
err := bcrypt.CompareHashAndPassword(hash, []byte("my plain text password"))
Хранение информации о пользователе
Следующим этапом нашей сборки является обновление метода UserModel.Insert() таким образом, чтобы он создал новую запись в нашей таблице пользователей, содержащую проверенное имя, адрес электронной почты и хэшированный пароль.
- Для обработки ошибок в базе данных и сохранения хэша понадобятся библиотеки
import (
"database/sql"
"errors" // New import
"strings" // New import
"time"
"github.com/go-sql-driver/mysql" // New import
"golang.org/x/crypto/bcrypt" // New import
)
- Файл
internal/models/users.go для обработки запросов к базе данных
type UserModel struct {
DB *sql.DB
}
func (m *UserModel) Insert(name, email, password string) error {
// Create a bcrypt hash of the plain-text password.
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return err
}
stmt := `INSERT INTO users (name, email, hashed_password, created)
VALUES(?, ?, ?, UTC_TIMESTAMP())`
// Use the Exec() method to insert the user details and hashed password
// into the users table.
_, err = m.DB.Exec(stmt, name, email, string(hashedPassword))
if err != nil {
// If this returns an error, we use the errors.As() function to check
// whether the error has the type *mysql.MySQLError. If it does, the
// error will be assigned to the mySQLError variable. We can then check
// whether or not the error relates to our users_uc_email key by
// checking if the error code equals 1062 and the contents of the error
// message string. If it does, we return an ErrDuplicateEmail error.
var mySQLError *mysql.MySQLError
if errors.As(err, &mySQLError) {
if mySQLError.Number == 1062 && strings.Contains(mySQLError.Message, "users_uc_email") {
return ErrDuplicateEmail
}
}
return err
}
return nil
}
- В основном приложении handlers.go отрабатывает запросы по маршрутам
func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) {
var form userSignupForm
...
err = app.users.Insert(form.Name, form.Email, form.Password)
if err != nil {
if errors.Is(err, models.ErrDuplicateEmail) {
form.AddFieldError("email", "Email address is already in use")
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "signup.tmpl", data)
} else {
app.serverError(w, err)
}
return
}
...
Внимание
Автор не рекомендует использовать встроенные функции преобразования паролей в хэш в базах данных.
User login
Для начала обновим internal/validator/validator.go
package validator
...
// Add a new NonFieldErrors []string field to the struct, which we will use to
// hold any validation errors which are not related to a specific form field.
type Validator struct {
NonFieldErrors []string
FieldErrors map[string]string
}
// Update the Valid() method to also check that the NonFieldErrors slice is
// empty.
func (v *Validator) Valid() bool {
return len(v.FieldErrors) == 0 && len(v.NonFieldErrors) == 0
}
// Create an AddNonFieldError() helper for adding error messages to the new
// NonFieldErrors slice.
func (v *Validator) AddNonFieldError(message string) {
v.NonFieldErrors = append(v.NonFieldErrors, message)
}
основная идея изменений, это добавить в основной валидатор проверку не только некорректных данных в форме, но и проверок в базе данных
Создадим шаблон для login (ui/html/pages/login.tmpl)
{{define "title"}}Login{{end}}
{{define "main"}}
<form action='/user/login' method='POST' novalidate>
<!-- Notice that here we are looping over the NonFieldErrors and displaying
them, if any exist -->
{{range .Form.NonFieldErrors}}
<div class='error'>{{.}}</div>
{{end}}
<div>
<label>Email:</label>
{{with .Form.FieldErrors.email}}
<label class='error'>{{.}}</label>
{{end}}
<input type='email' name='email' value='{{.Form.Email}}'>
</div>
<div>
<label>Password:</label>
{{with .Form.FieldErrors.password}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='password'>
</div>
<div>
<input type='submit' value='Login'>
</div>
</form>
{{end}}
Создадим структуру данных для login (handlers.go)
// Create a new userLoginForm struct.
type userLoginForm struct {
Email string `form:"email"`
Password string `form:"password"`
validator.Validator `form:"-"`
}
// Update the handler so it displays the login page.
func (app *application) userLogin(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data.Form = userLoginForm{}
app.render(w, http.StatusOK, "login.tmpl", data)
}
Проверка пользователя
- Проверить есть такой email — если нет, то вернуть ошибку
- Сравнить хэш пароля — если не совпадает, вернуть ошибку
- Если все ОК — вернем ID пользователя
Внесем изменения в internal/models/users.go
func (m *UserModel) Authenticate(email, password string) (int, error) {
// Retrieve the id and hashed password associated with the given email. If
// no matching email exists we return the ErrInvalidCredentials error.
var id int
var hashedPassword []byte
stmt := "SELECT id, hashed_password FROM users WHERE email = ?"
err := m.DB.QueryRow(stmt, email).Scan(&id, &hashedPassword)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, ErrInvalidCredentials
} else {
return 0, err
}
}
// Check whether the hashed password and plain-text password provided match.
// If they don't, we return the ErrInvalidCredentials error.
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return 0, ErrInvalidCredentials
} else {
return 0, err
}
}
// Otherwise, the password is correct. Return the user ID.
return id, nil
}
- После подтверждения пользователя создадим текущую сессию с пользователем. Внесем изменения в handlers.go
func (app *application) userLoginPost(w http.ResponseWriter, r *http.Request) {
// Decode the form data into the userLoginForm struct.
var form userLoginForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "login.tmpl", data)
return
}
// Check whether the credentials are valid. If they're not, add a generic
// non-field error message and re-display the login page.
id, err := app.users.Authenticate(form.Email, form.Password)
if err != nil {
if errors.Is(err, models.ErrInvalidCredentials) {
form.AddNonFieldError("Email or password is incorrect")
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "login.tmpl", data)
} else {
app.serverError(w, err)
}
return
}
//после каждого изменения состояния пользователя login/logout изменяем хэш текущей сессии
err = app.sessionManager.RenewToken(r.Context())
if err != nil {
app.serverError(w, err)
return
}
// Add the ID of the current user to the session, so that they are now
// 'logged in'.
app.sessionManager.Put(r.Context(), "authenticatedUserID", id)
// Redirect the user to the create snippet page.
http.Redirect(w, r, "/snippet/create", http.StatusSeeOther)
}
Примечание: Метод SessionManager.RenewToken(), который мы используем в приведенном выше коде, изменит идентификатор текущей сессии пользователя, но сохранит все данные, связанные с сессией. Рекомендуется делать это перед входом в систему, чтобы снизить риск атаки фиксации сеанса. Для получения дополнительной информации и информации по этому вопросу, пожалуйста, ознакомьтесь со шпаргалкой по управлению сеансами OWASP.
User logout
- Нужно удалить authenticatedUserID из сессии и все.
Обновим файл handlers.go
func (app *application) userLogoutPost(w http.ResponseWriter, r *http.Request) {
// обновим токен сессии
err := app.sessionManager.RenewToken(r.Context())
if err != nil {
app.serverError(w, err)
return
}
// удалим authenticatedUserID из сессии'logged out'.
app.sessionManager.Remove(r.Context(), "authenticatedUserID")
// Add a flash message to the session to confirm to the user that they've been
// logged out.
app.sessionManager.Put(r.Context(), "flash", "You've been logged out successfully!")
// Redirect the user to the application home page.
http.Redirect(w, r, "/", http.StatusSeeOther)
}
User authorization
В этом разделе включаются те полезности, которые доступны только аутентифицированным пользователям
- они могут создавать сниппеты
- в меню появляются новые пункты и убираются ненужные
Содержимое панели навигации меняется в зависимости от того, аутентифицирован ли пользователь (вошел в систему) или нет. В частности:
- аутентифицированные пользователи должны видеть ссылки на «Главную», «Создать сниппет» и «Выйти».
- Неаутентифицированные пользователи должны видеть ссылки на «Главную», «Регистрация» и «Вход».
Проверка аутентификации
Чтобы проверить аутентификацию пользователя, нужно проверить значение authenticatedUserID в сессии.
Чтобы не забыть, мы при аутентификации выполнили команду:
app.sessionManager.Put(r.Context(), "authenticatedUserID", id)
и эта информация записалась в базу данных в поле data. С этим контекстом работает пакет SCS и выполняет все проверки через свои команды.
Добавим вспомогательную функцию isAuthenticated() для возврата статуса аутентификации (helpers.go):
func (app *application) isAuthenticated(r *http.Request) bool {
return app.sessionManager.Exists(r.Context(), "authenticatedUserID")
}
Добавим новое поле IsAuthenticated в нашу структуру templateData (templates.go):
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
Form any
Flash string
IsAuthenticated bool // Add an IsAuthenticated field to the templateData struct.
}
Обновить newTemplateData() в helpers.go, чтобы эта информация автоматически добавлялась в структуру templateData каждый раз, когда мы отрисовываем шаблон.
func (app *application) newTemplateData(r *http.Request) *templateData {
return &templateData{
CurrentYear: time.Now().Year(),
Flash: app.sessionManager.PopString(r.Context(), "flash"),
// Add the authentication status to the template data.
IsAuthenticated: app.isAuthenticated(r),
}
}
Проверяем поле {{if .IsAuthenticated}} в шаблонах nav.tmpl
{{define "nav"}}
<nav>
<div>
<a href='/'>Home</a>
<!-- Toggle the link based on authentication status -->
{{if .IsAuthenticated}}
<a href='/snippet/create'>Create snippet</a>
{{end}}
</div>
<div>
<!-- Toggle the links based on authentication status -->
{{if .IsAuthenticated}}
<form action='/user/logout' method='POST'>
<button>Logout</button>
</form>
{{else}}
<a href='/user/signup'>Signup</a>
<a href='/user/login'>Login</a>
{{end}}
</div>
</nav>
{{end}}
Ограничение доступа к страницам
Ограничение доступа
В настоящее время мы скрываем навигационную ссылку «Создать сниппет» для всех пользователей, которые не вошли в систему. Но пользователь, не прошедший аутентификацию, все равно может создать новый сниппет, посетив страницу https://localhost:4000/snippet/create напрямую.
Давайте исправим это, так что если неаутентифицированный пользователь попытается посетить какие-либо маршруты с URL-адресом /snippet/create, он будет перенаправлен на /user/login.
Будем использовать middleware
Внесем изменения в middleware.go
func (app *application) requireAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Если пользователь не аутентифицирован, перенаправьте его на страницу входа и
// вернитесь из цепочки промежуточного ПО, чтобы последующие обработчики в
// цепочке не выполнялись.
if !app.isAuthenticated(r) {
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
return
}
//В противном случае установите заголовок "Cache-Control: no-store" так, чтобы страницы,
// требующие аутентификации, не сохранялись в кэше браузера пользователя (или
// другом промежуточном кэше).
w.Header().Add("Cache-Control", "no-store")
// And call the next handler in the chain.
next.ServeHTTP(w, r)
})
}
В нашем случае мы захотим защитить маршруты GET /snippet/create и POST /snippet/create.
И нет особого смысла выходить из системы пользователя, если он не вошел в систему, поэтому имеет смысл использовать его также и в маршруте POST /user/logout.
Чтобы помочь в этом, давайте разделим маршруты наших приложений на две «группы».
- Первая группа будет содержать наши «незащищенные» маршруты и использовать нашу существующую динамическую цепочку промежуточного программного обеспечения.
- Вторая группа будет содержать наши «защищенные» маршруты и будет использовать новую защищенную цепочку промежуточного ПО, состоящую из динамической цепочки промежуточного ПО и нашего нового промежуточного ПО requireAuthentication().
Корректируем файл routes.go
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.notFound(w)
})
fileServer := http.FileServer(http.Dir("./ui/static/"))
router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
// Unprotected application routes using the "dynamic" middleware chain.
dynamic := alice.New(app.sessionManager.LoadAndSave)
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
router.Handler(http.MethodGet, "/user/login", dynamic.ThenFunc(app.userLogin))
router.Handler(http.MethodPost, "/user/login", dynamic.ThenFunc(app.userLoginPost))
// Protected (authenticated-only) application routes, using a new "protected"
// middleware chain which includes the requireAuthentication middleware.
protected := dynamic.Append(app.requireAuthentication)
router.Handler(http.MethodGet, "/snippet/create", protected.ThenFunc(app.snippetCreate))
router.Handler(http.MethodPost, "/snippet/create", protected.ThenFunc(app.snippetCreatePost))
router.Handler(http.MethodPost, "/user/logout", protected.ThenFunc(app.userLogoutPost))
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
return standard.Then(router)
}
Без использования alice
Если вы не используете пакет justinas/alice — вы можете вручную обернуть обработчики следующим образом:
router.Handler(http.MethodPost, "/snippet/create", app.sessionManager.LoadAndSave(app.requireAuthentication(http.HandlerFunc(app.snippetCreate))))
CSRF protection
Что такое CSRF Protection
Что такое CSRF Protection?
CSRF (Cross-Site Request Forgery) — это тип атаки, при которой злоумышленник заставляет пользователя выполнить нежелательное действие на веб-сайте, на котором он аутентифицирован. Это может привести к несанкционированным действиям, таким как изменение паролей, отправка сообщений или выполнение финансовых транзакций.
Зачем нужна защита от CSRF?
Защита от CSRF необходима для предотвращения таких атак, чтобы обеспечить безопасность пользователей и защитить их данные. Без этой защиты злоумышленники могут использовать доверие пользователя к веб-приложению для выполнения вредоносных действий.
Автор рекомендует использовать пакет justinas/nosurf для борьбы с CSRF
go get github.com/justinas/nosurf@v1
Добавление функции в middleware.go
import (
"fmt"
"net/http"
"github.com/justinas/nosurf" // New import
)
...
// Create a NoSurf middleware function which uses a customized CSRF cookie with
// the Secure, Path and HttpOnly attributes set.
func noSurf(next http.Handler) http.Handler {
csrfHandler := nosurf.New(next)
csrfHandler.SetBaseCookie(http.Cookie{
HttpOnly: true,
Path: "/",
Secure: true,
})
return csrfHandler
}
Защита формы logout
Одной из форм, которую нам необходимо защитить от CSRF-атак, является форма выхода из системы, которая включена в наш partial nav.tmpl и потенциально может появиться на любой странице нашего приложения. Таким образом, из-за этого нам необходимо использовать промежуточное ПО noSurf() на всех маршрутах нашего приложения (кроме /static/*filepath).
Исправим routes.go
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf)
остальные маршруты получают настройку автоматически
Добавление скрытых полей с токеном на каждую форму
Исправим templates.go
добавим поле с токеном в структуру templateData
package main
import (
"html/template"
"path/filepath"
"time"
"snippetbox.alexedwards.net/internal/models"
)
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
Form any
Flash string
IsAuthenticated bool
CSRFToken string // Add a CSRFToken field.
}
Исправим helpers.go
внесем изменения в помощник для всех форм
func (app *application) newTemplateData(r *http.Request) *templateData {
return &templateData{
CurrentYear: time.Now().Year(),
Flash: app.sessionManager.PopString(r.Context(), "flash"),
IsAuthenticated: app.isAuthenticated(r),
CSRFToken: nosurf.Token(r), // Add the CSRF token.
}
}
Исправим все шаблоны и внесем в них скрытые поля
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
Using request context
Мы могли бы сделать эту проверку более надежной, запросив таблицу базы данных наших пользователей, чтобы убедиться, что значение “authenticatedUserID” является реальным, действительным значением (т.е. мы не удаляли учетную запись пользователя с момента его последнего входа в систему).
Поэтому сейчас будем изучать контекст запроса.
Как работает контекст запроса
Синтаксис контекста запроса
Основной код для добавления информации в контекст запроса выглядит следующим образом:
// Где r — это *http.Request...
ctx := r.Context()
ctx = context.WithValue(ctx, "isAuthenticated", true)
r = r.WithContext(ctx)
Давайте разберем это построчно.
- Сначала мы используем метод r.Context(), чтобы получить существующий контекст из запроса и присваиваем его переменной ctx.
- Затем мы используем метод context.WithValue(), чтобы создать новую копию существующего контекста, содержащую ключ “isAuthenticated” и значение true.
- Наконец, мы используем метод r.WithContext(), чтобы создать копию запроса с нашим новым контекстом.
Важно
Обратите внимание, что мы не обновляем контекст запроса напрямую. Мы создаем новую копию объекта http.Request с нашим новым контекстом.
Также стоит отметить, что для ясности я сделал этот фрагмент кода немного более многословным, чем это необходимо. Обычно его пишут так:
ctx = context.WithValue(r.Context(), "isAuthenticated", true)
r = r.WithContext(ctx)
Итак, вот как вы добавляете данные в контекст запроса. Но как их снова извлечь?
Важно объяснить, что за кулисами значения контекста запроса хранятся с типом any. Это означает, что после извлечения их из контекста вам нужно будет привести их к исходному типу перед использованием.
Чтобы извлечь значение, нам нужно использовать метод r.Context().Value(), вот так:
isAuthenticated, ok := r.Context().Value("isAuthenticated").(bool)
if !ok {
return errors.New("не удалось преобразовать значение в bool")
}
В этом коде мы пытаемся получить значение, связанное с ключом “isAuthenticated”, и одновременно проверяем, удалось ли нам привести его к типу bool. Если преобразование не удалось, мы возвращаем ошибку.
Избежание коллизий ключей
В приведенных выше примерах кода я использовал строку “isAuthenticated” в качестве ключа для хранения и извлечения данных из контекста запроса. Однако это не рекомендуется, поскольку существует риск, что другие сторонние пакеты, используемые вашим приложением, также захотят хранить данные с использованием ключа “isAuthenticated”, что приведет к коллизии имен.
Чтобы избежать этого, хорошей практикой является создание собственного пользовательского типа, который вы можете использовать для своих ключей контекста. Расширяя наш пример кода, гораздо лучше сделать что-то вроде этого:
// Объявляем пользовательский тип "contextKey" для ваших ключей контекста.
type contextKey string
// Создаем константу с типом contextKey, которую мы можем использовать.
const isAuthenticatedContextKey = contextKey("isAuthenticated")
...
// Устанавливаем значение в контексте запроса, используя нашу константу isAuthenticatedContextKey в качестве ключа.
ctx := r.Context()
ctx = context.WithValue(ctx, isAuthenticatedContextKey, true)
r = r.WithContext(ctx)
...
// Извлекаем значение из контекста запроса, используя нашу константу в качестве ключа.
isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool)
if !ok {
return errors.New("не удалось преобразовать значение в bool")
}
Таким образом, мы избегаем возможных коллизий ключей, используя пользовательский тип для ключей контекста.
Request context для authentication/authorization
Теперь, когда мы разобрали эти объяснения, давайте начнем использовать функциональность контекста запроса в нашем приложении.
Мы начнем с того, что вернемся к файлу internal/models/users.go и обновим метод UserModel.Exists(), чтобы он возвращал true, если пользователь с определенным ID существует в нашей таблице пользователей, и false в противном случае. Вот так:
Файл: internal/models/users.go
package models
...
func (m *UserModel) Exists(id int) (bool, error) {
var exists bool
stmt := "SELECT EXISTS(SELECT true FROM users WHERE id = ?)"
err := m.DB.QueryRow(stmt, id).Scan(&exists)
return exists, err
}
Затем давайте создадим новый файл cmd/web/context.go. В этом файле мы определим пользовательский тип contextKey и переменную isAuthenticatedContextKey, чтобы у нас был уникальный ключ, который мы можем использовать для хранения и извлечения статуса аутентификации из контекста запроса (без риска коллизий имен).
package main
type contextKey string
const isAuthenticatedContextKey = contextKey("isAuthenticated")
Давайте создадим новый метод промежуточного ПО authenticate(), который:
- Извлекает ID пользователя из данных сессии.
- Проверяет базу данных, чтобы узнать, соответствует ли ID действительному пользователю, используя метод
UserModel.Exists().
- Обновляет контекст запроса, добавляя ключ
isAuthenticatedContextKey со значением true.
Вот код:
Файл: cmd/web/middleware.go
package main
import (
"context" // Новый импорт
"fmt"
"net/http"
"github.com/justinas/nosurf"
)
...
func (app *application) authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Извлекаем значение authenticatedUserID из сессии, используя метод
// GetInt(). Это вернет нулевое значение для int (0), если в сессии
// нет значения "authenticatedUserID" — в этом случае мы
// вызываем следующий обработчик в цепочке как обычно и прерываем обработку.
id := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")
if id == 0 {
next.ServeHTTP(w, r)
return
}
// В противном случае мы проверяем, существует ли пользователь с этим ID
// в нашей базе данных.
exists, err := app.users.Exists(id)
if err != nil {
app.serverError(w, err)
return
}
// Если найден соответствующий пользователь, мы знаем, что запрос
// поступает от аутентифицированного пользователя, который существует
// в нашей базе данных. Мы создаем новую копию запроса (с
// значением isAuthenticatedContextKey равным true в контексте запроса)
// и присваиваем ее переменной r.
if exists {
ctx := context.WithValue(r.Context(), isAuthenticatedContextKey, true)
r = r.WithContext(ctx)
}
// Вызываем следующий обработчик в цепочке.
next.ServeHTTP(w, r)
})
}
Важно подчеркнуть следующее различие:
- Когда у нас нет действительного аутентифицированного пользователя, мы передаем оригинальный и неизмененный *http.Request следующему обработчику в цепочке.
- Когда у нас есть действительный аутентифицированный пользователь, мы создаем копию запроса с ключом
isAuthenticatedContextKey и значением true, хранящимся в контексте запроса. Затем мы передаем эту копию *http.Request следующему обработчику в цепочке.
Хорошо, давайте обновим файл cmd/web/routes.go, чтобы включить промежуточное ПО authenticate() в нашу динамическую цепочку промежуточного ПО.
package main
...
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.notFound(w)
})
fileServer := http.FileServer(http.Dir("./ui/static/"))
router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
// Add the authenticate() middleware to the chain.
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
router.Handler(http.MethodGet, "/user/login", dynamic.ThenFunc(app.userLogin))
router.Handler(http.MethodPost, "/user/login", dynamic.ThenFunc(app.userLoginPost))
protected := dynamic.Append(app.requireAuthentication)
router.Handler(http.MethodGet, "/snippet/create", protected.ThenFunc(app.snippetCreate))
router.Handler(http.MethodPost, "/snippet/create", protected.ThenFunc(app.snippetCreatePost))
router.Handler(http.MethodPost, "/user/logout", protected.ThenFunc(app.userLogoutPost))
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
return standard.Then(router)
}
Последнее, что нам нужно сделать, — это обновить наш вспомогательный метод isAuthenticated(), чтобы вместо проверки данных сессии он теперь проверял контекст запроса, чтобы определить, аутентифицирован ли пользователь или нет.
Мы можем сделать это следующим образом:
Файл: cmd/web/helpers.go
package main
...
func (app *application) isAuthenticated(r *http.Request) bool {
isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool)
if !ok {
return false
}
return isAuthenticated
}
Важно отметить, что если в контексте запроса нет значения с ключом isAuthenticatedContextKey или если подлежащие значение не является типом bool, то это приведение типов завершится неудачей. В этом случае мы принимаем «безопасный» запасной вариант и возвращаем false (т.е. предполагаем, что пользователь не аутентифицирован).
Если хотите, попробуйте снова запустить приложение. Оно должно корректно скомпилироваться, и если вы войдете как определенный пользователь и будете перемещаться по приложению, оно должно работать точно так же, как и раньше.
Дополнительная информация
Неправильное использование контекста запроса
Важно подчеркнуть, что контекст запроса следует использовать только для хранения информации, относящейся к времени жизни конкретного запроса. Документация Go для context.Context предупреждает:
Используйте значения контекста только для данных, ограниченных запросом, которые передаются между процессами и API.
Это означает, что не следует использовать его для передачи зависимостей, которые существуют вне времени жизни запроса — таких как логгеры, кэши шаблонов и пул соединений с базой данных — в ваше промежуточное ПО и обработчики.
По причинам безопасности типов и ясности кода почти всегда лучше явно предоставлять эти зависимости вашим обработчикам, либо делая ваши обработчики методами структуры приложения (как мы делаем в этой книге), либо передавая их в замыкании.
9.3 - Alex Edwards Let's Go часть 3
edwardsLetsGo учебный материал по написанию приложения на Go с авторизацией пользователя. Краткий конспект с основными мыслями (глава с 13)
Дополнительные возможности Go
В этом разделе книги мы поговорим о двух функциях Go, которые являются относительно новыми дополнениями к языку: встраивании файлов и обобщениях.
По сути:
- Встраивание файлов позволяет встраивать внешние файлы непосредственно в вашу программу на Go.
- Обобщения могут помочь сократить количество шаблонного кода, который вам нужно писать, при этом сохраняя безопасность типов на этапе компиляции.
Использование встроенных файлов
Одной из ключевых функций релиза Go 1.16 стал пакет embed, который позволяет встраивать внешние файлы непосредственно в вашу программу на Go.
Эта функция действительно полезна, поскольку она позволяет создавать (а затем и распространять) программы на Go, которые полностью самодостаточны и содержат все необходимое для их работы в составе исполняемого бинарного файла.
Чтобы проиллюстрировать, как использовать пакет embed, мы обновим наше приложение, чтобы встроить и использовать файлы из нашей существующей директории ui (которая содержит наши статические файлы CSS/JavaScript/изображения и HTML-шаблоны).
Если вы хотите следовать за нами, сначала создайте новый файл ui/efs.go:
package ui
import (
"embed"
)
//go:embed "html" "static"
var Files embed.FS
Ключевая строка здесь — //go:embed "html" "static".
Хотя это выглядит как комментарий, на самом деле это специальная директива. При компиляции приложения она указывает Go сохранить файлы из папок ui/html и ui/static в embedded-файловой системе embed.FS, связанной с глобальной переменной Files.
Вот несколько важных деталей, которые нужно учитывать:
-
Расположение директивы
Директива go:embed должна находиться непосредственно перед переменной, в которую нужно встроить файлы.
-
Формат директивы
Общий формат: go:embed <пути>. Можно указывать несколько путей в одной директиве (как в примере выше). Пути должны быть относительными к файлу с исходным кодом, содержащему директиву. В нашем случае go:embed "static" "html" встраивает папки ui/static и ui/html.
-
Область применения
Директива go:embed работает только с глобальными переменными на уровне пакета, но не внутри функций или методов. При попытке использовать её внутри функции возникнет ошибка компиляции: "go:embed cannot apply to var inside func".
-
Ограничения путей
- Пути не могут содержать
. или ...
- Они не должны начинаться или заканчиваться на
/.
- Это ограничивает встраивание файлами, находящимися в той же директории (или её поддиректориях), что и исходный файл с директивой.
-
Встраивание директорий
Если путь ведёт к папке, все её файлы встраиваются рекурсивно, кроме файлов, имена которых начинаются с . или _. Чтобы включить такие файлы, используйте префикс all:, например:
-
Разделитель путей
Всегда используйте прямой слеш (/), даже в Windows.
-
Корневая директория embedded-файловой системы
Она всегда соответствует расположению файла с директивой go:embed. В нашем примере Files содержит файловую систему embed.FS, корнем которой является папка ui.
Использование статических файлов
Давайте изменим наше приложение так, чтобы оно обслуживало наши статические файлы CSS, JavaScript и изображения из встроенной файловой системы, а не считывало их с диска во время выполнения.
Откройте файл cmd/web/routes.go и обновите его следующим образом:
Файл: cmd/web/routes.go
package main
import (
"net/http"
"snippetbox.alexedwards.net/ui" // Новый импорт
"github.com/julienschmidt/httprouter"
"github.com/justinas/alice"
)
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.notFound(w)
})
// Берем встроенную файловую систему ui.Files и преобразуем ее в тип http.FS, чтобы
// она соответствовала интерфейсу http.FileSystem. Затем передаем это в функцию
// http.FileServer() для создания обработчика файлового сервера.
fileServer := http.FileServer(http.FS(ui.Files))
// Наши статические файлы содержатся в папке "static" встроенной файловой системы ui.Files.
// Например, наш CSS-стилист находится по адресу "static/css/main.css". Это означает, что
// нам больше не нужно удалять префикс из URL запроса — любые запросы, начинающиеся с /static/,
// могут просто передаваться непосредственно файловому серверу, и соответствующий статический
// файл будет обслужен (если он существует).
router.Handler(http.MethodGet, "/static/*filepath", fileServer)
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
router.Handler(http.MethodGet, "/user/login", dynamic.ThenFunc(app.userLogin))
router.Handler(http.MethodPost, "/user/login", dynamic.ThenFunc(app.userLoginPost))
protected := dynamic.Append(app.requireAuthentication)
router.Handler(http.MethodGet, "/snippet/create", protected.ThenFunc(app.snippetCreate))
router.Handler(http.MethodPost, "/snippet/create", protected.ThenFunc(app.snippetCreatePost))
router.Handler(http.MethodPost, "/user/logout", protected.ThenFunc(app.userLogoutPost))
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
return standard.Then(router)
}
Если вы сохраните файлы и затем перезапустите приложение, вы должны увидеть, что все компилируется и работает корректно. Когда вы посетите https://localhost:4000 в вашем браузере, статические файлы должны обслуживаться из встроенной файловой системы, и все должно выглядеть нормально.
Встраивание HTML-шаблонов
Теперь давайте обновим файл cmd/web/templates.go, чтобы наш кэш шаблонов использовал встроенные HTML-шаблоны из ui.Files, а не те, что находятся на диске.
Чтобы помочь нам с этим, мы воспользуемся несколькими специальными функциями, которые Go 1.16 представил для работы с встроенными файловыми системами:
fs.Glob() возвращает срез путей файлов, соответствующих шаблону glob. Это фактически то же самое, что и функция filepath.Glob(), которую мы использовали ранее в книге, за исключением того, что она работает с встроенными файловыми системами.
Template.ParseFS() можно использовать для разбора HTML-шаблонов из встроенной файловой системы в набор шаблонов. Это фактически замена как для методов Template.ParseFiles(), так и Template.ParseGlob(), которые мы использовали ранее. Template.ParseFiles() также является функцией с переменным числом аргументов, что позволяет вам разбирать несколько шаблонов за один вызов ParseFiles().
Давайте применим это в нашем файле cmd/web/templates.go:
Файл: cmd/web/templates.go
package main
import (
"html/template"
"io/fs" // Новый импорт
"path/filepath"
"time"
"snippetbox.alexedwards.net/internal/models"
"snippetbox.alexedwards.net/ui" // Новый импорт
)
...
func newTemplateCache() (map[string]*template.Template, error) {
cache := map[string]*template.Template{}
// Используем fs.Glob() для получения среза всех путей файлов в
// встроенной файловой системе ui.Files, которые соответствуют шаблону
// 'html/pages/*.tmpl'. Это фактически дает нам срез всех 'page' шаблонов
// для приложения, как и раньше.
pages, err := fs.Glob(ui.Files, "html/pages/*.tmpl")
if err != nil {
return nil, err
}
for _, page := range pages {
name := filepath.Base(page)
// Создаем срез, содержащий шаблоны, которые мы хотим разобрать.
patterns := []string{
"html/base.tmpl",
"html/partials/*.tmpl",
page,
}
// Используем ParseFS() вместо ParseFiles() для разбора файлов шаблонов
// из встроенной файловой системы ui.Files.
ts, err := template.New(name).Funcs(functions).ParseFS(ui.Files, patterns...)
if err != nil {
return nil, err
}
cache[name] = ts
}
return cache, nil
}
Теперь, когда это сделано, когда наше приложение будет собрано в бинарный файл, оно будет содержать все необходимые UI-файлы для работы.
Вы можете быстро попробовать это, собрав исполняемый бинарный файл в вашей директории /tmp, скопировав сертификаты TLS и запустив бинарный файл. Вот так:
go build -o /tmp/web ./cmd/web/
cp -r ./tls /tmp/
cd /tmp/
./web
Использование обобщений
Go 1.18 — это первая версия языка, которая поддерживает обобщения, также известные под более техническим названием параметрический полиморфизм.
В общем, новая функциональность обобщений позволяет вам писать код, который работает с различными конкретными типами.
Например, в более ранних версиях Go, если вы хотели проверить, содержится ли определенное значение в срезе []string и срезе []int, вам нужно было бы написать две отдельные функции — одну для типа строк и другую для типа целых чисел. Это выглядело бы примерно так:
func containsString(v string, s []string) bool {
for i, vs := range s {
if v == vs {
return true
}
}
return false
}
func containsInt(v int, s []int) bool {
for i, vs := range s {
if v == vs {
return true
}
}
return false
}
Теперь, с обобщениями, возможно написать одну единственную функцию contains(), которая будет работать для строк, целых чисел и всех других сопоставимых типов. Код выглядит так:
func contains[T comparable](v T, s []T) bool {
for i := range s {
if v == s[i] {
return true
}
}
return false
}
Когда использовать обобщения?
Вы можете рассмотреть возможность использования обобщений, когда:
- Вы часто пишете повторяющийся шаблонный код для различных типов данных. Примеры этого могут включать общие операции над срезами, картами или каналами — или вспомогательные функции для выполнения проверок валидации или тестовых утверждений для различных типов данных.
- Вы пишете код и часто обращаетесь к типу
any (пустой интерфейс). Примером этого может быть создание структуры данных (например, очереди, кэша или связного списка), которая должна работать с различными типами.
В то же время, вероятно, вам не следует использовать обобщения:
- Если это делает ваш код труднее для понимания или менее ясным.
- Если все типы, с которыми вам нужно работать, имеют общий набор методов — в этом случае лучше определить и использовать обычный интерфейс.
- Просто потому, что вы можете. В первую очередь пишите простой не обобщенный код, и переходите к обобщенной версии только в том случае, если это действительно необходимо.
Использование обобщений в нашем приложении
В следующем разделе книги мы начнем писать тесты для нашего приложения, и в процессе этого мы сгенерируем много повторяющегося шаблонного кода. Мы воспользуемся функциональностью обобщений Go, чтобы помочь нам управлять этим и создать несколько универсальных вспомогательных функций для выполнения проверок тестов на различных типах данных.
Возможно, единственное, что действительно подходит для обобщений, — это функция PermittedInt() в нашем файле internal/validator/validator.go.
Давайте изменим ее на универсальную функцию PermittedValue(), которую мы сможем использовать каждый раз, когда захотим проверить, находится ли предоставленное пользователем значение в наборе разрешенных значений — независимо от того, является ли предоставленное пользователем значение строкой, целым числом, float64 или любым другим сопоставимым типом.
Вот так:
Вот технический перевод документации с соблюдением терминологии языка Go:
Файл: internal/validator/validator.go
package validator
// Замените функцию PermittedInt() на универсальную функцию PermittedValue().
// Эта функция возвращает true, если значение типа T равно одному из
// параметров variadic permittedValues.
func PermittedValue[T comparable](value T, permittedValues ...T) bool {
for i := range permittedValues {
if value == permittedValues[i] {
return true
}
}
return false
}
Затем мы можем обновить обработчик snippetCreatePost, чтобы использовать новую функцию PermittedValue() в проверках валидации, следующим образом:
Файл: cmd/web/handlers.go
package main
func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
var form snippetCreateForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.Title), "title", "Это поле не может быть пустым")
form.CheckField(validator.MaxChars(form.Title, 100), "title", "Это поле не может содержать более 100 символов")
form.CheckField(validator.NotBlank(form.Content), "content", "Это поле не может быть пустым")
// Используйте универсальную функцию PermittedValue() вместо
// специфичной для типа функции PermittedInt().
form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "Это поле должно равняться 1, 7 или 365")
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data)
return
}
id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)
if err != nil {
app.serverError(w, err)
return
}
app.sessionManager.Put(r.Context(), "flash", "Сниппет успешно создан!")
http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}
После внесения этих изменений вы должны убедиться, что ваше приложение компилируется корректно и продолжает функционировать так же, как и раньше.
9.4 - Alex Edwards Let's Go часть 4
edwardsLetsGo учебный материал по написанию приложения на Go с авторизацией пользователя. Краткий конспект с основными мыслями (глава 14) Тестирование
Тестирование
И вот мы наконец-то подошли к теме тестирования.
Как и структурирование и организация кода приложения, нет единственного “правильного” способа структурировать и организовать тесты в Go. Но есть некоторые соглашения, шаблоны и лучшие практики, которые вы можете соблюдать.
В этом разделе мы добавим тесты для части кода нашего приложения с целью продемонстрировать общий синтаксис для создания тестов и проиллюстрировать некоторые шаблоны, которые можно повторно использовать в широком спектре приложений.
Вы узнаете:
- Как создавать и запускать табличные юнит-тесты и подтесты в Go.
- Как тестировать HTTP-обработчики и middleware.
- Как проводить сквозное тестирование маршрутов веб-приложения, middleware и обработчиков.
- Как создавать мок-объекты моделей базы данных и использовать их в юнит-тестах.
- Шаблон для тестирования отправки HTML-форм с CSRF-защитой.
- Как использовать тестовый экземпляр MySQL для интеграционного тестирования.
- Как легко рассчитывать и профилировать покрытие кода тестами.
Юнит-тестирование и подтесты
В этой главе мы создадим юнит-тест, чтобы убедиться, что наша функция humanDate() (которую мы создали в главе о пользовательских функциях шаблонизации) выводит значения типа time.Time в точно таком формате, как нам нужно.
Если вы не помните, функция humanDate() выглядит так:
Файл: cmd/web/templates.go
package main
...
func humanDate(t time.Time) string {
return t.UTC().Format("02 Jan 2006 at 15:04")
}
...
Создание юнит-теста
Давайте сразу же создадим юнит-тест для этой функции.
В Go общепринятой практикой является создание тестов в файлах *_test.go, которые находятся непосредственно рядом с кодом, который вы тестируете. Итак, в этом случае первым делом мы создадим новый файл cmd/web/templates_test.go для хранения теста:
touch cmd/web/templates_test.go
Файл: cmd/web/templates_test.go
package main
import (
"testing"
"time"
)
func TestHumanDate(t *testing.T) {
// Initialize a new time.Time object and pass it to the humanDate function.
tm := time.Date(2022, 3, 17, 10, 15, 0, 0, time.UTC)
hd := humanDate(tm)
// Check that the output from the humanDate function is in the format we
// expect. If it isn't what we expect, use the t.Errorf() function to
// indicate that the test has failed and log the expected and actual
// values.
if hd != "17 Mar 2022 at 10:15" {
t.Errorf("got %q; want %q", hd, "17 Mar 2022 at 10:15")
}
}
Этот шаблон является базовым, который вы будете использовать почти для всех тестов, которые пишете на Go.
Важно запомнить:
-
Тест является обычным кодом на Go, который вызывает функцию humanDate() и проверяет, что результат соответствует ожидаемому.
-
Ваши юнит-тесты содержатся в обычной функции Go с сигнатурой func(*testing.T).
-
Чтобы быть действительным юнит-тестом, имя этой функции должно начинаться с слова Test. Обычно за ним следует имя функции, метода или типа, который вы тестируете, чтобы сделать очевидным, что тестируется.
-
Вы можете использовать функцию t.Errorf(), чтобы пометить тест как не прошедший и зарегистрировать описательное сообщение о сбое. Важно отметить, что вызов t.Errorf() не останавливает выполнение вашего теста — после вызова Go продолжит выполнять любой оставшийся тестовый код как обычно.
Давайте попробуем это. Сохраните файл, затем используйте команду go test, чтобы запустить все тесты в нашем пакете cmd/web:
$ go test ./cmd/web
ok snippetbox.igorra.net/cmd/web 0.002s
Итак, это хорошо. ok в этом выводе указывает, что все тесты в пакете (пока только наш тест TestHumanDate()) прошли без проблем.
Если вы хотите получить больше информации, вы можете увидеть, какие тесты выполняются, используя флаг -v, чтобы получить подробный вывод:
$ go test -v ./cmd/web
=== RUN TestHumanDate
--- PASS: TestHumanDate (0.00s)
PASS
ok snippetbox.igorra.net/cmd/web 0.002s
Табличные тесты
Теперь давайте расширим нашу функцию TestHumanDate(), чтобы охватить некоторые дополнительные тестовые случаи. В частности, мы обновим ее, чтобы также проверить, что:
-
Если входные данные для humanDate() являются нулевым временем, то она возвращает пустую строку “”.
-
Вывод функции humanDate() всегда использует часовой пояс UTC.
В Go идиоматическим способом запуска нескольких тестовых случаев является использование табличных тестов.
По сути, идея табличных тестов заключается в создании «таблицы» тестовых случаев, содержащих входные данные и ожидаемые выходные данные, а затем в итерации по ним, запуская каждый тестовый случай в подтесте. Есть несколько способов настроить это, но распространенным подходом является определение тестовых случаев в срезе анонимных структур.
Я продемонстрирую:
Файл: cmd/web/templates_test.go
package main
import (
"testing"
"time"
)
func TestHumanDate(t *testing.T) {
// Создаем срез анонимных структур, содержащих имя тестового случая,
// входные данные для нашей функции humanDate() (поле tm) и ожидаемый вывод
// (поле want).
tests := []struct {
name string
tm time.Time
want string
}{
{
name: "UTC",
tm: time.Date(2022, 3, 17, 10, 15, 0, 0, time.UTC),
want: "17 Mar 2022 at 10:15",
},
{
name: "Empty",
tm: time.Time{},
want: "",
},
{
name: "CET",
tm: time.Date(2022, 3, 17, 10, 15, 0, 0, time.FixedZone("CET", 1*60*60)),
want: "17 Mar 2022 at 09:15",
},
}
// Итерируем по тестовым случаям.
for _, tt := range tests {
// Используем функцию t.Run(), чтобы запустить подтест для каждого тестового случая.
// Первый параметр — имя теста (которое используется для идентификации подтеста в любом выводе журнала),
// а второй параметр — анонимная функция, содержащая фактический тест для каждого случая.
t.Run(tt.name, func(t *testing.T) {
hd := humanDate(tt.tm)
if hd != tt.want {
t.Errorf("got %q; want %q", hd, tt.want)
}
})
}
}
Примечание: В третьем тестовом случае мы используем CET (Центральноевропейское время) в качестве часового пояса, который на один час опережает UTC. Итак, мы хотим, чтобы вывод humanDate() (в UTC) был “17 Mar 2022 at 09:15”, а не “17 Mar 2022 at 10:15”.
Хорошо, давайте запустим это и посмотрим, что произойдет:
$ go test -v ./cmd/web
=== RUN TestHumanDate
=== RUN TestHumanDate/UTC
=== RUN TestHumanDate/Empty
templates_test.go:44: got "01 Jan 0001 at 00:00"; want ""
=== RUN TestHumanDate/CET
templates_test.go:44: got "17 Mar 2022 at 10:15"; want "17 Mar 2022 at 09:15"
--- FAIL: TestHumanDate (0.00s)
--- PASS: TestHumanDate/UTC (0.00s)
--- FAIL: TestHumanDate/Empty (0.00s)
--- FAIL: TestHumanDate/CET (0.00s)
FAIL
FAIL snippetbox.alexedwards.net/cmd/web 0.003s
FAIL
Итак, здесь мы можем видеть индивидуальный вывод для каждого из наших подтестов. Как вы, возможно, догадались, наш первый тестовый случай прошел, но Empty и CET-тесты оба не прошли. Заметим, как для неудавшихся тестовых случаев мы получаем соответствующее сообщение об ошибке и имя файла и номер строки в выводе?
Давайте вернемся к нашей функции humanDate() и обновим ее, чтобы исправить эти две проблемы:
Файл: cmd/web/templates.go
package main
...
func humanDate(t time.Time) string {
// Возвращаем пустую строку, если время имеет нулевое значение.
if t.IsZero() {
return ""
}
// Преобразуем время в UTC, прежде чем форматировать его.
return t.UTC().Format("02 Jan 2006 at 15:04")
}
...
И когда вы повторно запустите тесты,
$ go test -v ./cmd/web
=== RUN TestHumanDate
=== RUN TestHumanDate/UTC
=== RUN TestHumanDate/Empty
=== RUN TestHumanDate/CET
--- PASS: TestHumanDate (0.00s)
--- PASS: TestHumanDate/UTC (0.00s)
--- PASS: TestHumanDate/Empty (0.00s)
--- PASS: TestHumanDate/CET (0.00s)
PASS
ok snippetbox.alexedwards.net/cmd/web 0.003s
Вспомогательные функции для тестовых утверждений
Как я упоминал ранее в книге, в течение следующих нескольких глав мы будем писать много тестовых утверждений, которые являются вариацией этого шаблона:
if actualValue != expectedValue {
t.Errorf("got %v; want %v", actualValue, expectedValue)
}
Давайте быстро абстрагируем этот код в вспомогательную функцию.
Если вы следите за ходом событий, создайте новый пакет internal/assert:
$ mkdir internal/assert
$ touch internal/assert/assert.go
И затем добавьте следующий код:
Файл: internal/assert/assert.go
package assert
import (
"testing"
)
func Equal[T comparable](t *testing.T, actual, expected T) {
t.Helper()
if actual != expected {
t.Errorf("got: %v; want: %v", actual, expected)
}
}
Заметим, как мы настроили это так, что Equal() является универсальной функцией? Это означает, что мы сможем использовать ее независимо от типа фактических и ожидаемых значений. Пока фактические и ожидаемые значения имеют один и тот же тип (например, они оба являются строковыми значениями или оба являются целыми числами), наш тестовый код должен компилироваться и работать нормально, когда мы вызываем Equal().
Примечание
Функция t.Helper(), которую мы используем в приведенном выше коде, указывает Go-тест, что наша функция Equal() является тестовым помощником. Это означает, что когда t.Errorf() вызывается из нашей функции Equal(), Go-тест будет сообщать имя файла и номер строки кода, который вызвал нашу функцию Equal(), в выводе.
Без t.Helper() вывод теста будет указывать на строку внутри функции Equal(), где была вызвана t.Errorf(), а не на строку в фактическом тестовом коде, где была вызвана Equal(). Это может затруднить отладку и понимание, что именно пошло не так.
Используя t.Helper(), мы можем получить более точную и полезную информацию об ошибках в наших тестах.
package main
import (
"testing"
"time"
"snippetbox.alexedwards.net/internal/assert" // New import
)
func TestHumanDate(t *testing.T) {
tests := []struct {
name string
tm time.Time
want string
}{
{
name: "UTC",
tm: time.Date(2022, 3, 17, 10, 15, 0, 0, time.UTC),
want: "17 Mar 2022 at 10:15",
},
{
name: "Empty",
tm: time.Time{},
want: "",
},
{
name: "CET",
tm: time.Date(2022, 3, 17, 10, 15, 0, 0, time.FixedZone("CET", 1*60*60)),
want: "17 Mar 2022 at 09:15",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hd := humanDate(tt.tm)
// Use the new assert.Equal() helper to compare the expected and
// actual values.
assert.Equal(t, hd, tt.want)
})
}
}
Дополнительная информация
Субтесты без таблицы тестовых случаев
Важно отметить, что вам не нужно использовать субтесты в сочетании с табличными тестами (как мы делали до сих пор в этой главе). Полностью допустимо выполнять субтесты, вызывая t.Run() последовательно в ваших тестовых функциях, подобно этому:
func TestExample(t *testing.T) {
t.Run("Example sub-test 1", func(t *testing.T) {
// Выполняем один тест.
})
t.Run("Example sub-test 2", func(t *testing.T) {
// Выполняем другой тест.
})
t.Run("Example sub-test 3", func(t *testing.T) {
// И еще один...
})
}
Этот подход может быть полезен, когда у вас есть несколько независимых тестов, которые не связаны между собой таблицей тестовых случаев. Однако табличные тесты часто более удобны и кратки, когда у вас есть много тестов с похожими входными и выходными данными.
Тестирование HTTP-обработчиков и middleware
Давайте перейдем к конкретным техникам юнит-тестирования HTTP-обработчиков.
Все обработчики, которые мы написали для этого проекта, пока что довольно сложны для тестирования. Чтобы проще разобраться в теме, начнем с чего-то более простого.
Простой пример: обработчик ping
Откройте файл handlers.go и создайте новый обработчик ping, который:
- Возвращает статус-код 200 OK
- Отправляет тело ответа “OK”
Такой обработчик может использоваться для проверки работоспособности сервера (например, в мониторинге uptime).
package main
...
func ping(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
В этой главе мы создадим юнит-тест TestPing, который проверит:
- Что обработчик ping возвращает HTTP-статус 200.
- Что тело ответа содержит строго “OK”.
Запись ответов
Для помощи в тестировании ваших обработчиков HTTP пакет net/http/httptest в Go предоставляет набор полезных инструментов.
Одним из этих инструментов является тип httptest.ResponseRecorder. По сути, это реализация http.ResponseWriter, которая записывает код состояния ответа, заголовки и тело вместо того, чтобы фактически записывать их в соединение HTTP.
Таким образом, простым способом тестирования ваших обработчиков является создание нового объекта httptest.ResponseRecorder, передача его в функцию обработчика, а затем проверка его после возврата обработчика.
Давайте попробуем сделать именно это, чтобы протестировать функцию обработчика ping.
Сначала, следуя соглашениям Go, создайте новый файл handlers_test.go,
$ touch cmd/web/handlers_test.go
Затем добавьте следующий код:
Файл: cmd/web/handlers_test.go
package main
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
"snippetbox.igorra.net/internal/assert"
)
func TestPing(t *testing.T) {
// Инициализируйте новый `httptest.ResponseRecorder`.
rr := httptest.NewRecorder()
// Инициализируйте новый фиктивный `http.Request`.
r, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}
// Вызовите функцию обработчика `ping`, передав `httptest.ResponseRecorder` и `http.Request`.
ping(rr, r)
// Вызовите метод `Result()` для `http.ResponseRecorder`, чтобы получить `http.Response`, сгенерированный обработчиком `ping`.
rs := rr.Result()
// Проверьте, что код состояния, записанный обработчиком `ping`, был 200.
assert.Equal(t, rs.StatusCode, http.StatusOK)
// И мы можем проверить, что тело ответа, записанное обработчиком `ping`, равно "OK".
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
bytes.TrimSpace(body)
assert.Equal(t, string(body), "OK")
}
Примечание
В приведенном выше коде мы используем функцию t.Fatal() в нескольких местах для обработки ситуаций, когда в нашем тестовом коде возникает неожиданная ошибка. При вызове t.Fatal() пометит тест как неудачный, запишет ошибку и затем полностью остановит выполнение текущего теста (или подтеста).
Обычно следует вызывать t.Fatal() в ситуациях, когда не имеет смысла продолжать текущий тест — например, при ошибке во время шага настройки или когда неожиданная ошибка от функции стандартной библиотеки Go означает, что вы не можете продолжить тест.
Сохраните файл, затем попробуйте запустить go test снова с установленным флагом verbose. Вот так:
go test -v ./cmd/web
=== RUN TestPing
--- PASS: TestPing (0.00s)
=== RUN TestHumanDate
=== RUN TestHumanDate/UTC
=== RUN TestHumanDate/Empty
=== RUN TestHumanDate/CET
--- PASS: TestHumanDate (0.00s)
--- PASS: TestHumanDate/UTC (0.00s)
--- PASS: TestHumanDate/Empty (0.00s)
--- PASS: TestHumanDate/CET (0.00s)
PASS
ok snippetbox.igorra.net/cmd/web 0.002s
Тестирование middleware
Также возможно использовать тот же общий шаблон для модульного тестирования вашего middleware.
Мы продемонстрируем это, создав новый тест TestSecureHeaders для middleware secureHeaders(), который мы создали ранее в книге. В рамках этого теста мы хотим проверить, что:
Middleware `secureHeaders()` устанавливает все ожидаемые заголовки в ответе HTTP.
Middleware `secureHeaders()` корректно вызывает следующий обработчик в цепочке.
Сначала вам нужно создать файл cmd/web/middleware_test.go, чтобы содержать тест:
$ touch cmd/web/middleware_test.go
Затем добавьте следующий код:
Файл: cmd/web/middleware_test.go
package main
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
"snippetbox.alexedwards.net/internal/assert"
)
func TestSecureHeaders(t *testing.T) {
// Инициализируйте новый `httptest.ResponseRecorder` и фиктивный `http.Request`.
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}
// Создайте фиктивный обработчик HTTP, который можно передать нашему middleware `secureHeaders`,
// который пишет код состояния 200 и тело ответа "OK".
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
// Передайте фиктивный обработчик HTTP нашему middleware `secureHeaders`. Поскольку
// `secureHeaders` *возвращает* `http.Handler`, мы можем вызвать его метод `ServeHTTP()`,
// передав `http.ResponseRecorder` и фиктивный `http.Request`, чтобы выполнить его.
secureHeaders(next).ServeHTTP(rr, r)
// Вызовите метод `Result()` для `http.ResponseRecorder`, чтобы получить результаты теста.
rs := rr.Result()
// Проверьте, что middleware корректно установил заголовок `Content-Security-Policy` в ответе.
expectedValue := "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com"
assert.Equal(t, rs.Header.Get("Content-Security-Policy"), expectedValue)
// Проверьте, что middleware корректно установил заголовок `Referrer-Policy` в ответе.
expectedValue = "origin-when-cross-origin"
assert.Equal(t, rs.Header.Get("Referrer-Policy"), expectedValue)
// Проверьте, что middleware корректно установил заголовок `X-Content-Type-Options` в ответе.
expectedValue = "nosniff"
assert.Equal(t, rs.Header.Get("X-Content-Type-Options"), expectedValue)
// Проверьте, что middleware корректно установил заголовок `X-Frame-Options` в ответе.
expectedValue = "deny"
assert.Equal(t, rs.Header.Get("X-Frame-Options"), expectedValue)
// Проверьте, что middleware корректно установил заголовок `X-XSS-Protection` в ответе.
expectedValue = "0"
assert.Equal(t, rs.Header.Get("X-XSS-Protection"), expectedValue)
// Проверьте, что middleware корректно вызвал следующий обработчик в цепочке,
// и код состояния ответа и тело ответа соответствуют ожиданиям.
assert.Equal(t, rs.StatusCode, http.StatusOK)
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
bytes.TrimSpace(body)
assert.Equal(t, string(body), "OK")
}
Если вы запустите тесты сейчас, вы должны увидеть, что тест TestSecureHeaders проходит без каких-либо проблем.
go test -v ./cmd/web
=== RUN TestPing
--- PASS: TestPing (0.00s)
=== RUN TestSecureHeaders
--- PASS: TestSecureHeaders (0.00s)
=== RUN TestHumanDate
=== RUN TestHumanDate/UTC
=== RUN TestHumanDate/Empty
=== RUN TestHumanDate/CET
--- PASS: TestHumanDate (0.00s)
--- PASS: TestHumanDate/UTC (0.00s)
--- PASS: TestHumanDate/Empty (0.00s)
--- PASS: TestHumanDate/CET (0.00s)
PASS
ok snippetbox.igorra.net/cmd/web 0.002s
End-to-end testing (Сквозное тестирование)
В предыдущей главе мы обсудили общий шаблон для модульного тестирования ваших обработчиков HTTP в изоляции.
Но — в большинстве случаев — ваши обработчики HTTP не используются в изоляции. Итак, в этой главе мы объясним, как запустить сквозные тесты на вашем веб-приложении, которые охватывают маршрутизацию, middleware и обработчики.
Чтобы проиллюстрировать это, мы адаптируем нашу функцию TestPing так, чтобы она запускала сквозной тест на нашем коде. В частности, мы хотим, чтобы тест гарантировал, что GET-запрос к /ping нашему приложению вызывает функцию обработчика ping и приводит к статусу 200 OK и телу ответа “OK”.
По сути, мы хотим проверить, что наше приложение имеет маршрут, подобный этому:
| Метод |
Шаблон |
Обработчик |
Действие |
| GET |
/ping |
ping |
Вернуть ответ 200 OK |
Использование httptest.Server
Ключом к сквозному тестированию нашего приложения является функция httptest.NewTLSServer(), которая создает экземпляр httptest.Server, которому можно отправлять HTTPS-запросы.
С учетом этого, вернитесь к вашему файлу handlers_test.go и обновите тест TestPing так, чтобы он выглядел следующим образом:
Файл: cmd/web/handlers_test.go
package main
import (
"bytes"
"io"
"log" // Новый импорт
"net/http"
"net/http/httptest"
"testing"
"snippetbox.alexedwards.net/internal/assert"
)
func TestPing(t *testing.T) {
// Создайте новый экземпляр нашей структуры приложения. На данный момент это просто
// содержит пару фиктивных логгеров (которые игнорируют все, что пишется в них).
app := &application{
errorLog: log.New(io.Discard, "", 0),
infoLog: log.New(io.Discard, "", 0),
}
// Затем мы используем функцию `httptest.NewTLSServer()`, чтобы создать новый тестовый
// сервер, передав значение, возвращенное нашим методом `app.routes()`, в качестве
// обработчика для сервера. Это запускает HTTPS-сервер, который слушает на
// случайно выбранном порту вашей локальной машины в течение теста.
// Обратите внимание, что мы откладываем вызов `ts.Close()`, чтобы сервер был закрыт
// после завершения теста.
ts := httptest.NewTLSServer(app.routes())
defer ts.Close()
// Адрес сети, на котором тестовый сервер слушает, содержится в поле `ts.URL`.
// Мы можем использовать это вместе с методом `ts.Client().Get()`, чтобы отправить
// GET-запрос к `/ping` тестовому серверу. Это возвращает структуру `http.Response`,
// содержащую ответ.
rs, err := ts.Client().Get(ts.URL + "/ping")
if err != nil {
t.Fatal(err)
}
// Затем мы можем проверить значение кода состояния ответа и тела, используя
// тот же шаблон, что и раньше.
assert.Equal(t, rs.StatusCode, http.StatusOK)
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
bytes.TrimSpace(body)
assert.Equal(t, string(body), "OK")
}
Есть несколько вещей в этом коде, на которые стоит обратить внимание и обсудить.
- Когда мы вызываем
httptest.NewTLSServer(), чтобы инициализировать тестовый сервер, нам нужно передать http.Handler в качестве параметра — и этот обработчик вызывается каждый раз, когда тестовый сервер получает HTTPS-запрос. В нашем случае мы передали возвращаемое значение из нашего метода app.routes(), что означает, что запрос к тестовому серверу будет использовать все наши реальные маршруты приложения, middleware и обработчики.
- Это большое преимущество работы, которую мы проделали ранее в книге, чтобы изолировать всю маршрутизацию нашего приложения в методе
app.routes().
- Если вы тестируете HTTP-сервер (а не HTTPS), вы должны использовать функцию
httptest.NewServer(), чтобы создать тестовый сервер.
- Метод
ts.Client() возвращает клиент тестового сервера — который имеет тип http.Client — и мы всегда должны использовать этого клиента, чтобы отправлять запросы к тестовому серверу. Возможно настроить клиента, чтобы изменить его поведение, и мы объясним, как это сделать, в конце этой главы.
- Вы можете задаться вопросом, почему мы установили поля
errorLog и infoLog нашей структуры приложения, но не все остальные поля. Причина этого в том, что логгеры необходимы middleware logRequest и recoverPanic, которые используются нашим приложением на каждом маршруте. Попытка запустить этот тест без установки этих двух зависимостей приведет к панике.
В любом случае, давайте попробуем новый тест:
go test -v ./cmd/web
=== RUN TestPing
handlers_test.go:42: got: 404; want: 200
handlers_test.go:51: got: Not Found
; want: OK
--- FAIL: TestPing (0.00s)
Из тестового вывода мы видим, что ответ на наш запрос GET /ping имеет код состояния 404, а не 200, как мы ожидали. И это потому, что мы еще не зарегистрировали маршрут GET /ping с помощью нашего маршрутизатора. Внесем правку в файл File: cmd/web/routes.go
router.HandlerFunc(http.MethodGet, "/ping", ping)
Итог:
go test -v ./cmd/web
=== RUN TestPing
--- PASS: TestPing (0.00s)
ok snippetbox.igorra.net/cmd/web 0.005s
Использование тестовых помощников
Наш тест TestPing теперь работает хорошо. Но есть хорошая возможность выделить некоторые части этого кода в функции-помощники, которые мы можем повторно использовать, добавляя больше сквозных тестов к нашему проекту.
Нет жестких и быстрых правил о том, где размещать методы-помощники для тестов. Если помощник используется только в определенном файле *_test.go, то, вероятно, имеет смысл включить его встроенным в этот файл рядом с вашими тестами. С другой стороны, если вы собираетесь использовать помощник в тестах в нескольких пакетах, то вы можете поместить его в повторно используемый пакет, называемый internal/testutils (или аналогичный), который может быть импортирован вашими тестовыми файлами.
В нашем случае помощники будут использоваться только в нашем пакете cmd/web, и мы поместим их в новый файл cmd/web/testutils_test.go.
Если вы следите за ходом, пожалуйста, создайте этот файл сейчас…
$ touch cmd/web/testutils_test.go
Затем добавьте следующий код:
Файл: cmd/web/testutils_test.go
package main
import (
"bytes"
"io"
"log"
"net/http"
"net/http/httptest"
"testing"
)
// Создайте помощник `newTestApplication`, который возвращает экземпляр нашей
// структуры приложения, содержащий фиктивные зависимости.
func newTestApplication(t *testing.T) *application {
return &application{
errorLog: log.New(io.Discard, "", 0),
infoLog: log.New(io.Discard, "", 0),
}
}
// Определите пользовательский тип `testServer`, который включает экземпляр `httptest.Server`.
type testServer struct {
*httptest.Server
}
// Создайте помощник `newTestServer`, который инициализирует и возвращает новый экземпляр
// нашего пользовательского типа `testServer`.
func newTestServer(t *testing.T, h http.Handler) *testServer {
ts := httptest.NewTLSServer(h)
return &testServer{ts}
}
// Реализуйте метод `get()` на нашем пользовательском типе `testServer`. Это делает GET-запрос
// к заданному пути URL, используя клиент тестового сервера, и возвращает код состояния,
// заголовки и тело ответа.
func (ts *testServer) get(t *testing.T, urlPath string) (int, http.Header, string) {
rs, err := ts.Client().Get(ts.URL + urlPath)
if err != nil {
t.Fatal(err)
}
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
bytes.TrimSpace(body)
return rs.StatusCode, rs.Header, string(body)
}
По сути, это просто обобщение кода, который мы уже написали в этой главе, чтобы запустить тестовый сервер и отправить GET-запрос к нему.
Давайте вернемся к нашему тесту TestPing и используем эти новые помощники:
package main
import (
"net/http"
"testing"
"snippetbox.alexedwards.net/internal/assert"
)
func TestPing(t *testing.T) {
app := newTestApplication(t)
ts := newTestServer(t, app.routes())
defer ts.Close()
code, _, body := ts.get(t, "/ping")
assert.Equal(t, code, http.StatusOK)
assert.Equal(t, body, "OK")
}
все сработало
go test -v ./cmd/web
ok snippetbox.igorra.net/cmd/web 0.005s
Cookies and redirections
До сих пор в этой главе мы использовали настройки клиента тестового сервера по умолчанию. Но есть несколько изменений, которые я хотел бы внести, чтобы сделать его более подходящим для тестирования нашего веб-приложения. В частности:
- Мы хотим, чтобы клиент автоматически хранил любые куки, отправленные в ответе HTTPS, чтобы мы могли включить их (если это необходимо) в любые последующие запросы обратно к тестовому серверу. Это пригодится позже в книге, когда нам нужно будет поддерживать куки на нескольких запросах, чтобы протестировать наши меры противодействия CSRF.
- Мы не хотим, чтобы клиент автоматически следовал перенаправлениям. Вместо этого мы хотим, чтобы он возвращал первый ответ HTTPS, отправленный нашим сервером, чтобы мы могли протестировать ответ на этот конкретный запрос.
Изменим testutils_test.go и обновим функцию newTestServer():
Файл: cmd/web/testutils_test.go
package main
import (
"bytes"
"io"
"log"
"net/http"
"net/http/cookiejar" // Новый импорт
"net/http/httptest"
"testing"
)
...
func newTestServer(t *testing.T, h http.Handler) *testServer {
// Инициализируйте тестовый сервер как обычно.
ts := httptest.NewTLSServer(h)
// Инициализируйте новый контейнер для куки.
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatal(err)
}
// Добавьте контейнер для куки к клиенту тестового сервера. Любые куки ответа теперь
// будут храниться и отправляться с последующими запросами при использовании этого клиента.
ts.Client().Jar = jar
// Отключите следование за перенаправлениями для клиента тестового сервера, установив
// пользовательскую функцию `CheckRedirect`. Эта функция будет вызвана всякий раз, когда
// клиент получит ответ 3xx, и, всегда возвращая ошибку `http.ErrUseLastResponse`,
// она заставляет клиента немедленно вернуть полученный ответ.
ts.Client().CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return &testServer{ts}
}
...
Настройка тестирования
Управление запуском тестов
До сих пор в этой книге мы запускали тесты в определенном пакете (пакете cmd/web) следующим образом:
$ go test ./cmd/web
Но также возможно запустить все тесты в вашем текущем проекте, используя шаблон ./.... В нашем случае мы можем использовать его для запуска всех тестов в нашем проекте следующим образом:
$ go test ./...
ok snippetbox.alexedwards.net/cmd/web 0.007s
? snippetbox.alexedwards.net/internal/models [no test files]
? snippetbox.alexedwards.net/internal/validator [no test files]
? snippetbox.alexedwards.net/ui [no test files]
Или, идя в другом направлении, можно запустить только определенные тесты, используя флаг -run. Это позволяет вам передать регулярное выражение — и только тесты с именем, совпадающим с регулярным выражением, будут запущены.
Например, мы могли бы запустить только тест TestPing следующим образом:
$ go test -v -run="^TestPing$" ./cmd/web/
=== RUN TestPing
--- PASS: TestPing (0.00s)
PASS
ok snippetbox.alexedwards.net/cmd/web 0.008s
Вы также можете использовать флаг -run, чтобы ограничить тестирование до определенных подтестов, используя формат {regexp}/{sub-test regexp}. Например, чтобы запустить подтест UTC нашего теста TestHumanDate, мы можем сделать это:
$ go test -v -run="^TestHumanDate$/^UTC$" ./cmd/web
=== RUN TestHumanDate
=== RUN TestHumanDate/UTC
--- PASS: TestHumanDate (0.00s)
--- PASS: TestHumanDate/UTC (0.00s)
PASS
ok snippetbox.alexedwards.net/cmd/web 0.003s
Кэширование тестов
Если вы запустите один и тот же тест дважды — без внесения каких-либо изменений в пакет, который вы тестируете — то отображается кэшированная версия результата теста (указанная аннотацией (cached) рядом с именем пакета).
$ go test ./cmd/web
ok snippetbox.alexedwards.net/cmd/web (cached)
В большинстве случаев кэширование результатов тестов действительно полезно (особенно для больших кодовых баз), поскольку оно помогает сократить общее время выполнения тестов. Но если вы хотите принудительно запустить свои тесты в полном объеме (и избежать кэша), вы можете использовать флаг -count=1:
$ go test -count=1 ./cmd/web
Примечание
Флаг count используется для указания go test, сколько раз вы хотите выполнить каждый тест. Это не кэшируемый флаг, что означает, что каждый раз, когда вы его используете, go test не будет ни читать, ни записывать результаты тестов в кэш. Таким образом, использование count=1 — это своего рода трюк, чтобы избежать кэша без влияния на выполнение тестов.
Кроме того, вы можете очистить кэшированные результаты для всех тестов с помощью команды go clean:
$ go clean -testcache
Быстрое завершение
Когда мы используем функцию t.Errorf(), чтобы пометить тест как неудачный, она не вызывает немедленное завершение go test. Все остальные тесты (и подтесты) будут продолжать выполняться после сбоя.
Если вы предпочитаете завершить тесты сразу после первого сбоя, вы можете использовать флаг -failfast:
$ go test -failfast ./cmd/web
Важно отметить, что флаг -failfast останавливает тесты только в пакете, где произошел сбой. Если вы запускаете тесты в нескольких пакетах (например, используя go test ./...), то тесты в других пакетах будут продолжать выполняться.
Параллельное тестирование
По умолчанию команда go test выполняет все тесты последовательно, один за другим. Когда у вас небольшое количество тестов (как у нас) и время выполнения очень быстрое, это абсолютно нормально.
Но если у вас сотни или тысячи тестов, общее время выполнения может начать накапливаться до чего-то более существенного. И в этом сценарии вы можете сэкономить себе некоторое время, запустив тесты параллельно.
Вы можете указать, что тест можно запускать одновременно с другими тестами, вызвав функцию t.Parallel() в начале теста. Например:
func TestPing(t *testing.T) {
t.Parallel()
...
}
Важно отметить, что:
-
Тесты, отмеченные с помощью t.Parallel(), будут запускаться параллельно с другими параллельными тестами и только с ними.
-
По умолчанию максимальное количество тестов, которые будут запускаться одновременно, равно текущему значению GOMAXPROCS. Вы можете переопределить это, установив конкретное значение с помощью флага -parallel. Например:
$ go test -parallel 4 ./...
-
Не все тесты подходят для параллельного выполнения. Например, если у вас есть интеграционный тест, который требует, чтобы таблица базы данных находилась в определенном известном состоянии, то вы не хотели бы запускать его параллельно с другими тестами, которые изменяют ту же таблицу базы данных.
Включение детектора гонок
Команда go test включает флаг -race, который активирует детектор гонок Go при запуске тестов.
Если код, который вы тестируете, использует конкурентность, или вы запускаете тесты параллельно, включение этого может быть хорошей идеей, чтобы помочь выявить гонки, существующие в вашем приложении. Вы можете использовать его следующим образом:
$ go test -race ./cmd/web/
Важно отметить, что детектор гонок — это просто инструмент, который выявляет гонки данных, если и когда они обнаруживаются во время выполнения тестов. Он не проводит статический анализ вашего кода, и чистый запуск не гарантирует, что ваш код свободен от гонок.
Включение детектора гонок также увеличит общее время выполнения ваших тестов. Поэтому, если вы запускаете тесты очень часто в рамках рабочего процесса TDD, вы можете предпочесть использовать флаг -race только во время выполнения тестов перед коммитом.
Мокирование зависимостей
Мокирование зависимостей
Мокирование зависимостей (или “mocking dependencies”) — это техника, используемая в тестировании программного обеспечения, которая позволяет создавать поддельные (мок) версии зависимостей, с которыми взаимодействует тестируемый компонент. Это делается для того, чтобы изолировать тестируемый код от реальных зависимостей, что позволяет:
-
Избежать побочных эффектов: Использование реальных зависимостей может привести к нежелательным изменениям состояния (например, запись в базу данных или отправка сетевых запросов). Моки позволяют избежать этих побочных эффектов.
-
Упростить тестирование: Моки могут быть настроены для возврата предопределенных значений, что упрощает тестирование различных сценариев, включая ошибки и исключительные ситуации.
-
Ускорить тесты: Тесты с моками обычно выполняются быстрее, так как они не зависят от внешних систем, таких как базы данных или API.
-
Улучшить контроль: Моки позволяют контролировать и проверять, как тестируемый код взаимодействует с зависимостями, что помогает выявить ошибки в логике.
В Go для мокирования зависимостей часто используются библиотеки, такие как gomock или testify, которые позволяют создавать моки и определять их поведение в тестах.
Новые тесты для нашего обработчика snippetView и маршрута GET /snippet/view/:id.
На протяжении всего этого проекта мы внедряли зависимости в наши обработчики через структуру application, которая сейчас выглядит следующим образом:
type application struct {
errorLog *log.Logger
infoLog *log.Logger
snippets *models.SnippetModel
users *models.UserModel
templateCache map[string]*template.Template
formDecoder *form.Decoder
sessionManager *scs.SessionManager
}
При тестировании иногда имеет смысл мокировать эти зависимости вместо использования точно таких же, как в вашем производственном приложении.
Например, в предыдущей главе мы мокировали зависимости errorLog и infoLog логгерами, которые пишут сообщения в io.Discard, вместо потоков os.Stdout и os.Stderr, как мы делаем в нашем производственном приложении:
func newTestApplication(t *testing.T) *application {
return &application{
errorLog: log.New(io.Discard, "", 0),
infoLog: log.New(io.Discard, "", 0),
}
}
Причина мокирования этих и записи в io.Discard заключается в том, чтобы избежать засорения вывода наших тестов ненужными сообщениями журнала при запуске go test -v (с включенным режимом verbose).
Примечание: В зависимости от вашего происхождения и опыта программирования, вы можете не считать эти логгеры моками. Вы можете назвать их фейками, заглушками или чем-то совсем другим. Но название не имеет значения — и разные люди называют их по-разному. Главное, что мы используем что-то, что exposes тот же интерфейс, что и производственный объект, для целей тестирования.
Две другие зависимости, которые имеет смысл мокировать, — это модели базы данных models.SnippetModel и models.UserModel. Создавая моки этих моделей, можно тестировать поведение наших обработчиков без необходимости настраивать весь тестовый экземпляр базы данных MySQL.
Мокирование моделей базы данных
Создайте новый пакет internal/models/mocks, содержащий файлы snippets.go и users.go для хранения моков моделей базы данных, следующим образом:
$ mkdir internal/models/mocks
$ touch internal/models/mocks/snippets.go
$ touch internal/models/mocks/users.go
Давайте начнем с создания мока для нашей модели models.SnippetModel. Для этого мы создадим простую структуру, которая реализует те же методы, что и наша производственная модель models.SnippetModel, но методы будут возвращать фиксированные тестовые данные.
Файл: internal/models/mocks/snippets.go
package mocks
import (
"time"
"snippetbox.alexedwards.net/internal/models"
)
var mockSnippet = &models.Snippet{
ID: 1,
Title: "Старый тихий пруд",
Content: "Старый тихий пруд...",
Created: time.Now(),
Expires: time.Now(),
}
type SnippetModel struct{}
func (m *SnippetModel) Insert(title string, content string, expires int) (int, error) {
return 2, nil
}
func (m *SnippetModel) Get(id int) (*models.Snippet, error) {
switch id {
case 1:
return mockSnippet, nil
default:
return nil, models.ErrNoRecord
}
}
func (m *SnippetModel) Latest() ([]*models.Snippet, error) {
return []*models.Snippet{mockSnippet}, nil
}
Теперь сделаем то же самое для нашей модели models.UserModel:
Файл: internal/models/mocks/users.go
package mocks
import (
"snippetbox.alexedwards.net/internal/models"
)
type UserModel struct{}
func (m *UserModel) Insert(name, email, password string) error {
switch email {
case "dupe@example.com":
return models.ErrDuplicateEmail
default:
return nil
}
}
func (m *UserModel) Authenticate(email, password string) (int, error) {
if email == "alice@example.com" && password == "pa$$word" {
return 1, nil
}
return 0, models.ErrInvalidCredentials
}
func (m *UserModel) Exists(id int) (bool, error) {
switch id {
case 1:
return true, nil
default:
return false, nil
}
}
Инициализация моков
На следующем этапе нашего проекта давайте вернемся к файлу testutils_test.go и обновим функцию newTestApplication(), чтобы она создавала структуру приложения со всеми необходимыми зависимостями для тестирования.
Файл: cmd/web/testutils_test.go
package main
import (
"bytes"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"testing"
"time" // Новый импорт
"snippetbox.alexedwards.net/internal/models/mocks" // Новый импорт
"github.com/alexedwards/scs/v2" // Новый импорт
"github.com/go-playground/form/v4" // Новый импорт
)
func newTestApplication(t *testing.T) *application {
// Создаем экземпляр кеша шаблонов.
templateCache, err := newTemplateCache()
if err != nil {
t.Fatal(err)
}
// И декодер форм.
formDecoder := form.NewDecoder()
// И экземпляр менеджера сессий. Обратите внимание, что мы используем те же настройки, что и
// в производственной среде, за исключением того, что мы *не* устанавливаем Store для менеджера сессий.
// Если Store не установлен, пакет SCS по умолчанию будет использовать временное
// хранилище в памяти, что идеально подходит для тестирования.
sessionManager := scs.New()
sessionManager.Lifetime = 12 * time.Hour
sessionManager.Cookie.Secure = true
return &application{
errorLog: log.New(io.Discard, "", 0),
infoLog: log.New(io.Discard, "", 0),
snippets: &mocks.SnippetModel{}, // Используем мок.
users: &mocks.UserModel{}, // Используем мок.
templateCache: templateCache,
formDecoder: formDecoder,
sessionManager: sessionManager,
}
}
…
Если вы попробуете запустить тесты сейчас, они не скомпилируются и выдадут следующие ошибки:
$ go test ./cmd/web
# snippetbox.alexedwards.net/cmd/web [snippetbox.alexedwards.net/cmd/web.test]
cmd/web/testutils_test.go:40:19: cannot use &mocks.SnippetModel{} (value of type *mocks.SnippetModel) as type *models.SnippetModel in struct literal
cmd/web/testutils_test.go:41:19: cannot use &mocks.UserModel{} (value of type *mocks.UserModel) as type *models.UserModel in struct literal
FAIL snippetbox.alexedwards.net/cmd/web [build failed]
FAIL
Это происходит потому, что наша структура приложения ожидает указатели на экземпляры models.SnippetModel и models.UserModel, но мы пытаемся использовать указатели на экземпляры mocks.SnippetModel и mocks.UserModel.
Идиоматическое решение этой проблемы — изменить нашу структуру приложения так, чтобы она использовала интерфейсы, которые удовлетворяются как нашими моками, так и производственными моделями базы данных.
Для этого давайте вернемся к файлу internal/models/snippets.go и создадим новый тип интерфейса SnippetModelInterface, который описывает методы, которые есть у нашей фактической структуры SnippetModel.
Файл: internal/models/snippets.go
package models
import (
"database/sql"
"errors"
"time"
)
type SnippetModelInterface interface {
Insert(title string, content string, expires int) (int, error)
Get(id int) (*Snippet, error)
Latest() ([]*Snippet, error)
}
…
Теперь сделаем то же самое для нашей структуры UserModel:
Файл: internal/models/users.go
package models
import (
"database/sql"
"errors"
"strings"
"time"
"github.com/go-sql-driver/mysql"
"golang.org/x/crypto/bcrypt"
)
type UserModelInterface interface {
Insert(name, email, password string) error
Authenticate(email, password string) (int, error)
Exists(id int) (bool, error)
}
…
Теперь, когда мы определили эти интерфейсы, давайте обновим нашу структуру приложения, чтобы она использовала их вместо конкретных типов SnippetModel и UserModel. Вот так:
Файл: cmd/web/main.go
package main
import (
"crypto/tls"
"database/sql"
"flag"
"html/template"
"log"
"net/http"
"os"
"time"
"snippetbox.alexedwards.net/internal/models"
"github.com/alexedwards/scs/mysqlstore"
"github.com/alexedwards/scs/v2"
"github.com/go-playground/form/v4"
_ "github.com/go-sql-driver/mysql"
)
type application struct {
errorLog *log.Logger
infoLog *log.Logger
snippets models.SnippetModelInterface // Используем наш новый интерфейс.
users models.UserModelInterface // Используем наш новый интерфейс.
templateCache map[string]*template.Template
formDecoder *form.Decoder
sessionManager *scs.SessionManager
}
…
Если вы снова попробуете запустить тесты, все должно работать корректно.
go test -v ./cmd/web
ok snippetbox.igorra.net/cmd/web 0.005s
Мы обновили структуру приложения так, что вместо того, чтобы поля snippets и users имели конкретные типы *models.SnippetModel и *models.UserModel, теперь они являются интерфейсами.
Пока объект имеет необходимые методы для удовлетворения интерфейса, мы можем использовать его в нашей структуре приложения. Как наши «реальные» модели базы данных (например, models.SnippetModel), так и моковые модели базы данных (например, mocks.SnippetModel) удовлетворяют этим интерфейсам, поэтому мы теперь можем использовать их взаимозаменяемо.
Тестирование обработчика snippetView
Перейдем к написанию сквозного теста для нашего обработчика snippetView, который использует эти мокированные зависимости.
В рамках этого теста код в нашем обработчике snippetView будет вызывать метод mock.SnippetModel.Get(). Напоминаем, что этот мокированный метод модели возвращает models.ErrNoRecord, если ID сниппета не равен 1 — в противном случае он вернет следующий мок-сниппет:
var mockSnippet = &models.Snippet{
ID: 1,
Title: "Старый тихий пруд",
Content: "Старый тихий пруд...",
Created: time.Now(),
Expires: time.Now(),
}
Таким образом, мы хотим протестировать следующее:
- Для запроса
GET /snippet/view/1 мы получаем ответ 200 OK с соответствующим мок-сниппетом, содержащимся в теле HTML-ответа.
- Для всех остальных запросов к
GET /snippet/view/* мы должны получить ответ 404 Not Found.
Для первой части теста мы хотим проверить, что тело ответа содержит определенное содержимое, а не является точно равным ему. Давайте быстро добавим новую функцию StringContains() в наш пакет assert, чтобы помочь с этим:
Файл: assert/assert.go
package assert
import (
"strings" // Новый импорт
"testing"
)
...
func StringContains(t *testing.T, actual, expectedSubstring string) {
t.Helper()
if !strings.Contains(actual, expectedSubstring) {
t.Errorf("got: %q; expected to contain: %q", actual, expectedSubstring)
}
}
Затем откройте файл cmd/web/handlers_test.go и создайте новый тест TestSnippetView, как показано ниже:
Файл: cmd/web/handlers_test.go
package main
...
func TestSnippetView(t *testing.T) {
// Создаем новый экземпляр нашей структуры приложения, который использует мокированные зависимости.
app := newTestApplication(t)
// Устанавливаем новый тестовый сервер для выполнения сквозных тестов.
ts := newTestServer(t, app.routes())
defer ts.Close()
// Настраиваем тесты с использованием таблицы для проверки ответов, отправляемых нашим приложением для различных URL.
tests := []struct {
name string
urlPath string
wantCode int
wantBody string
}{
{
name: "Valid ID",
urlPath: "/snippet/view/1",
wantCode: http.StatusOK,
wantBody: "Старый тихий пруд...",
},
{
name: "Non-existent ID",
urlPath: "/snippet/view/2",
wantCode: http.StatusNotFound,
},
{
name: "Negative ID",
urlPath: "/snippet/view/-1",
wantCode: http.StatusNotFound,
},
{
name: "Decimal ID",
urlPath: "/snippet/view/1.23",
wantCode: http.StatusNotFound,
},
{
name: "String ID",
urlPath: "/snippet/view/foo",
wantCode: http.StatusNotFound,
},
{
name: "Empty ID",
urlPath: "/snippet/view/",
wantCode: http.StatusNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
code, _, body := ts.get(t, tt.urlPath)
assert.Equal(t, code, tt.wantCode)
if tt.wantBody != "" {
assert.StringContains(t, body, tt.wantBody)
}
})
}
}
Когда вы снова запустите тесты, все должно пройти успешно, и вы увидите вывод, включая новые тесты TestSnippetView, который будет выглядеть примерно так:
--- PASS: TestSnippetView (0.01s)
--- PASS: TestSnippetView/Valid_ID (0.00s)
--- PASS: TestSnippetView/Non-existent_ID (0.00s)
--- PASS: TestSnippetView/Negative_ID (0.00s)
--- PASS: TestSnippetView/Decimal_ID (0.00s)
--- PASS: TestSnippetView/String_ID (0.00s)
--- PASS: TestSnippetView/Empty_ID (0.00s)
Обратите внимание, как названия субтестов были канонизированы? Все пробелы в названии подтеста были заменены символом подчеркивания (и все непечатаемые символы также будут экранированы) в выходных данных теста.
Тестирование HTML-форм
В этой главе мы добавим сквозной тест для маршрута POST /user/signup, который обрабатывается нашим обработчиком userSignupPost.
Тестирование этого маршрута немного усложняется из-за проверки на наличие CSRF-токена, которую выполняет наше приложение. Любой запрос к POST /user/signup всегда будет получать ответ 400 Bad Request, если запрос не содержит действительный CSRF-токен и куки. Чтобы обойти это, нам нужно эмулировать рабочий процесс реального пользователя в рамках нашего теста, следующим образом:
- Сделать запрос
GET /user/signup. Это вернет ответ, который содержит CSRF-куку в заголовках ответа и CSRF-токен для страницы регистрации в теле ответа.
- Извлечь CSRF-токен из HTML-тела ответа.
- Сделать запрос
POST /user/signup, используя тот же http.Client, который мы использовали на шаге 1 (чтобы он автоматически передал CSRF-куку с POST-запросом) и включая CSRF-токен вместе с другими данными POST, которые мы хотим протестировать.
Давайте начнем с добавления новой вспомогательной функции в наш файл cmd/web/testutils_test.go для извлечения CSRF-токена (если он существует) из HTML-тела ответа:
Файл: cmd/web/testutils_test.go
package main
import (
"bytes"
"html" // Новый импорт
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"regexp" // Новый импорт
"testing"
"time"
"snippetbox.alexedwards.net/internal/models/mocks"
"github.com/alexedwards/scs/v2"
"github.com/go-playground/form/v4"
)
// Определяем регулярное выражение, которое захватывает значение CSRF-токена из
// HTML для нашей страницы регистрации пользователя.
var csrfTokenRX = regexp.MustCompile(`<input type='hidden' name='csrf_token' value='(.+)'>`)
func extractCSRFToken(t *testing.T, body string) string {
// Используем метод FindStringSubmatch для извлечения токена из HTML-тела.
// Обратите внимание, что это возвращает массив с полным совпадением в
// первой позиции и значениями любых захваченных данных в последующих
// позициях.
matches := csrfTokenRX.FindStringSubmatch(body)
if len(matches) < 2 {
t.Fatal("no csrf token found in body")
}
return html.UnescapeString(string(matches[1]))
}
Примечание
Вы можете задаться вопросом, почему мы используем функцию html.UnescapeString() перед возвратом CSRF-токена.
Причина в том, что пакет html/template в Go автоматически экранирует все динамически отображаемые данные, включая наш CSRF-токен. Поскольку CSRF-токен является строкой, закодированной в base64, он потенциально может содержать символ +, который будет экранирован как +. Поэтому после извлечения токена из HTML нам нужно пропустить его через html.UnescapeString(), чтобы получить оригинальное значение токена.
Теперь, когда это сделано, давайте вернемся к файлу cmd/web/handlers_test.go и создадим новый тест TestUserSignup.
Для начала мы сделаем запрос GET /user/signup, а затем извлечем и выведем CSRF-токен из тела HTML-ответа. Вот так:
Файл: cmd/web/handlers_test.go
package main
...
func TestUserSignup(t *testing.T) {
// Создаем структуру приложения, содержащую наши мокированные зависимости, и настраиваем
// тестовый сервер для выполнения сквозного теста.
app := newTestApplication(t)
ts := newTestServer(t, app.routes())
defer ts.Close()
// Выполняем запрос GET /user/signup и затем извлекаем CSRF-токен из
// тела ответа.
_, _, body := ts.get(t, "/user/signup")
csrfToken := extractCSRFToken(t, body)
// Логируем значение CSRF-токена в нашем тестовом выводе, используя функцию t.Logf().
// Функция t.Logf() работает так же, как fmt.Printf(), но записывает
// предоставленное сообщение в тестовый вывод.
t.Logf("CSRF token is: %q", csrfToken)
}
Важно, чтобы вы запускали тесты с флагом -v (для включения подробного вывода), чтобы увидеть любой вывод от функции t.Logf().
Давайте сделаем это сейчас:
go test -v -run="TestUserSignup" ./cmd/web/
=== RUN TestUserSignup
handlers_test.go:96: CSRF token is: "+mBVJFv/BJOlinbDGyrjuB4noDRwCDctcf/gckiVWc2z2zo8mdY52+uuPiXNxqGnhZnFh7udQvsIehQx7/NO4Q=="
--- PASS: TestUserSignup (0.00s)
PASS
ok snippetbox.igorra.net/cmd/web 0.006s
Тестирование POST-запросов
Теперь давайте вернемся к файлу cmd/web/testutils_test.go и создадим новый метод postForm() для нашего типа testServer, который мы можем использовать для отправки POST-запроса на наш тестовый сервер с конкретными данными формы в теле запроса.
Добавьте следующий код (который следует тому же общему паттерну, что и метод get(), описанный ранее в книге):
Файл: cmd/web/testutils_test.go
package main
import (
"bytes"
"html"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url" // Новый импорт
"regexp"
"testing"
"time"
"snippetbox.alexedwards.net/internal/models/mocks"
"github.com/alexedwards/scs/v2"
"github.com/go-playground/form/v4"
)
...
// Создаем метод postForm для отправки POST-запросов на тестовый сервер.
// Последний параметр этого метода — это объект url.Values, который может
// содержать любые данные формы, которые вы хотите отправить в теле запроса.
func (ts *testServer) postForm(t *testing.T, urlPath string, form url.Values) (int, http.Header, string) {
rs, err := ts.Client().PostForm(ts.URL+urlPath, form)
if err != nil {
t.Fatal(err)
}
// Читаем тело ответа от тестового сервера.
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
bytes.TrimSpace(body)
// Возвращаем статус ответа, заголовки и тело.
return rs.StatusCode, rs.Header, string(body)
}
Теперь, наконец, мы готовы добавить несколько тестов с использованием таблицы для проверки поведения маршрута POST /user/signup нашего приложения. В частности, мы хотим протестировать, что:
- Действительная регистрация приводит к ответу 303 See Other.
- Отправка формы без действительного CSRF-токена приводит к ответу 400 Bad Request.
- Неверная отправка формы приводит к ответу 422 Unprocessable Entity и повторному отображению формы регистрации. Это должно происходить, когда:
- Поля имени, электронной почты или пароля пустые.
- Электронная почта не имеет действительного формата.
- Пароль короче 8 символов.
- Адрес электронной почты уже используется.
Давайте обновим функцию TestUserSignup, чтобы выполнить эти тесты следующим образом:
package main
import (
"net/http"
"net/url" // New import
"testing"
"snippetbox.alexedwards.net/internal/assert"
)
...
func TestUserSignup(t *testing.T) {
app := newTestApplication(t)
ts := newTestServer(t, app.routes())
defer ts.Close()
_, _, body := ts.get(t, "/user/signup")
validCSRFToken := extractCSRFToken(t, body)
const (
validName = "Bob"
validPassword = "validPa$$word"
validEmail = "bob@example.com"
formTag = "<form action='/user/signup' method='POST' novalidate>"
)
tests := []struct {
name string
userName string
userEmail string
userPassword string
csrfToken string
wantCode int
wantFormTag string
}{
{
name: "Valid submission",
userName: validName,
userEmail: validEmail,
userPassword: validPassword,
csrfToken: validCSRFToken,
wantCode: http.StatusSeeOther,
},
{
name: "Invalid CSRF Token",
userName: validName,
userEmail: validEmail,
userPassword: validPassword,
csrfToken: "wrongToken",
wantCode: http.StatusBadRequest,
},
{
name: "Empty name",
userName: "",
userEmail: validEmail,
userPassword: validPassword,
csrfToken: validCSRFToken,
wantCode: http.StatusUnprocessableEntity,
wantFormTag: formTag,
},
{
name: "Empty email",
userName: validName,
userEmail: "",
userPassword: validPassword,
csrfToken: validCSRFToken,
wantCode: http.StatusUnprocessableEntity,
wantFormTag: formTag,
},
{
name: "Empty password",
userName: validName,
userEmail: validEmail,
userPassword: "",
csrfToken: validCSRFToken,
wantCode: http.StatusUnprocessableEntity,
wantFormTag: formTag,
},
{
name: "Invalid email",
userName: validName,
userEmail: "bob@example.",
userPassword: validPassword,
csrfToken: validCSRFToken,
wantCode: http.StatusUnprocessableEntity,
wantFormTag: formTag,
},
{
name: "Short password",
userName: validName,
userEmail: validEmail,
userPassword: "pa$$",
csrfToken: validCSRFToken,
wantCode: http.StatusUnprocessableEntity,
wantFormTag: formTag,
},
{
name: "Duplicate email",
userName: validName,
userEmail: "dupe@example.com",
userPassword: validPassword,
csrfToken: validCSRFToken,
wantCode: http.StatusUnprocessableEntity,
wantFormTag: formTag,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
form := url.Values{}
form.Add("name", tt.userName)
form.Add("email", tt.userEmail)
form.Add("password", tt.userPassword)
form.Add("csrf_token", tt.csrfToken)
code, _, body := ts.postForm(t, "/user/signup", form)
assert.Equal(t, code, tt.wantCode)
if tt.wantFormTag != "" {
assert.StringContains(t, body, tt.wantFormTag)
}
})
}
}
9.5 - Alex Edwards Let's Go часть 5
edwardsLetsGo учебный материал по написанию приложения на Go с авторизацией пользователя. Краткий конспект с основными мыслями (глава 17)
Добавьте страницу «О программе» в приложение
Создайте обработчик about
File: cmd/web/handlers.go
package main
...
func (app *application) about(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data.About = "Это приложение создано для того,чтобы тренироваться. <br />И я думаю, здесь потом написать markdown текст."
app.render(w, http.StatusOK, "about.tmpl", data)
}
Создайте маршрут GET /about
File: cmd/web/routes.go
router.Handler(http.MethodGet, "/about", dynamic.ThenFunc(app.about))
Создайте шаблон about .tmpl
File: ui/html/pages/about.tmpl
{{define "title"}}О приложении{{end}}
{{define "main"}}
{{with .About}}
<div class='snippet'>
<p>{{.}}</p>
</div>
{{end}}
{{end}}
Создать навигацию в nav.tmpl
<a href='/about'>About</a>
Создать новый флаг -debug
Создаем описание флага
Три точки правки для добавления флага:
type application struct {
debug bool // 1. Добавить в структуру приложения
errorLog *log.Logger
infoLog *log.Logger
snippets models.SnippetModelInterface
users models.UserModelInterface
templateCache map[string]*template.Template
formDecoder *form.Decoder
sessionManager *scs.SessionManager
}
...
debug := flag.Bool("debug", false, "Включить режим отладки, по умолчанию false") //2. добавить описание флага до Parse
...
app := &application{
debug: *debug, // Инициализация флага в экземпляре приложения
errorLog: errorLog,
infoLog: infoLog,
snippets: &models.SnippetModel{DB: db},
users: &models.UserModel{DB: db},
templateCache: templateCache,
formDecoder: formDecoder,
sessionManager: sessionManager,
}
Настройка cmd/web/helpers.go для serverError()
Добавим условие для проверки флага app.debug.
func (app *application) serverError(w http.ResponseWriter, err error) {
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
app.errorLog.Output(2, trace)
if app.debug { //Добавил условие проверки флага
http.Error(w, trace, http.StatusInternalServerError)
return
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
Продолжаем тестирование. Тестирование обработчика snippetCreate
Создадим сквозной тест для маршрута GET /snippet/create.
- Пользователи, не прошедшие аутентификацию, перенаправляются на форму входа.
- Пользователям, прошедшим проверку подлинности, отображается форма для создания нового сниппета.
Создадим заготовку для теста handlers_test.go
func TestSnippetCreate(t *testing.T) {
// Создаем новый экземпляр нашей структуры приложения, который использует мокированные зависимости.
app := newTestApplication(t)
// Устанавливаем новый тестовый сервер для выполнения сквозных тестов.
ts := newTestServer(t, app.routes())
defer ts.Close()
}
Создадим подтест с проверкой доступа неаутентифицированного пользователя
func TestSnippetCreate(t *testing.T) {
// Создаем новый экземпляр нашей структуры приложения, который использует мокированные зависимости.
app := newTestApplication(t)
// Устанавливаем новый тестовый сервер для выполнения сквозных тестов.
ts := newTestServer(t, app.routes())
defer ts.Close()
t.Run("Unauthenticated", func(t *testing.T) {//название теста и безымянная функция
code, headers, _ := ts.get(t, "/snippet/create") //запрос к серверу с моками по адресу проверки
//code и headers - запишем ответы от сервера кода и заголовка
assert.Equal(t, code, http.StatusSeeOther) //проверим код
assert.Equal(t, headers.Get("Location"), "/user/login") //проверим заголовок Location
//location показывает адрес текущей страницы
})
}
Страница личного кабинета
Добавить новую страницу «Ваша учетная запись» в приложение. Он должен быть сопоставлен с новым маршрутом GET /account/view и отображать имя, адрес электронной почты и дату регистрации для текущего аутентифицированного пользователя
Создадим в файле internal/models/users.go функция UserModel.Get()
- Добавим метод Get в интерфейс UserModelInterface
type UserModelInterface interface {
Insert(name, email, password string) error
Authenticate(email, password string) (int, error)
Exists(id int) (bool, error)
Get(id int) (*User, error) //добавил метод Get
}
- Создадим метод Get
func (m *UserModel) Get(id int) (*User, error) {
var user User
stmt := `SELECT id, name, email, created FROM users WHERE id = ?`
err := m.DB.QueryRow(stmt, id).Scan(&user.ID, &user.Name, &user.Email, &user.Created)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoRecord
} else {
return nil, err
}
}
return &user, err
}
routes.go
router.Handler(http.MethodGet, "/account/view", protected.ThenFunc(app.accountView))
handlers.go
Добавим функцию accountView
func (app *application) accountView(w http.ResponseWriter, r *http.Request) {
userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")
user, err := app.users.Get(userID)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
} else {
app.serverError(w, err)
}
return
}
fmt.Fprintf(w, "%+v", user)
}
templates.go
добавим поле User
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
Form any
Flash string
IsAuthenticated bool
CSRFToken string
User *models.User //добавили User
}
ui/html/pages/account.tmpl
Создаем шаблон
{{define "title"}}Your Account{{end}}
{{define "main"}}
<h2>Your Account</h2>
{{with .User}}
<table>
<tr>
<th>Name</th>
<td>{{.Name}}</td>
</tr>
<tr>
<th>Email</th>
<td>{{.Email}}</td>
</tr>
<tr>
<th>Joined</th>
<td>{{humanDate .Created}}</td>
</tr>
</table>
{{end }}
{{end}}
handkers.go
func (app *application) accountView(w http.ResponseWriter, r *http.Request) {
userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")
user, err := app.users.Get(userID)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
} else {
app.serverError(w, err)
}
return
}
data := app.newTemplateData(r)
data.User = user
app.render(w, http.StatusOK, "account.tmpl", data)
}
Настроим меню приложения
{{define "nav"}}
<nav>
<div>
<a href='/'>Home</a>
<a href='/about'>About</a>
{{if .IsAuthenticated}}
<a href='/snippet/create'>Create snippet</a>
{{end}}
</div>
<div>
{{if .IsAuthenticated}}
<!-- Add the view account link for authenticated users -->
<a href='/account/view'>Account</a>
<form action='/user/logout' method='POST'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<button>Logout</button>
</form>
{{else}}
<a href='/user/signup'>Signup</a>
<a href='/user/login'>Login</a>
{{end}}
</div>
</nav>
{{end}}
Перенаправление пользователя после входа в систему
Обновить middleware requireAuthentication()
чтобы перед тем, как неаутентифицированный пользователь будет перенаправлен на страницу входа, URL-путь, который он пытается посетить, был добавлен в данные его сеанса.
func (app *application) requireAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !app.isAuthenticated(r) {
// Add the path that the user is trying to access to their session
// data.
app.sessionManager.Put(r.Context(), "redirectPathAfterLogin", r.URL.Path)
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
return
}
w.Header().Add("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}
Обновите обработчик userLogin
чтобы проверить путь URL-адреса пользователя в сеансе после успешного входа в систему. Если он существует, удалите его из данных сеанса и перенаправьте пользователя на этот URL-путь. В противном случае по умолчанию пользователь будет перенаправлен в /snippet/create.
path := app.sessionManager.PopString(r.Context(), "redirectPathAfterLogin")
if path != "" {
http.Redirect(w, r, path, http.StatusSeeOther)
return
}
http.Redirect(w, r, "/snippet/create", http.StatusSeeOther)
Восстановление пароля
Создадим 2 новых routes и handlers
GET /account/password/update — accountPasswordUpdate
POST /account/password/update — accountPasswordUpdatePost
handlers.go
func (app *application) accountPasswordUpdate(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data.Form = userPasswordUpdate{}
app.render(w, http.StatusOK, "password.tmpl", data)
}
func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) {
var form userPasswordUpdate
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
routes.go
router.Handler(http.MethodGet, "/account/password/update", protected.ThenFunc(app.accountPasswordUpdate))
router.Handler(http.MethodPost, "/account/password/update", protected.ThenFunc(app.accountPasswordUpdatePost))
ui/html/pages/password.tmpl
{{define "title"}}Change Password{{end}}
{{define "main"}}
<h2>Change Password</h2>
<form action='/account/password/update' method='POST' novalidate>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<div>
<label>Current password:</label>
{{with .Form.FieldErrors.currentPassword}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='currentPassword'>
</div>
<div>
<label>New password:</label>
{{with .Form.FieldErrors.newPassword}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='newPassword'>
</div>
<div>
<label>Confirm new password:</label>
{{with .Form.FieldErrors.newPasswordConfirmation}}
<label class='error'>{{.}}</label>
{{end}}
<input type='password' name='newPasswordConfirmation'>
</div>
<div>
<input type='submit' value='Change password'>
</div>
</form>
{{end}}
handlers.go
Обработчик Get
type accountPasswordUpdateForm struct {
CurrentPassword string `form:"currentPassword"`
NewPassword string `form:"newPassword"`
NewPasswordConfirmation string `form:"newPasswordConfirmation"`
validator.Validator `form:"-"`
}
func (app *application) accountPasswordUpdate(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data.Form = accountPasswordUpdateForm{}
app.render(w, http.StatusOK, "password.tmpl", data)
}
обработчик Post
func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) {
var form accountPasswordUpdateForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.CurrentPassword), "currentPassword", "This field cannot be blank")
form.CheckField(validator.NotBlank(form.NewPassword), "newPassword", "This field cannot be blank")
form.CheckField(validator.MinChars(form.NewPassword, 8), "newPassword", "This field must be at least 8 characters long")
form.CheckField(validator.NotBlank(form.NewPasswordConfirmation), "newPasswordConfirmation", "This field cannot be blank")
form.CheckField(form.NewPassword == form.NewPasswordConfirmation, "newPasswordConfirmation", "Passwords do not match")
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "password.tmpl", data)
return
}
}
users.go
- добавим интерфейс PasswordUpdate
type UserModelInterface interface {
Insert(name, email, password string) error
Authenticate(email, password string) (int, error)
Exists(id int) (bool, error)
Get(id int) (*User, error)
PasswordUpdate(id int, currentPassword, newPassword string) error
}
- добавим метод PasswordUpdate
func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error {
var currentHashedPassword []byte
stmt := "SELECT hashed_password FROM users WHERE id = ?"
err := m.DB.QueryRow(stmt, id).Scan(¤tHashedPassword)
if err != nil {
return err
}
err = bcrypt.CompareHashAndPassword(currentHashedPassword, []byte(currentPassword))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return ErrInvalidCredentials
} else {
return err
}
}
newHashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), 12)
if err != nil {
return err
}
stmt = "UPDATE users SET hashed_password = ? WHERE id = ?"
_, err = m.DB.Exec(stmt, string(newHashedPassword), id)
return err
}
handlers.go
окончательный вариантфункции accountPasswordUpdatePost
func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) {
var form accountPasswordUpdateForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.CurrentPassword), "currentPassword", "This field cannot be blank")
form.CheckField(validator.NotBlank(form.NewPassword), "newPassword", "This field cannot be blank")
form.CheckField(validator.MinChars(form.NewPassword, 8), "newPassword", "This field must be at least 8 characters long")
form.CheckField(validator.NotBlank(form.NewPasswordConfirmation), "newPasswordConfirmation", "This field cannot be blank")
form.CheckField(form.NewPassword == form.NewPasswordConfirmation, "newPasswordConfirmation", "Passwords do not match")
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "password.tmpl", data)
return
}
userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")
err = app.users.PasswordUpdate(userID, form.CurrentPassword, form.NewPassword)
if err != nil {
if errors.Is(err, models.ErrInvalidCredentials) {
form.AddFieldError("currentPassword", "Current password is incorrect")
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "password.tmpl", data)
} else if err != nil {
app.serverError(w, err)
}
return
}
app.sessionManager.Put(r.Context(), "flash", "Your password has been updated!")
http.Redirect(w, r, "/account/view", http.StatusSeeOther)
}
account.tmpl
поставим ссылку на изменение пароля
{{define "title"}}Your Account{{end}}
{{define "main"}}
<h2>Your Account</h2>
{{with .User}}
<table>
<tr>
<th>Name</th>
<td>{{.Name}}</td>
</tr>
<tr>
<th>Email</th>
<td>{{.Email}}</td>
</tr>
<tr>
<th>Joined</th>
<td>{{humanDate .Created}}</td>
</tr>
<tr>
<!-- Add a link to the change password form -->
<th>Password</th>
<td><a href="/account/password/update">Change password</a></td>
</tr>
</table>
{{end }}
{{end}}
10 - Памятка по примерам GO
Описание различных примеров и подходов в GO
10.1 - таблица с основными форматами Printf в Go
Шпаргалка по форматным строкам fmt.Printf
Шпаргалка по форматным строкам fmt.Printf
| Формат |
Тип данных |
Пример вывода |
Описание |
С # (если применимо) |
| Общие |
|
|
|
|
%v |
Любой |
{John 25} |
Универсальный формат |
%#v → main.Person{Name:"John"} |
%+v |
Структуры |
{Name:John Age:25} |
С именами полей |
— |
%T |
Любой |
string |
Тип переменной |
— |
%% |
— |
% |
Вывод знака % |
— |
| Целые числа |
|
|
|
|
%d |
int |
42 |
Десятичное число |
— |
%#x |
int |
0x2a |
Шестнадцатеричное с 0x |
Без #: 2a |
%#o |
int |
052 |
Восьмеричное с 0 |
Без #: 52 |
%b |
int |
101010 |
Двоичное |
%#b → 0b101010 (редко) |
| Символы |
|
|
|
|
%c |
rune |
A |
Символ по коду |
— |
%U |
rune |
U+0041 |
Код Unicode |
%#U → U+0041 'A' |
%q |
rune/string |
'A' / "A" |
Кавычки вокруг значения |
%#q → 'A' (для рун) |
| Строки |
|
|
|
|
%s |
string |
Hello |
Чистая строка |
— |
%x |
string/[]byte |
48656c6c6f |
Шестнадцатеричный дамп |
%#x → 0x48656c6c6f |
| Плавающие |
|
|
|
|
%f |
float64 |
3.141593 |
Обычный формат |
— |
%g |
float64 |
3.14159 |
Автовыбор (%f или %e) |
%#g → Без изменений |
%e |
float64 |
3.141593e+00 |
Экспоненциальный формат |
— |
| Указатели |
|
|
|
|
%p |
Указатель |
0xc0000180a8 |
Адрес памяти |
%#p → c0000180a8 (без 0x) |
| Ширина/Точность |
|
|
|
|
%5d |
int |
42 |
Фиксированная ширина (5 символов) |
— |
%05d |
int |
00042 |
Дополнение нулями |
— |
%.2f |
float64 |
3.14 |
Ограничение знаков после точки |
— |
Примеры использования
fmt.Printf("|%#v|%5d|%05d|\n", struct{ X int }{1}, 42, 42)
// |struct { X int }{X:1}| 42|00042|
fmt.Printf("|%#x|%#U|\n", 42, 'A')
// |0x2a|U+0041 'A'|
⚠️ Особенности
- Флаг
# работает только с %v, %x, %X, %o, %U, %p, %q.
- Для
%g, %s, %d и других — игнорируется.
%#q для строк оставляет двойные кавычки, для рун — одинарные.
10.2 - Closure
В этом примере на Go демонстрируется замыкание (closure) — функция, которая запоминает окружение, в котором она была создана.
1. Функция intSeq() создаёт замыкание
func intSeq() func() int {
i := 0 // Локальная переменная внутри intSeq
return func() int { // Возвращаемая анонимная функция
i++ // Замыкание "видит" переменную i
return i
}
}
i := 0 — это локальная переменная внутри intSeq.
- Возвращаемая функция
func() int “замыкается” вокруг i, то есть запоминает её и может изменять.
2. Создаём экземпляр замыкания
nextInt := intSeq() // Переменная nextInt теперь — функция
- При вызове
intSeq():
- Создаётся переменная
i = 0.
- Возвращается функция-замыкание, которая удерживает ссылку на
i.
3. Вызов nextInt() изменяет состояние i
p(nextInt()) // Выведет: === 1 ===
p(nextInt()) // Выведет: === 2 ===
p(nextInt()) // Выведет: === 3 ===
- Каждый вызов
nextInt():
- Увеличивает
i на 1 (i++).
- Возвращает новое значение.
i сохраняется между вызовами — это и есть “запомненное окружение”.
4. Новое замыкание — новый экземпляр i
newInts := intSeq() // Создаётся новый экземпляр замыкания
p(newInts()) // Выведет: === 1 === (не 4!)
newInts — это новое замыкание с собственной переменной i = 0.
- Оно не связано с
nextInt — их состояния (i) независимы.
Итог: как работает замыкание
- Запоминает окружение (переменные, например
i), даже после выхода из внешней функции.
- Сохраняет состояние между вызовами (
i увеличивается).
- Каждое новое замыкание создаёт свой экземпляр переменных.
Это полезно для:
- Генераторов (как в примере).
- Callback-функций с сохранением состояния.
- Инкапсуляции данных (аналог private переменных в ООП).
Пример в JavaScript (для сравнения):
function intSeq() {
let i = 0;
return () => ++i;
}
Работает аналогично!
10.3 - Создание модуля в GO
Создание и подключение новых модулей локально
Создание проекта
Создать директорию с проектом
mkdir greetings
cd greetings
Инициализация
$ go mod init example.com/greetings
go: creating new go.mod: module example.com/greetings
Команда init создает файл go.mod с основными параметрами модуля:
module example.com/hello
go 1.24.3
Создать файл GO
В редакторе создаю файл: напрмер greetings.go
package greetings
import "fmt"
// Hello returns a greeting for the named person.
func Hello(name string) string {
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message
}
важно указать в package название пакета для последующей ссылки на него
Создание основного модуля MAIN
Если проект многомодульный, то повторяем из корня проекта создание нового модуля:
cd ..
mkdir hello
cd hello
Выполняем инициализацию
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
Создаем файл основного проекта
имя файла должно быть таким, каким потом будет исполняемый файл, но в файле обязательно должен быть: package main
package main
import (
"fmt"
"example.com/greetings"
)
func main() {
// Get a greeting message and print it.
message := greetings.Hello("Gladys")
fmt.Println(message)
}
Корректируем путь на зависимый модуль
go mod edit -replace example.com/greetings=../greetings
изменит в файле go.mod на фактический путь зависимого модуля
Получаем файл:
module example.com/hello
go 1.24.3
replace example.com/greetings => ../greetings
Выполняем go mod tidy
$ go mod tidy
go: found example.com/greetings in example.com/greetings v0.0.0-00010101000000-000000000000
для фактической связки только нужных пакетов и проверки go.mod
получаем go.mod
module example.com/hello
go 1.24.3
replace example.com/greetings => ../greetings
require example.com/greetings v0.0.0-00010101000000-000000000000
Запуск проекта
$ go run .
Hi, Gladys. Welcome!
10.4 - Создание тестов в GO
Создание и запуск тестов в GO + Emacs
Основные правила написания тестов в Go
-
Именование файлов:
- Тестовые файлы должны заканчиваться на
_test.go.
- Пример:
main.go → main_test.go.
-
Именование функций:
- Тестовые функции должны начинаться с
Test (для юнит-тестов) или Benchmark (для бенчмарков).
- Пример:
func TestAdd(t *testing.T) { ... }
func BenchmarkAdd(b *testing.B) { ... }
-
Параметры тестовых функций:
- Юнит-тесты принимают
*testing.T.
- Бенчмарки принимают
*testing.B.
- Примеры тестов (
t.Error, t.Fail, t.Run для подтестов).
-
Табличные тесты (Table-Driven Tests):
- Рекомендуемый подход для покрытия разных сценариев.
- Пример:
func TestAdd(t *testing.T) {
cases := []struct {
a, b, expected int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tc := range cases {
result := Add(tc.a, tc.b)
if result != tc.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tc.a, tc.b, result, tc.expected)
}
}
}
-
Покрытие кода (Coverage):
- Замер покрытия:
go test -cover
go test -coverprofile=coverage.out && go tool cover -html=coverage.out
-
Моки и тестовые данные:
- Используйте интерфейсы для замены зависимостей (например,
sqlmock для тестирования SQL-запросов).
Автоматизация тестов в Emacs для Go
-
go-mode:
- Основной режим для работы с Go в Emacs.
- Установка:
(use-package go-mode
:ensure t)
-
go-test.el:
- Пакет для запуска тестов (встроен в
go-mode).
- Команды:
M-x go-test-current-test — запуск текущего теста.
M-x go-test-current-file — тестирование текущего файла.
M-x go-test-current-project — тесты всего проекта.
M-x go-test-current-benchmark — запуск бенчмарка.
-
flycheck / golangci-lint:
- Проверка кода и тестов в реальном времени.
-
dap-mode (Debug Adapter Protocol):
- Отладка тестов (аналог VS Code).
Требования к тестовым файлам
-
Расположение:
- Тестовые файлы должны быть в том же пакете (
package math → package math_test для black-box тестирования).
-
Доступ к символам:
- Если тестируемые функции/переменные должны быть экспортированы (
UpperCase).
-
Использование testing:
- Обязателен импорт
"testing".
-
Тестовые данные:
- Можно использовать
testdata/ для дополнительных файлов (например, JSON-фикстур).
Основные команды для тестов
-
Запуск всех тестов:
-
Запуск конкретного теста:
-
Покрытие кода:
go test -coverprofile=cover.out && go tool cover -html=cover.out
-
Бенчмарки:
-
Пропуск кеширования:
-
Вербозный вывод:
-
Параллельные тесты:
func TestParallel(t *testing.T) {
t.Parallel()
// ...
}
Бенчмарки (Benchmarks) в Go
Бенчмарки в Go — это тесты производительности, которые измеряют скорость выполнения кода (время на операцию, аллокации памяти и т. д.). Они помогают находить узкие места в программе и сравнивать разные реализации алгоритмов.
Как работают бенчмарки?
-
Именование функции:
- Должно начинаться с
Benchmark.
- Принимает
*testing.B (аналог *testing.T в обычных тестах).
- Пример:
func BenchmarkSum(b *testing.B) {
for i := 0; i < b.N; i++ {
Sum(1, 2) // Тестируемая функция
}
}
-
Цикл b.N:
- Go автоматически подбирает
N (количество итераций), чтобы получить стабильные результаты.
- Чем быстрее функция, тем больше итераций будет сделано.
-
Запуск бенчмарка:
Полезные флаги для бенчмарков
| Флаг |
Описание |
Пример |
-benchmem |
Показывать аллокации памяти |
go test -bench . -benchmem |
-benchtime |
Установить время выполнения |
go test -bench . -benchtime=5s |
-count |
Количество прогонов |
go test -bench . -count=3 |
-cpu |
Тестировать на разном числе CPU |
go test -bench . -cpu=1,2,4 |
Пример бенчмарка
Допустим, есть функция сложения:
// sum.go
package math
func Sum(a, b int) int {
return a + b
}
Тест и бенчмарк:
// sum_test.go
package math
import "testing"
func TestSum(t *testing.T) {
if Sum(1, 2) != 3 {
t.Error("Ожидается 3")
}
}
func BenchmarkSum(b *testing.B) {
for i := 0; i < b.N; i++ {
Sum(1, 2)
}
}
Запуск:
go test -bench . -benchmem
Вывод:
BenchmarkSum-8 1000000000 0.265 ns/op 0 B/op 0 allocs/op
0.265 ns/op — 0.265 наносекунд на операцию.
0 B/op — 0 байт выделено за итерацию.
0 allocs/op — 0 аллокаций памяти.
Сравнение производительности
Бенчмарки полезны для сравнения разных реализаций. Например:
func BenchmarkSlow(b *testing.B) {
for i := 0; i < b.N; i++ {
SlowFunction()
}
}
func BenchmarkFast(b *testing.B) {
for i := 0; i < b.N; i++ {
FastFunction()
}
}
Запуск:
go test -bench . -benchmem
Покажет, какая функция работает быстрее и использует меньше памяти.
Бенчмарки в Go — это тесты производительности.
- Запускаются через
go test -bench.
- Показывают время на операцию (
ns/op), аллокации памяти (B/op, allocs/op).
Если нужно углублённо анализировать производительность, можно использовать pprof:
go test -bench . -cpuprofile=cpu.out
go tool pprof cpu.out
10.5 - Работа с Workspace в Go: Полное руководство
Workspace (рабочее пространство) в Go — это мощный инструмент для управления несколькими модулями в одном проекте. Он появился в Go 1.18 и упрощает разработку, когда у вас есть взаимосвязанные модули.
1. Что такое Workspace и зачем он нужен?
Workspace позволяет:
- Работать с несколькими модулями одновременно без необходимости публиковать их в
GOPATH или go.mod replace.
- Упрощать разработку в монолитных репозиториях (monorepo).
- Управлять зависимостями между локальными модулями без
replace в go.mod.
2. Создание Workspace
Шаг 1: Инициализация Workspace
Создается файл go.work в корне проекта.
Шаг 2: Добавление модулей в Workspace
go work use ./module1 ./module2
Теперь go.work выглядит так:
go 1.21
use (
./module1
./module2
)
3. Структура Workspace
Пример структуры проекта:
myproject/
├── go.work # Workspace файл
├── module1/ # Первый модуль
│ ├── go.mod # go.mod модуля 1
│ └── main.go
└── module2/ # Второй модуль
├── go.mod # go.mod модуля 2
└── utils.go
4. Основные команды для работы с Workspace
| Команда |
Описание |
go work init |
Создает go.work |
go work use [dir] |
Добавляет модуль в Workspace |
go work edit |
Редактирует go.work вручную |
go work sync |
Синхронизирует зависимости |
5. Пример: Разработка двух связанных модулей
Модуль 1 (module1/main.go)
package main
import (
"fmt"
"github.com/user/module2/utils" // Локальный модуль
)
func main() {
fmt.Println(utils.Add(1, 2)) // Используем функцию из module2
}
Модуль 2 (module2/utils.go)
package utils
func Add(a, b int) int {
return a + b
}
Файл go.work
go 1.21
use (
./module1
./module2
)
Теперь module1 может использовать module2 без replace в go.mod!
6. Преимущества Workspace перед replace
| Workspace |
replace в go.mod |
| Глобальная настройка для всех модулей |
Требует правки в каждом go.mod |
Не нужно коммитить изменения в go.mod |
replace должен быть в go.mod |
| Удобен для монолитных репозиториев |
Подходит для временных изменений |
7. Отключение Workspace
Если нужно временно отключить Workspace:
Вернуть обратно:
go env -w GOWORK=$(pwd)/go.work
8. Советы по работе с Workspace
- Не коммитьте
go.work в общий репозиторий (если это не шаблон для команды).
- Используйте Workspace для локальной разработки, а
go.mod — для продакшена.
go work sync помогает при обновлении зависимостей.
10.6 - массивы (arrays) и срезы (slices)
В Go массивы (arrays) и срезы (slices) — это разные структуры данных, а len и cap — функции, которые работают со срезами (и массивами, но для массивов они всегда равны).
🔹 Массив vs Срез
| Характеристика |
Массив ([n]T) |
Срез ([]T) |
| Размер |
Фиксированный (известен на этапе компиляции) |
Динамический (может расти и уменьшаться) |
| Передача в функцию |
По значению (копируется) |
По ссылке (указатель на массив) |
| Инициализация |
arr := [3]int{1, 2, 3} |
sl := []int{1, 2, 3} или make([]int, len, cap) |
| Гибкость |
Неизменяемый размер |
Можно изменять с помощью append |
Примеры:
// Массив (фиксированная длина)
arr := [3]int{1, 2, 3} // len=3, cap=3 (для массива len == cap)
// Срез (динамическая длина)
sl := []int{1, 2, 3} // len=3, cap=3
sl = append(sl, 4) // len=4, cap=6 (удвоился)
🔹 len vs cap
Обе функции применяются к срезам (и массивам), но имеют разный смысл:
len(s) — текущее количество элементов в срезе.
cap(s) — максимальное количество элементов, которое может вместить базовый массив без переаллокации.
Пример:
sl := make([]int, 2, 5) // len=2, cap=5
fmt.Println(len(sl)) // 2 (элементы: [0, 0])
fmt.Println(cap(sl)) // 5 (может вместить ещё 3 элемента)
sl = append(sl, 1, 2, 3) // len=5, cap=5
sl = append(sl, 4) // len=6, cap=10 (удваивается при переполнении)
Вывод:
- Массивы — статические, срезы — динамические.
len — текущая длина, cap — ёмкость внутреннего массива.
append может увеличить len, а если места нет (len == cap), то Go выделит новый массив (обычно с удвоенной cap).
10.7 - Назначение пакетов net и net/http
В языке Go пакеты net и net/http тесно связаны, но имеют разные уровни абстракции и предназначены для разных задач.
1. Пакет net
Для чего используется:
- Предоставляет низкоуровневые сетевые примитивы для работы с различными сетевыми протоколами (TCP, UDP, IP, Unix-сокеты, DNS и др.).
- Позволяет создавать серверы и клиенты на уровне сокетов.
- Используется, когда нужен полный контроль над сетевым взаимодействием.
Основные возможности:
- Работа с TCP (
net.TCPConn, net.ListenTCP)
- Работа с UDP (
net.UDPConn)
- Разрешение DNS (
net.LookupHost, net.ResolveIP)
- Управление сетевыми интерфейсами (
net.Interface)
Пример TCP-сервера:
package main
import (
"net"
)
func main() {
ln, _ := net.Listen("tcp", ":8080")
for {
conn, _ := ln.Accept()
go func(c net.Conn) {
c.Write([]byte("Hello from TCP server\n"))
c.Close()
}(conn)
}
}
2. Пакет net/http
Для чего используется:
- Реализует высокоуровневый HTTP-стек (клиент и сервер).
- Подходит для создания веб-серверов, REST API, работы с HTTP-запросами.
- Включает роутинг, middleware, куки, сессии и другие HTTP-специфичные функции.
Основные возможности:
- HTTP-сервер (
http.Server, http.HandleFunc)
- HTTP-клиент (
http.Client, http.Get, http.Post)
- Работа с заголовками, куками, формами
- Поддержка HTTPS (через
http.ListenAndServeTLS)
Пример HTTP-сервера:
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from HTTP server"))
})
http.ListenAndServe(":8080", nil)
}
Ключевые отличия
| Критерий |
net |
net/http |
| Уровень |
Низкоуровневый (TCP, UDP, IP) |
Высокоуровневый (HTTP/HTTPS) |
| Использование |
Сокеты, DNS, RAW-соединения |
Веб-серверы, API, HTTP-клиенты |
| Сложность |
Требует больше кода |
Упрощает HTTP-взаимодействие |
| Примеры |
Чат-сервер, DNS-резолвер |
Веб-сайт, RESTful API |
Когда что использовать?
net – если нужен контроль над сетевым уровнем (например, пишете свой протокол поверх TCP/UDP).
net/http – для стандартных HTTP-задач (веб-сервисы, API, скачивание данных).
Иногда их комбинируют: например, net/http использует net под капотом для работы с TCP-соединениями.