💡 Как можно тривиально реализовать типы sum/option на чистом Go, без использования сторонних пакетов или других хаков?
◆ Например, вы захотели, чтобы функция возвращала либо
◆ Тип
📌 Возможно ли это в Go?
◆ Да, но для этого нужно немного изменить концепцию интерфейсов. Интерфейсы описывают общее поведение, перечисляя одну или несколько функций. Каждый тип, который реализует эти функции, является экземпляром этого интерфейса.
◆ Однако тип
◆ Взгляните на этот интерфейс:
◆ Функция
◆ И обратите внимание, что
◆ Два варианта,
◆ Вот и все! Наш тип
◆ Опытные разработчики могут сказать, что возвращать интерфейс считается плохим стилем! И на это есть причина: если вызывающий получает обратно интерфейс, ему придется анализировать возвращаемое значение, чтобы определить конкретный тип за интерфейсом.
◆ Однако этот анализ возвращаемого значения является намеренной частью подхода типа
◆ Не волнуйтесь, определение варианта и действия на его основе требуют только переключения по типу возвращаемого значения и отдельного
◆ Но будьте осторожны: где бы вы ни писали такой переключатель типа
◆ К счастью, для этого есть линтер go-check-sumtype:
#tip
◆ Например, вы захотели, чтобы функция возвращала либо
data value
, либо error
. Вот как это может выглядеть псевдокодом:func f() Option {
result, err := DoSomething()
if err != nil {
return Error("oops:", err)
}
return Data(result)
}
◆ Тип
Option
объединяет два варианта: Data
и Error
. Если все идет хорошо, функция возвращает Data
, в противном случае — Error
.📌 Возможно ли это в Go?
◆ Да, но для этого нужно немного изменить концепцию интерфейсов. Интерфейсы описывают общее поведение, перечисляя одну или несколько функций. Каждый тип, который реализует эти функции, является экземпляром этого интерфейса.
◆ Однако тип
sum
не обязательно имеет какие-либо функции для реализации. Ему нужно только представлять разные виды значений. Поэтому нам придется использовать пустой интерфейс. Но это тоже не сработает: любой тип удовлетворяет пустому интерфейсу. ◆ Взгляните на этот интерфейс:
type Option interface {
isOption()
}
◆ Функция
isOption()
служит только для того, чтобы сделать этот интерфейс отличным от пустого интерфейса. Только типы, реализующие isOption
, являются вариантами Option
.◆ И обратите внимание, что
isOption()
не экспортируется. Это предотвращает добавление вариантов к типу Option
сторонним кодом. Другими словами, эта функция «запечатывает» интерфейс Option
.◆ Два варианта,
Data
и Error
, следовательно, реализуются следующим образом:type Data[T any] struct {
Value T
}
func (Data[T]) isOption() {}
type Error struct {
Err error
}
func (Error) isOption() {}
◆ Вот и все! Наш тип
Option
готов к использованию. Вот функция, которая возвращает тип Option
вместо известной пары (value
, error
):func DoSomething(b bool) Option {
if b {
return Data[int]{Value: 42}
}
return Error{
Err: fmt.Errorf("oops"),
}
}
◆ Опытные разработчики могут сказать, что возвращать интерфейс считается плохим стилем! И на это есть причина: если вызывающий получает обратно интерфейс, ему придется анализировать возвращаемое значение, чтобы определить конкретный тип за интерфейсом.
◆ Однако этот анализ возвращаемого значения является намеренной частью подхода типа
sum
в Go. Разные варианты типа sum
требуют разных действий, поэтому естественной частью обработки типа sum
является ветвление в обработке возвращаемого типа, специфичного для варианта.◆ Не волнуйтесь, определение варианта и действия на его основе требуют только переключения по типу возвращаемого значения и отдельного
case
для каждого из возможных вариантов, вот так:func main() {
opt := DoSomething(true)
switch option := opt.(type) {
case Data[int]:
fmt.Println(option.Value)
case Error:
fmt.Println(option.Err)
}
default:
}
}
◆ Но будьте осторожны: где бы вы ни писали такой переключатель типа
sum
, вы должны включить все варианты как case
.◆ К счастью, для этого есть линтер go-check-sumtype:
//sumtype:decl
type Option interface {
isOption()
}
#tip
GitHub
GitHub - alecthomas/go-check-sumtype: A simple utility for running exhaustiveness checks on Go "sum types."
A simple utility for running exhaustiveness checks on Go "sum types." - alecthomas/go-check-sumtype
💡Стек или куча?
🤔 Живет ли переменная на стеке вызовов, или она динамически выделена в куче?
В большинстве случаев вам не стоит беспокоиться об этом. Go собирает мусор и автоматически очищает неиспользуемые переменные.
Однако сборка мусора имеет свою цену, поэтому чем меньше выделений делает ваш код, тем быстрее он может работать.
📌 Как узнать, выделяется ли переменная в куче?
Некоторые операции по умолчанию вызывают выделение памяти в куче и поэтому легко обнаруживаются и исправляются. Вот несколько примеров:
🔸Строковые переменные неизменяемы. Конкатенация двух строк приводит к новой аллокации и сборке мусора. В качестве альтернативы можно использовать
🔸Срезы, которые растут за пределы своей емкости, реаллоцируются. Решение: предварительно выделить срез с помощью
🔸Когда функция создает локальную переменную и возвращает указатель на эту переменную, переменная должна быть выделена в куче.
📌 Однако есть ситуации, когда выделения в куче неочевидны. Подумайте об указателях, скрытых внутри других типов данных, таких как срезы или мапы. Или рассмотрите массивы. Если массив слишком большой, чтобы жить на стеке, он выделяется в куче.
📌 Как найти эти случаи выделения в куче?
Запустите или скомпилируйте свой код с флагом сборки мусора "
#tip
🤔 Живет ли переменная на стеке вызовов, или она динамически выделена в куче?
В большинстве случаев вам не стоит беспокоиться об этом. Go собирает мусор и автоматически очищает неиспользуемые переменные.
Однако сборка мусора имеет свою цену, поэтому чем меньше выделений делает ваш код, тем быстрее он может работать.
📌 Как узнать, выделяется ли переменная в куче?
Некоторые операции по умолчанию вызывают выделение памяти в куче и поэтому легко обнаруживаются и исправляются. Вот несколько примеров:
🔸Строковые переменные неизменяемы. Конкатенация двух строк приводит к новой аллокации и сборке мусора. В качестве альтернативы можно использовать
strings.Builde
r.🔸Срезы, которые растут за пределы своей емкости, реаллоцируются. Решение: предварительно выделить срез с помощью
make(
)
.🔸Когда функция создает локальную переменную и возвращает указатель на эту переменную, переменная должна быть выделена в куче.
📌 Однако есть ситуации, когда выделения в куче неочевидны. Подумайте об указателях, скрытых внутри других типов данных, таких как срезы или мапы. Или рассмотрите массивы. Если массив слишком большой, чтобы жить на стеке, он выделяется в куче.
📌 Как найти эти случаи выделения в куче?
Запустите или скомпилируйте свой код с флагом сборки мусора "
-
m", и команда Go выведет заметку каждый раз, когда переменная перемещается или уходит со стека в кучу:go run -gcflags "-m"
или
go tools compile -m
#tip
◆ Embedding (встраивание) структур позволяет одной структуре наследовать поля и методы другой, делая их частью себя. Это может использоваться для создания новых типов данных, которые включают в себя свойства и функциональность существующих структур.
📌 Предположим, у вас есть структура:
type Person struct {
Name string
Address string
SSN string
}
и вы хотите создать тип
customer
.type Customer struct {
Person
ID int
}
◆
Person
является embedded структурой. Все ее поля и методы встраиваются во внешнюю структуру.◆ Приложению, которому нужен тип
Customer
, не интересен номер социального страхования клиента и не должен быть, но SSN неизбежно встраивается в Customer
вместе с полями Name
и Address
.◆ Любой код, имеющий доступ к переменной
Customer
, таким образом, имеет доступ и к SSN
клиента.func main() {
p := Person{
Name: "John Doe",
Address: "Doe Blvd, Doe City"
SSN: "123-45-6789",
}
c := Customer{
Person: p,
}
fmt.Println(c.SSN)
}
◆ Будьте осторожны с встраиванием структур. Это очень полезная техника, но вы можете случайно открыть доступ к полям или функциям, к которым клиентский код не должен иметь доступа.
#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
context.Context
?HTTP-обработчики могут использовать контекст для управления тайм-аутами запросов или отменой в рамках запроса. Тип
Context
также позволяет передавать значения (например, идентификатор пользователя, связанный с запросом) другим функциям.❓Следует ли использовать этот механизм для передачи информации по цепочке вызовов запроса? Или лучше использовать обычную структуру?
Вот две причины, по которым использование
Context
для передачи значений может быть плохой идеей:Значения в
Context
представляют собой пары ключ/значение, где ключ и значение являются пустыми интерфейсами (т. е. any
). Другими словами, значения в контексте — это как мешок со всем подряд. Компилятор не может помочь вам отловить ошибки типов или даже проверить, есть ли информация. Удачи в устранении неполадок 😉Если вы видите функцию, принимающую параметр
Context
, вы не можете сказать, что внутри. Если вы видите функцию, которая принимает параметр структуры, вы можете ясно видеть, что данные передаются, и что это за данные.❓Но разве
doSomething(ctx)
не выглядит намного чище, чем doSomething(ctx, someStruct)
?Чистый код сам по себе не является целью. Код не чист только потому, что он короткий. Код чистый, если читатель может ясно видеть, что он делает.
❓Так
context.WithValue()
следует избегать?Значения в контексте могут быть полезны, если они не критичны для бизнес-логики приложения. Например, совершенно нормально передавать идентификаторы запросов для логирования или измерения метрик. Читатель все равно сможет понять логику кода, и если что-то пойдет не так с этим идентификатором, это повлияет только на логирование или метрики приложения, но не на результат запроса.
Context
.#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
🔗 pkg.go.dev
Здесь вы можете найдете пакеты по различным критериям и получите информацию о версии, лицензии и многом другом.
🔗 GitHub
Вводите в поисковую строку
language:go
+ свой запрос и анализируете результаты.🔗 Awesome Go
Один из самых старых и самых поддерживаемых кураторских списков по Go.
🔗 Libhunt.com
Коллекция Go-проектов, отфильтрованных по количеству упоминаний, звезд или другим критериям.
🔗 go-recipes
Тщательно подобранный список полезных Go-инструментов, разделенных на категориям.
#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
pkg.go.dev
Go Packages - Go Packages
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.
s := a[3:7]
Она создает срез из массива
a
, начиная с третьего элемента (с нулевым индексом) и до, но не включая, элемента седьмого элемента.Таким образом, «последний» индекс кажется указывающим за пределы
a
.Вместо того, чтобы напрямую сопоставлять индекс с элементом...
| G | o | p | h | e | r | s | |
^ ^ ^ ^ ^ ^ ^ ^
0 1 2 3 4 5 6 7?
...представьте индексы, как будто они находятся между элементами.
| G | o | p | h | e | r | s |
^ ^ ^ ^ ^ ^ ^ ^
0 1 2 3 4 5 6 7
Тогда срез
a[3:7]
вполне естественно относится к элементам между индексом №3 и индексом №7.#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
В Go новички часто сталкиваются с проблемой интерфейсных переменных, которым присваивае
тся
nil указатель. В таком случае, хотя значение в интерфейсе является
nil, сама переменная интерфейса не равна
nil.Пример: создаем перемен
н
ую x как указатель на
int, который по умолчанию
nil, и переменн
ую y как пустой интерфейс, который тоже
nil по умолчанию. После присваиван
ия x переменн
ой y, интерфе
йс y уже не является
nil, хо
тя x все еще
nil.
var x *int
var y any
y = x
📌 Что верне
т y == n
il? Верне
т fal
se. Это потому, что интерфейс не просто представляет значение, которое ему присвоено, а действует как контейнер для этого значения.Для проверки, является ли значение в интерфейс
е n
il, нужно использовать утверждение типа. Например, для проверки
y на n
il, используем:
y.(*int) == nil
Это показывает, что интерфейс
y
не nil
, но содержащееся в нем значение — nil
. Полный пример здесь.#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
go.dev
Go Playground - The Go Programming Language
В настоящее время, в отличие от Python или NodeJS, Go не позволяет указывать зависимости разработки отдельно от зависимостей приложения. Однако автор предпочитает явно указывать зависимости разработки для лучшей воспроизводимости.
Работая над новым CLI-инструментом для проверки неработающих URL-адресов в файлах markdown, автор столкнулся с интересным соглашением: можно указать зависимости разработки в файле
tools.go
и затем исключить их при сборке бинарного файла, используя тег сборки.Вот как это работает. Предположим, у нашего проекта
foo
есть следующая структура:foo
├── go.mod
├── go.sum
└── main.go
Файл
main.go
содержит простую функцию "hello-world", использующую стороннюю зависимость:package main
import (
"fmt"
// Cowsay - это сторонняя зависимость приложения
cowsay "github.com/Code-Hex/Neo-cowsay"
)
func main() {
fmt.Println(cowsay.Say(cowsay.Phrase("Hello, World!")))
}
Здесь
Neo-cowsay
— это зависимость приложения. Для инициализации проекта запускаются следующие команды:go mod init example.com/foo # создает файлы go.mod и go.sum
go mod tidy # устанавливает зависимости приложения
Теперь предположим, что мы хотим добавить следующие зависимости разработки:
golangci-lint
для линтинга проекта в CI и gofumpt
как более строгий gofmt
. Поскольку эти инструменты не импортируются напрямую, они не отслеживаются инструментарием сборки.Но можно воспользоваться следующим воркфлоу:
1. Разместить файл
tools.go
в корневой директории.2. Импортировать зависимости разработки в этом файле.
3. Запустить
go mod tidy
, чтобы отслеживать как зависимости приложения, так и зависимости разработки через go.mod
и go.sum
.4. Указать тег сборки в
tools.go
, чтобы исключить зависимости разработки из бинарного файла.В этом случае файл
tools.go
выглядит следующим образом:// go:build tools
package tools
import (
// Зависимости разработки
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "mvdan.cc/gofumpt"
)
Теперь, если вы запустите
go mod tidy
, инструментарий Go будет отслеживать зависимости через файлы go.mod
и go.sum
. Вы можете проверить зависимости в go.mod
.Хотя зависимости разработки отслеживаются вместе с зависимостями приложения, тег сборки
// go:build tools
в начале файла tools.go
скажет инструментарию сборки игнорировать их при создании бинарного файла.Из корневой директории
foo
можно собрать проект, запустив:go build main.go
Это создаст бинарный файл
main
в корневой директории. Чтобы убедиться, что бинарный файл не содержит зависимости разработки, запустите:go tool nm main | grep -Ei 'golangci-lint|gofumpt'
Это не вернет ничего, если зависимости разработки не упакованы в бинарный файл.
Но если вы сделаете это для зависимости приложения, она выведет артефакты:
go tool nm main | grep -Ei 'cowsay'
Команда выведет:
1000b6d40 T github.com/Code-Hex/Neo-cowsay.(*Cow).Aurora
1000b6fb0 T github.com/Code-Hex/Neo-cowsay.(*Cow).Aurora.func1
1000b5610 T github.com/Code-Hex/Neo-cowsay.(*Cow).Balloon
1000b6020 T github.com/Code-Hex/Neo-cowsay.(*Cow).Balloon.func1
...
Если по какой-то причине вы хотите включить зависимости разработки в свой бинарный файл, вы можете передать тег
tools
при сборке бинарного файла:go build --tags tools main.go
#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
Redowan's Reflections
Omitting dev dependencies in Go binaries
As of now, unlike Python or NodeJS, Go doesn’t allow you to specify your development
dependencies separately from those of the application. However, I like to specify the dev
dependencies explicitly for better reproducibility.
While working on a new CLI tool…
dependencies separately from those of the application. However, I like to specify the dev
dependencies explicitly for better reproducibility.
While working on a new CLI tool…
⚒️ Jaegar + Open Telemetry в действии: простой пример для Go-разработчика
📌 Пример файла Docker Compose для запуска Jaeger:
📌 Пример минимального Go-приложения для демонстрации интеграции Open Telemetry:
👉 Источник
#tip
📌 Пример файла Docker Compose для запуска Jaeger:
version: '3'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
- "14268:14268" # Collector
- "14250:14250" # gRPC
- "9411:9411" # Zipkin
📌 Пример минимального Go-приложения для демонстрации интеграции Open Telemetry:
package main
import (
"context"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"log"
"math/rand"
"net/http"
"time"
)
func main() {
// Initialize Jaeger Exporter
exporter, err := jaeger.New(jaeger.WithCollectorEndpoint())
if err != nil {
log.Fatal(err)
}
// Create Trace Provider
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("app-one"),
)),
)
otel.SetTracerProvider(tp)
http.Handle("/", otelhttp.NewHandler(http.HandlerFunc(SimpleHandler), "Hello"))
log.Fatal(http.ListenAndServe(":8081", nil))
}
func SimpleHandler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Hello, World!"))
}
👉 Источник
#tip
🤔 В чем сила пакета singleflight?
🔸
🔸Если несколько запросов инициируют один и тот же вызов, это может привести к множественным идентичным вызовам к БД или API. Это создает нагрузку на систему, ведет к потерям CPU, памяти и пропускной способности сети.
🔸Matthew Boyle, автор Domain-Driven Design with Golang, приводит пример использования
💡
#tip
🔸
singleflight
предоставляет механизм подавления дублирующихся вызовов функций. Например, наше приложение запрашивает данные из API или базы данных. 🔸Если несколько запросов инициируют один и тот же вызов, это может привести к множественным идентичным вызовам к БД или API. Это создает нагрузку на систему, ведет к потерям CPU, памяти и пропускной способности сети.
🔸Matthew Boyle, автор Domain-Driven Design with Golang, приводит пример использования
singleflight
для устранения дублирующихся вызовов. В примере, несмотря на то, что 5 горутин одновременно запрашивают данные для одного и того же ключа, функция fetchData
будет вызвана только один раз благодаря функции group.Do
из пакета singleflight
.💡
singleflight
также может используется в serverless кейсах. Google App Engine, например, применяет его как часть функции инициализации, поскольку там нет main.go
.#tip
Это позволяет обрабатывать данные построчно, упрощает использование CLI-инструментов (grep, awk, wc) и уменьшает неоднозначность по сравнению с CSV. Каждая строка JSONL может содержать более сложные данные, чем CSV-строка.
#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
Value receivers и nil
Представьте себе структуру с двумя методами: один использует pointer receiver, а другой — value receiver.
Что происходит, если receiver равен
Переменная
Однако, если мы выполняем этот код, вызов
📌 Что происходит?
Рассмотрим, что методы — это просто функции с некоторым синтаксическим сахаром. Метод
Таким образом, вышеуказанный код может быть переписан без методов следующим образом:
Теперь должно быть легко понять, почему
Для метода
Так что, если у вас есть тип с pointer/value receivers, будьте осторожны, чтобы не вызывать какие-либо методы для
#tip
Представьте себе структуру с двумя методами: один использует pointer receiver, а другой — value receiver.
package main
type S struct {
N int
}
func (s *S) PointerRcv() {
}
func (s S) ValueRcv() {
}
Что происходит, если receiver равен
nil
?
func main() {
var s *S // s равно nil
s.PointerRcv()
s.ValueRcv()
}
Переменная
s
принимает нулевое значение типа *S
, которое является nil
. Поскольку ни один из методов не обращается к receiver'у, оба вызова метода должны пройти без проблем.Однако, если мы выполняем этот код, вызов
s.ValueRcv()
вызовет панику!📌 Что происходит?
Рассмотрим, что методы — это просто функции с некоторым синтаксическим сахаром. Метод
func (s S) f()
семантически идентичен функции func f(s S)
. Method receiver становится первым аргументом функции.Таким образом, вышеуказанный код может быть переписан без методов следующим образом:
package main
type S struct {
N int
}
func PointerFunc(s *S) {
}
func ValueFunc(s S) {
}
func main() {
var s *S
PointerFunc(s)
ValueFunc(*s)
}
Теперь должно быть легко понять, почему
ValueFunc()
вызывает панику. Указатель s
должен быть разыменован при передаче его в ValueFunc()
. Разыменование nil
указателя невозможно и приводит к панике.Для метода
func (s *S) PointerRcv()
, receiver (или параметр функции во втором примере) не нуждается в разыменовании. Следовательно, паники не будет.Так что, если у вас есть тип с pointer/value receivers, будьте осторожны, чтобы не вызывать какие-либо методы для
nil
значения этого типа.#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
💡 Если у вас запущено множество локальных серверов, и вам надоело обращаться к ним как
Предположим, у вас есть локальный сервер на порту 9000. После установки Caddy, выполните команду:
и откройте https://myserver.localhost. Вы увидите, что сервер на
А если вы хотите проксировать больше серверов таким образом, создайте файл с именем
#tip
localhost:8081
, localhost:9000
и т. д., посмотрите в сторону Caddy. Он сделает настройку «доменов» для локальных серверов проще простого.Предположим, у вас есть локальный сервер на порту 9000. После установки Caddy, выполните команду:
caddy reverse-proxy --from myserver.localhost --to :9000
и откройте https://myserver.localhost. Вы увидите, что сервер на
localhost:9000
отвечает. Caddy даже предоставляет локальные TLS-сертификаты. А если вы хотите проксировать больше серверов таким образом, создайте файл с именем
Caddyfile
и введите конфигурацию хоста следующим образом:
myapp.localhost {
reverse-proxy :9000
}
myhugoblog.localhost {
reverse-proxy :1313
}
#tip
Вы наверняка знакомы со «стандартным» способом ветвления кода в зависимости от заданного значения:
Так работает
Но оператор
1. Несколько значений в одном
В
Заданное значение может использоваться только в одном блоке
2. Инициализатор, как в цикле
Вы можете инициализировать значение перед использованием его в
3. Нет выражения
Если текущее значение
4. Переключение по типу переменной.
Если ваш код получает значение интерфейса из какого-то источника, вы можете использовать переключение по типу, чтобы проверить фактический тип этого значения:
5. Переключение по типу параметра.
Это может показаться немного эзотерическим: вы можете определить дженерик функцию, где типовой параметр не используется в списке параметров. Вместо этого оператор
Как и с выражениями
👉 Go Playground
#tip
switch a {
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("default")
}
Так работает
switch
в Go и во многих других языках (за исключением того, что в Go не происходит перехода к последующим case
).Но оператор
switch
может делать больше. Вот несколько кейсов.1. Несколько значений в одном
case
.В
case
можно указать несколько значений для сопоставления:
switch a {
case 1:
fmt.Println("1")
case 2, 3, 4:
fmt.Println("2, 3 или 4")
// case 1,2: // ошибка: дублирование case 1, дублирование case 2
// fmt.Println("1 или 2")
}
Заданное значение может использоваться только в одном блоке
case
. Дублирование значений в case
вызовет ошибку.2. Инициализатор, как в цикле
for
.Вы можете инициализировать значение перед использованием его в
switch
. Область видимости переменной a
ограничена конструкцией switch
:
switch a := f(); a {
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
}
3. Нет выражения
switch
, но есть выражения case
.case
не ограничен статическими значениями. Если вы опустите выражение switch
, вы можете использовать выражения для каждого case
:
switch {
case a == 1:
fmt.Println("1")
case a >=2 && a <= 4:
fmt.Println("2")
case a <= 5:
fmt.Println("3")
}
Если текущее значение
a
совпадает более чем с одним case
, выбирается первый подходящий.4. Переключение по типу переменной.
Если ваш код получает значение интерфейса из какого-то источника, вы можете использовать переключение по типу, чтобы проверить фактический тип этого значения:
switch v := a.(type) {
case int:
fmt.Println("a — это int:", v)
case string, []byte:
fmt.Println("a — это string:", v)
}
5. Переключение по типу параметра.
Это может показаться немного эзотерическим: вы можете определить дженерик функцию, где типовой параметр не используется в списке параметров. Вместо этого оператор
switch
может ссылаться на этот параметр, чтобы проверить значение параметра:
func do[T comparable](a any) {
switch v := a.(type) {
case int:
fmt.Println("a — это int:", v)
case T:
fmt.Printf("a — это %T: %v", v, v)
case []T:
fmt.Println("a — это срез:", v)
case []byte:
fmt.Println("a — это срез байт:", v)
}
}
func main() {
do[bool](a)
do[bool](true)
do[int]([]int{1, 2, 3})
}
Как и с выражениями
case
, если фактический тип a
совпадает с несколькими case
, выбирается первый подходящий.👉 Go Playground
#tip
go.dev
Go Playground - The Go Programming Language
Флаг
запустит тесты 2 раза. Тесты сначала будут запущены с четырьмя процессорами, а затем второй раз — с пятью.
#tip
-cpu
можно использовать при запуске тестов Go, чтобы указать список значений GOMAXPROCS
, с использованием которых необходимо запустить тесты. Например,go test -cpu=4,5
запустит тесты 2 раза. Тесты сначала будут запущены с четырьмя процессорами, а затем второй раз — с пятью.
#tip
Please open Telegram to view this post
VIEW IN TELEGRAM
Кстати, функция
👉 Go Playground
#tip
LookupEnv
в Go может использоваться для определения того, установлена ли переменная окружения или нет. 👉 Go Playground
#tip
🚀 Оптимизация и стресс-тесты в Go с флагом -cpu
Когда речь идет о тестировании производительности и устойчивости приложений, работающих в многопоточной среде, Go предоставляет отличный инструмент — флаг -cpu. Этот параметр позволяет запускать тесты с разным количеством логических процессоров (GOMAXPROCS), моделируя реальное поведение приложения в условиях разного уровня параллелизма.
📌 Как работает флаг -cpu?
Флаг -cpu указывается при запуске тестов и задаёт список значений, с которыми тесты должны быть выполнены. Например:
👉 Этот пример запустит тесты дважды:
1️⃣ С четырьмя логическими процессорами.
2️⃣ Затем с пятью.
🛠 Зачем это использовать?
➖ Тестирование под разной нагрузкой: использование нескольких значений -cpu позволяет понять, как ваше приложение ➖ поведёт себя на системах с разным количеством ядер.
➖ Поиск узких мест: помогает выявить проблемы в конкурентном доступе, такие как гонки данных или узкие места в производительности.
➖ Реализм тестов: ваш код проверяется в условиях, максимально приближенных к реальной эксплуатации.
🔑 Ключевые моменты:
➖ Можно указать несколько значений через запятую, например -cpu=1,2,4,8, чтобы протестировать приложение в разнообразных сценариях.
➖ Если -cpu не задан, тесты запускаются с текущим значением GOMAXPROCS.
Практическая выгода: Регулярное использование -cpu в тестах повышает устойчивость вашего кода и предотвращает неожиданные проблемы при высоких нагрузках.
💡 Пример для продвинутых:
Если вы хотите протестировать код на нескольких уровнях параллелизма, запустите:
📊 Результат: тесты покажут, как приложение справляется с 1, 2, 4 и 8 логическими процессорами. Это отличный способ убедиться, что ваш код работает эффективно и безопасно в конкурентной среде.
#tip
Когда речь идет о тестировании производительности и устойчивости приложений, работающих в многопоточной среде, Go предоставляет отличный инструмент — флаг -cpu. Этот параметр позволяет запускать тесты с разным количеством логических процессоров (GOMAXPROCS), моделируя реальное поведение приложения в условиях разного уровня параллелизма.
Флаг -cpu указывается при запуске тестов и задаёт список значений, с которыми тесты должны быть выполнены. Например:
go test -cpu=4,5
👉 Этот пример запустит тесты дважды:
🛠 Зачем это использовать?
🔑 Ключевые моменты:
Практическая выгода: Регулярное использование -cpu в тестах повышает устойчивость вашего кода и предотвращает неожиданные проблемы при высоких нагрузках.
Если вы хотите протестировать код на нескольких уровнях параллелизма, запустите:
go test -cpu=1,2,4,8 -v
📊 Результат: тесты покажут, как приложение справляется с 1, 2, 4 и 8 логическими процессорами. Это отличный способ убедиться, что ваш код работает эффективно и безопасно в конкурентной среде.
#tip
Please open Telegram to view this post
VIEW IN TELEGRAM