Горячее
Лучшее
Свежее
Подписки
Сообщества
Блоги
Эксперты
Войти
Забыли пароль?
или продолжите с
Создать аккаунт
Я хочу получать рассылки с лучшими постами за неделю
или
Восстановление пароля
Восстановление пароля
Получить код в Telegram
Войти с Яндекс ID Войти через VK ID
Создавая аккаунт, я соглашаюсь с правилами Пикабу и даю согласие на обработку персональных данных.
ПромокодыРаботаКурсыРекламаИгрыПополнение Steam
Пикабу Игры +1000 бесплатных онлайн игр
 Что обсуждали люди в 2024 году? Самое время вспомнить — через виммельбух Пикабу «Спрятано в 2024»! Печенька облегчит поиск предметов.

Спрятано в 2024

Поиск предметов, Казуальные

Играть

Топ прошлой недели

  • AlexKud AlexKud 38 постов
  • SergeyKorsun SergeyKorsun 12 постов
  • SupportHuaport SupportHuaport 5 постов
Посмотреть весь топ

Лучшие посты недели

Рассылка Пикабу: отправляем самые рейтинговые материалы за 7 дней 🔥

Нажимая кнопку «Подписаться на рассылку», я соглашаюсь с Правилами Пикабу и даю согласие на обработку персональных данных.

Спасибо, что подписались!
Пожалуйста, проверьте почту 😊

Помощь Кодекс Пикабу Команда Пикабу Моб. приложение
Правила соцсети О рекомендациях О компании
Промокоды Биг Гик Промокоды Lamoda Промокоды МВидео Промокоды Яндекс Директ Промокоды Отелло Промокоды Aroma Butik Промокоды Яндекс Путешествия Постила Футбол сегодня

Golang + Программирование

С этим тегом используют

IT Программист Разработка IT юмор Python Картинка с текстом Юмор Все
54 поста сначала свежее
FreshAngry007
1 год назад

Go и Горутины⁠⁠

Горутины в языке программирования Go — это легковесные потоки, используемые для выполнения функций параллельно с другими горутинами в одном и том же адресном пространстве процесса. Горутины являются одной из ключевых особенностей Go и предоставляют очень эффективный способ для обработки параллельных задач и асинхронного выполнения.

Легковесность

Горутины очень легковесны по сравнению с традиционными потоками. Создание горутины требует всего несколько килобайт стека, который может увеличиваться и уменьшаться по мере необходимости. Это позволяет создавать тысячи и даже миллионы горутин в одном приложении без значительного потребления ресурсов.

Мультиплексирование на меньшем количестве ОС потоков

Горутины мультиплексируются на меньшем количестве потоков операционной системы. Это значит, что даже при блокировке одной горутины (например, при ожидании ввода/вывода), другие горутины продолжат выполняться на других потоках ОС, что обеспечивает высокую производительность и эффективность использования ресурсов.

Планировщик

Go имеет свой встроенный планировщик, который распределяет горутины по доступным потокам операционной системы. Планировщик использует механизм M:N, где M горутин мультиплексируются на N потоков операционной системы. Планировщик Go работает на уровне пользовательского пространства и оптимизирован для работы с большим количеством горутин.

Синхронизация и коммуникация

Для синхронизации и обмена данными между горутинами в Go используется концепция каналов. Каналы позволяют безопасно передавать сообщения между горутинами, обеспечивая при этом синхронизацию доступа к данным.

Каналы в Go — это средства для синхронизации и коммуникации между горутинами, позволяя им безопасно обмениваться данными. Работа с каналами в Go обеспечивает синхронизацию доступа к данным без необходимости использования блокировок или других механизмов синхронизации, что делает код более простым и безопасным.

Блокировка

  • Небуферизованные каналы блокируют отправляющую горутину до тех пор, пока другая горутина не прочитает из канала, и наоборот — получающая горутина блокируется до тех пор, пока значение не будет отправлено.

  • Буферизованные каналы позволяют отправлять значения в канал без блокировки до тех пор, пока буфер не будет заполнен. Аналогично, данные могут быть прочитаны из канала, если он не пуст.

Go и Горутины Golang, Программирование, IT, Длиннопост

Закрытие канала

Канал можно закрыть с помощью функции close, чтобы указать, что больше нет значений для отправки. После закрытия канала нельзя отправлять в него данные, но можно продолжать получать данные, которые были в нем до закрытия:

close(ch)

Попытка отправить данные в закрытый канал вызовет панику.

Проверка, закрыт ли канал

При чтении из канала можно использовать вторую переменную, чтобы проверить, закрыт ли канал:

value, ok := <-ch // ok будет false, если канал закрыт и в нем больше нет значений

Использование каналов для синхронизации

Каналы могут использоваться не только для обмена данными, но и для синхронизации выполнения горутин, например, чтобы дождаться завершения работы нескольких горутин перед продолжением выполнения основной программы.

Go и Горутины Golang, Программирование, IT, Длиннопост

package main

func main() {

done := make(chan bool)

go func() {

// Выполнение некоторой работы...

done <- true // Сигнализация о завершении работы

}()


<-done // Ожидание сигнала о завершении работы

}

Давайте рассмотрим простой, но весьма показательный пример, который может использоваться в продакшене: параллельную загрузку данных из нескольких источников с помощью горутин и каналов. Этот подход часто используется при работе с внешними API или при выполнении других I/O-операций, требующих асинхронности и конкурентности.

Цель

Мы хотим параллельно запросить данные из трех разных источников. Для упрощения примера представим, что эти "запросы" просто спят (time.Sleep) разное количество времени для имитации задержки сети. После "запроса" каждая горутина отправляет результат в канал. Основная горутина ожидает все результаты и затем продолжает выполнение.

Go и Горутины Golang, Программирование, IT, Длиннопост

package main

import (

"fmt"

"math/rand"

"time"

)

func fetchData(source string, ch chan<- string) {


time.Sleep(time.Duration(rand.Intn(3)) * time.Second)

ch <- source + " data"

}

func main() {

ch := make(chan string, 3)

go fetchData("Source 1", ch)

go fetchData("Source 2", ch)

go fetchData("Source 3", ch)

for i := 0; i < 3; i++ {

result := <-ch

fmt.Println(result)

}

fmt.Println("All data fetched")

}

Подводные камни

Утечки горутин

Горутины, которые никогда не завершаются, могут привести к утечкам памяти. Это часто происходит, когда горутина ожидает на канале, который никогда не будет закрыт, или ожидает на блокировке, которая никогда не освободится.

Go и Горутины Golang, Программирование, IT, Длиннопост

Как избежать: Убедитесь, что все горутины имеют чёткие условия завершения и что все каналы, на которых они ожидают, будут в какой-то момент закрыты.

Проблемы с синхронизацией и гонки данных

Доступ к общему состоянию из нескольких горутин без надлежащей синхронизации может привести к гонкам данных, что делает поведение программы непредсказуемым.

Go и Горутины Golang, Программирование, IT, Длиннопост

Как избежать: Используйте мьютексы (sync.Mutex) или каналы для синхронизации доступа к общим ресурсам.

Мёртвая блокировка (Deadlock)

Мёртвая блокировка может произойти, когда две или более горутин ожидают друг друга, образуя цикл ожидания, из которого невозможно выйти.

Go и Горутины Golang, Программирование, IT, Длиннопост

package main

func main() {

ch1 := make(chan int)

ch2 := make(chan int)

go func() {

<-ch1

ch2 <- 1

}()

go func() {

<-ch2

ch1 <- 1

}()

// Мёртвая блокировка: ни один из каналов не получит значение.

<-ch1 // fatal error: all goroutines are asleep - deadlock!

}

Как избежать

go vet — это инструмент командной строки в Go, предназначенный для анализа исходного кода на предмет общих ошибок, таких как гонки данных, неправильное использование синтаксиса, несоответствия типов и многое другое. Он не заменяет тесты, но может помочь выявить потенциальные проблемы в коде на ранних этапах разработки.

Go и Горутины Golang, Программирование, IT, Длиннопост

Race Detector - Запуск приложения с включенным детектором гонок (-race флаг компилятора) может помочь выявить некоторые виды блокировок и условий гонки, хотя его основная цель — обнаружение гонок данных.

Go и Горутины Golang, Программирование, IT, Длиннопост

package main

import (

"fmt"

"sync"

)

func main() {

var counter int

var wg sync.WaitGroup

for i := 0; i < 1000; i++ {

wg.Add(1)

go func() {

counter++

wg.Done()

}()

}

wg.Wait()

fmt.Println("Final counter value:", counter)

}

// go run -race main.go

/*

WARNING: DATA RACE

Read at 0x00c0000140b8 by goroutine 7:

main.main.func1()

main.go:14 +0x33

Final counter value: 820

Found 2 data race(s)

exit status 66

*/

Go и Горутины Golang, Программирование, IT, Длиннопост

правильный вариант

package main

import (

"fmt"

"sync"

)

func main() {

var counter int

var wg sync.WaitGroup

var mutex sync.Mutex // Добавляем мьютекс для синхронизации

for i := 0; i < 1000; i++ {

wg.Add(1)

go func() {

mutex.Lock()  // Захватываем мьютекс перед изменением счётчика

counter++

mutex.Unlock() // Освобождаем мьютекс после изменения счётчика

wg.Done()

}()

}

wg.Wait()

fmt.Println("Final counter value:", counter)

}

Дебаггеры и инструменты профилирования: Использование инструментов, таких как pprof или отладчиков, которые могут помочь вам визуализировать и анализировать блокировки и другие проблемы синхронизации в вашем коде.

Создадим файл main.go с простым кодом, который намеренно создаёт нагрузку на CPU и память, чтобы мы могли увидеть что-то интересное в отчётах pprof.

Go и Горутины Golang, Программирование, IT, Длиннопост

Этот код запускает бесконечный цикл, который выполняет функцию computeFunction, создающую нагрузку. Кроме того, он запускает HTTP-сервер с поддержкой pprof на порту 6060. Запустите программу

go run main.go

Откройте другой терминал и используйте go tool pprof для сбора различных видов профилей (CPU, память, блокировки и т. д.) с вашей работающей программы.

Профиль использования CPU

go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

Эта команда соберёт информацию о производительности CPU за последние 30 секунд.

Профиль использования памяти

go tool pprof http://localhost:6060/debug/pprof/heap

Эта команда соберёт информацию о выделении памяти программой.

После сбора профиля pprof откроет интерактивную консоль, в которой вы можете выполнять различные команды для анализа собранных данных, например:

  • top показывает топ функций по использованию ресурсов.

  • list [function name] показывает детализацию использования ресурсов для указанной функции.

  • web генерирует граф вызовов в виде SVG и открывает его в вашем браузере (требует Graphviz).

Установить Graphviz можно так

sudo apt-get update && sudo apt-get install graphviz

Эти команды помогут вам выявить узкие места в производительности и оптимизировать ваш код.

Go и Горутины Golang, Программирование, IT, Длиннопост

Когда вы используете pprof с Graphviz для визуализации графов на Debian (или любой другой Unix-подобной системе), местоположение сохранения графа зависит от того, как вы вызываете команду визуализации. Если вы используете команду web в интерактивном режиме pprof, она обычно открывает SVG-файл непосредственно в вашем браузере, не сохраняя его локально. Однако, если вы хотите сохранить граф в файл, вы можете использовать другие команды в pprof, такие как svg или png, для создания файла определенного формата. В интерактивном режиме pprof введите команду для сохранения в SVG:

(pprof) svg > cpu-usage.svg

Эта команда создаст файл cpu-usage.svg в текущем рабочем каталоге. Если вы хотите сохранить файл в другом месте, укажите полный путь. Аналогично, если вы предпочитаете другие форматы, такие как PNG, используйте команду png.

Go и Горутины Golang, Программирование, IT, Длиннопост

Внимательный анализ логики и дизайна: Часто лучший способ избежать мёртвых блокировок — это тщательно продумать дизайн вашей конкурентной логики, чтобы исключить взаимные блокировки на этапе планирования.

Показать полностью 12
[моё] Golang Программирование IT Длиннопост
3
FreshAngry007
1 год назад

Go и Generics⁠⁠

Дженерики (обобщенное программирование) были введены в язык программирования Go в версии 1.18. Они позволяют писать функции и типы данных, которые могут работать с различными типами данных без потери типобезопасности. До появления дженериков разработчики Go часто использовали интерфейсы и тип interface{} для создания функций и структур, способных работать с любыми типами данных, что могло привести к ошибкам времени выполнения из-за неправильного преобразования типов.

Синтаксис

Дженерики в Go определяются с использованием квадратных скобок [] после имени функции или типа. Внутри скобок указываются один или несколько типовых параметров, которые затем можно использовать в теле функции или типа как обычные типы данных.

Go и Generics Программирование, IT, Golang, Длиннопост

В этом примере T является типовым параметром, который может быть заменен любым типом данных. Ключевое слово any означает, что T может быть любым типом.

Преимущества

  • Повторное использование кода: Дженерики позволяют создавать гибкие функции и типы данных, которые можно использовать с различными типами данных без дублирования кода.

  • Типобезопасность: В отличие от использования interface{}, дженерики обеспечивают проверку типов на этапе компиляции, что уменьшает риск ошибок времени выполнения.

  • Производительность: Использование дженериков может улучшить производительность, так как избавляет от необходимости преобразования типов и позволяет компилятору оптимизировать генерируемый код.

Ограничения

  • Синтаксис дженериков может быть сложным для понимания, особенно для начинающих разработчиков.

  • Обобщенное программирование может усложнить структуру кода и его читаемость.

  • Не все существующие библиотеки и инструменты уже полностью поддерживают дженерики.

Несмотря на ограничения, введение дженериков в Go значительно улучшило возможности языка для создания более гибкого и безопасного кода.

Следующий код демонстрирует применение обобщенного программирования в Go с использованием типового параметра T, ограниченного интерфейсом Signed. Интерфейс Signed определяет типы, которые могут быть одним из подписанных (signed) целочисленных типов (int, int8, int16, int32, int64). Структура NumericBox использует этот типовый параметр, что позволяет создавать экземпляры NumericBox для любого подписанного целочисленного типа.

Go и Generics Программирование, IT, Golang, Длиннопост

package main

import (

"fmt"

)

type Signed interface {

~int | ~int8 | ~int16 | ~int32 | ~int64

}

// NumericBox - обобщенная структура, ограниченная для работы только с числовыми типами.

type NumericBox[T Signed] struct {

Value T

}

// NewNumericBox создает новый экземпляр NumericBox с заданным числовым значением.

func NewNumericBox[T Signed](value T) NumericBox[T] {

return NumericBox[T]{Value: value}

}

// GetValue возвращает числовое значение из NumericBox.

func (b NumericBox[T]) GetValue() T {

return b.Value

}

func main() {

int_32 := NewNumericBox(int32(123))

fmt.Println("IntBox:", int_32.GetValue()) // Вывод: IntBox: 123

int_64 := NewNumericBox(int64(-123))

fmt.Println("IntBox:", int_64.GetValue()) // Вывод: IntBox: -123

// Следующая строка вызовет ошибку компиляции, так как строка не является числовым типом.

// stringBox := NewNumericBox("Hello, Generics!") // string does not satisfy Signed (string missing in ~int | ~int8 | ~int16 | ~int32 | ~int64)

}

Давайте теперь напишем замер скорости работы, простой структуры, интерфейса и Ginerics

Go и Generics Программирование, IT, Golang, Длиннопост

package main

import (

"fmt"

"time"

)

type Animal interface { Get() int }

type Dog struct{ Value int }

func (d Dog) Get() int { return d.Value }

type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 }

type NumericBox[T Signed] struct { Value T }

// Функция, принимающая конкретный тип.

func printInt(i int) {

fmt.Println(i)

}

// функция, принимающая интерфейс Animal.

func printInterface(animal Animal) {

fmt.Println(animal.Get())

}

// Функция, принимающая экземпляр NumericBox с любым поддерживаемым числовым типом.

func printGinerics[T Signed](box NumericBox[T]) {

fmt.Println(box.Value)

}

func main() {

// Замер производительности при использовании конкретного типа.

startConcrete := time.Now()

for i := 0; i < 1000000; i++ {

printInt(i)

}

resultConcrete := time.Since(startConcrete)

// Замер производительности при использовании интерфейса.

startInter := time.Now()

for i := 0; i < 1000000; i++ {

printInterface(Dog{Value: i})

}

resultInter := time.Since(startInter)

// Замер производительности при использовании дженерика.

startGeneric := time.Now()

for i := 0; i < 1000000; i++ {

printGinerics(NumericBox[int]{Value: i})

}

resultGeneric := time.Since(startGeneric)

fmt.Printf("Using concrete type: %v\n", resultConcrete) // Using concrete type: 869.528353ms

fmt.Printf("Using interface: %v\n", resultInter) // Using interface: 877.926898ms

fmt.Printf("Using generic: %v\n", resultGeneric) // Using generic: 841.916956ms

}

В примере обнаружено, что использование дженериков (обобщенных типов) в Go приводит к самому быстрому времени выполнения по сравнению с использованием конкретных типов и интерфейсов. Это может быть связано с несколькими факторами:

  1. Меньшее количество преобразований типов: Когда вы используете дженерики, Go может генерировать код непосредственно для конкретного типа во время компиляции, что уменьшает необходимость в преобразованиях типов или упаковке/распаковке интерфейсов во время выполнения. Это делает операции более быстрыми.

  2. Прямой доступ к данным: В случае дженериков, доступ к данным происходит напрямую, без дополнительного уровня абстракции, который требуется при использовании интерфейсов. Это уменьшает накладные расходы и может привести к более высокой производительности.

  3. Отсутствие динамического поиска методов: Когда вы используете интерфейсы, Go должен выполнить поиск соответствующего метода в таблице виртуальных методов интерфейса во время выполнения. Дженерики же позволяют избежать этого, так как конкретная реализация метода известна на этапе компиляции.

  4. Компилятор оптимизирует код для дженериков: Компилятор Go может выполнять специализацию кода для дженериков, что означает, что он может генерировать оптимизированный код для каждого конкретного случая использования.

Показать полностью 3
[моё] Программирование IT Golang Длиннопост
5
FreshAngry007
1 год назад

Go и интерфейсы⁠⁠

В Go интерфейсы представляют собой типы, определяющие наборы методов. Они используются для обеспечения полиморфного поведения объектов. В отличие от многих других языков программирования, в Go не требуется явное указание на то, что структура реализует интерфейс. Если структура имеет все методы, описанные в интерфейсе, то считается, что она его реализует.

Основные принципы работы интерфейсов:

  1. Определение интерфейса: Интерфейс в Go описывается как набор методов. Структура или любой другой тип "реализует" интерфейс, если предоставляет реализацию всех методов, перечисленных в интерфейсе.

  2. Полиморфизм: Интерфейсы позволяют переменным иметь различные типы значения во время выполнения программы, что обеспечивает полиморфизм. Это значит, что функция, принимающая интерфейс в качестве аргумента, может работать с любым типом, который реализует этот интерфейс.

  3. Интерфейс{}: Пустой интерфейс interface{} не имеет методов и поэтому может представлять значение любого типа, включая встроенные типы данных. Он часто используется, когда точный тип данных заранее неизвестен.

Go и интерфейсы Golang, Программирование, IT, Интерфейс, Длиннопост

package main

import "fmt"

// Определение интерфейса Animal

type Animal interface {

Speak() string

}

// Реализация интерфейса Animal для структуры Dog

type Dog struct {}

func (d Dog) Speak() string {

return "Woof!"

}

// Реализация интерфейса Animal для структуры Cat

type Cat struct {}

func (c Cat) Speak() string {

return "Meow"

}

// Функция, принимающая интерфейс Animal

func printAnimalSound(a Animal) {

fmt.Println(a.Speak())

}

func main() {

dog := Dog{}

cat := Cat{}


// Оба типа, Dog и Cat, реализуют интерфейс Animal

// поэтому printAnimalSound может с ними работать

printAnimalSound(dog) // Woof!

printAnimalSound(cat) // Meow

}

В этом примере Dog и Cat являются разными типами, которые реализуют интерфейс Animal, поскольку оба предоставляют метод Speak(). Функция printAnimalSound может принимать любой тип, реализующий Animal.

Подводные камни.

Производительность. Использование интерфейсов несколько снижает производительность по сравнению с работой с конкретными типами из-за необходимости решения типов во время выполнения. Давайте рассмотрим ситуацию, где производительность критична, и использование интерфейсов может влиять на производительность из-за динамического определения типов и вызова методов через интерфейсы. В таких случаях прямое использование конкретных типов может быть более эффективным.

Go и интерфейсы Golang, Программирование, IT, Интерфейс, Длиннопост

package main


import (

"fmt"

"time"

)


// Animal интерфейс, который реализуют разные животные.

type Animal interface {

Say() string

}


// Dog структура, реализующая интерфейс Animal.

type Dog struct{}


func (d Dog) Say() string {

return "Woof!"

}


// функция, принимающая интерфейс Animal.

func makeNoise(animal Animal) {

fmt.Println(animal.Say())

}


// Функция, принимающая конкретный тип Dog.

func makeDogNoise(dog Dog) {

fmt.Println(dog.Say())

}


func main() {


dog := Dog{}


// Замер производительности при использовании интерфейса.

start_inter := time.Now()

for i := 0; i < 1000000; i++ {

makeNoise(dog)

}

result_iter := time.Since(start_inter)


// Замер производительности при использовании конкретного типа.

start := time.Now()

for i := 0; i < 1000000; i++ {

makeDogNoise(dog)

}

result_concrete := time.Since(start)


fmt.Printf("Using interface: %v\n", result_iter) // Using interface: 841.732596ms

fmt.Printf("Using concrete type: %v\n", result_concrete) // Using concrete type: 822.222233ms


}

В этом примере мы сравниваем время выполнения функций, которые работают с интерфейсом Animal и с конкретным типом Dog. Хотя разница в производительности может быть незначительной для малого количества вызовов, в высокопроизводительных системах или в системах с большим количеством вызовов эта разница может накапливаться.

Преимущества.

  1. Полиморфизм: Интерфейсы позволяют писать функции и методы, которые могут работать с любым типом данных, реализующим интерфейс, не заботясь о конкретной реализации. Это означает, что вы можете легко заменить одну реализацию другой без изменения кода, который использует интерфейс.

  2. Отделение интерфейса от реализации: Интерфейсы позволяют скрыть детали реализации за абстракцией. Это упрощает понимание кода, поскольку вам нужно сосредоточиться только на том, что делает код, а не как он это делает.

  3. Упрощение тестирования: Использование интерфейсов упрощает написание модульных тестов. Вы можете легко создать "моки" или фиктивные реализации интерфейсов для тестирования, не завися от реализаций, которые могут требовать внешних зависимостей (например, базы данных).

  4. Гибкость и расширяемость: Интерфейсы делают ваш код более гибким и легко расширяемым. Вы можете добавлять новые реализации интерфейсов без изменения существующего кода, что особенно полезно в больших и сложных системах.

  5. Взаимозаменяемость компонентов: Поскольку компоненты системы общаются через интерфейсы, вы можете легко заменять одни компоненты другими, которые реализуют те же интерфейсы. Это особенно ценно для создания плагинов, расширений или для поддержки различных вариантов конфигурации системы.

Показать полностью 2
Golang Программирование IT Интерфейс Длиннопост
0
FreshAngry007
1 год назад

Структуры и Go⁠⁠

В Go структуры являются композитным типом данных, позволяющим объединять данные различных типов в одну логическую группу. Структуры часто используются для представления объектов и записей.

Вот основы работы со структурами:

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост

package main

import (

"fmt"

)

// Этот код определяет структуру Person с 3-мя полями: Name (строка), Age (целое число) и hidden (булево значение).

// Приватные поля в Go определяются с помощью нижнего регистра первой буквы имени поля в структуре.

// Они не доступны за пределами пакета, в котором объявлена структура.

type Person struct {

hidden bool

Name string

Age  int

}

// К структурам можно привязывать методы, используя получатель (receiver):

func (p *Person) Greet() string {

return "Hello, " + p.Name

}

// Методы структуры могут иметь доступ к её приватным полям, что позволяет вам управлять доступом к этим полям через публичные методы.

func (e *Person) SetPrivateField(value bool) {

e.hidden = value

}

func (e *Person) GetPrivateField() bool {

return e.hidden

}

// В Go поддерживается встраивание структур что позволяет создавать сложные структуры данных и реализовывать наследование в стиле композиции

// Экземпляр Employee наследует поля и методы Person, к ним можно обращаться так, как если бы они были объявлены напрямую в Employee.

type Employee struct {

Person

Position string

}

func main() {

// Экземпляр структуры может быть создан следующим образом

p := Person{false, "John Doe", 30}


// или с использованием именованных полей для повышения читаемости

p = Person{Name: "John Doe", Age: 30}


// Доступ к полям структуры осуществляется через оператор точки

fmt.Println(p.Name) // Выведет: John Doe


// Вызов метода осуществляется так же, как и доступ к полям

message := p.Greet()

fmt.Println(message) // Выведет: Hello, John Doe


// Go поддерживает анонимные структуры, которые могут быть полезны для одноразовых структур данных:

var user = struct {

ID  int

Name string

}{ID: 1, Name: "John Doe"}

fmt.Println(user) // Выведет: {1 John Doe}

}

В Go методы структур могут иметь два типа приемников (receiver): по значению (копия структуры) и по указателю. Выбор между ними влияет на возможность метода изменять саму структуру и на производительность. Вот основные различия:

Приемник по значению (копия)

Когда метод принимает структуру по значению, он работает с копией этой структуры. Изменения, сделанные внутри метода, не отражаются на оригинальной структуре.

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост

Использование приемника по значению хорошо подходит для маленьких структур или в ситуациях, когда не требуется модифицировать саму структуру в методе.

Приемник по указателю

Когда метод принимает структуру по указателю, он может изменять саму структуру, поскольку работает не с копией, а с реальным адресом структуры в памяти.

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост

Использование приемника по указателю рекомендуется, когда:

  • Необходимо модифицировать саму структуру в методе.

  • Структура достаточно большая, и копирование её значений могло бы отрицательно сказаться на производительности.

Подводные камни

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост

package main

import "fmt"

type Example struct {

Count int

Name string

}

type Derived struct {

Example

Name string

}

type ExampleP struct {

Example *Example

}

func main() {

// Значение по умолчанию для структур

var ex Example

fmt.Println(ex.Count) // Выведет 0, значение по умолчанию для int

fmt.Println(ex.Name)  // Выведет "", значение по умолчанию для string

// Встраивание структур и теневая реализация

d := Derived{}

d.Name = "Derived"

d.Example.Name = "Base"

fmt.Println(d.Name, d.Example.Name) // Чтобы получить доступ к полю Name из встроенной структуры Example, необходимо явно указать путь к нему

// Неинициализированные указатели в структурах

exp := ExampleP{}

fmt.Println(exp.Example.Name) // Приведет к панике, так как ex.Nested == nil

// Сравнение структур

// Не будет работать для структур, содержащих срезы!

ex1 := Example{Count: 5}

ex2 := Example{Count: 5}

fmt.Println(ex1 == ex2) // Выведет "true", так как все поля совпадают

}

В Go, сравнение структур с помощью оператора == будет работать только если все поля структуры поддерживают операцию сравнения. Это означает, что структуры, содержащие следующие типы данных в качестве полей, не могут быть напрямую сравнены:

  1. Срезы (slices): Как вы уже заметили, срезы не поддерживают операцию сравнения. Попытка сравнить структуры, содержащие срезы, приведет к компиляционной ошибке.

  2. Мапы (maps): Также как срезы, мапы не поддерживают операцию сравнения. Структуры с полями типа мапа не могут быть сравнены с помощью ==.

  3. Функции (functions): Функции в Go также не могут быть сравнены напрямую, кроме сравнения на равенство или неравенство с nil.

  4. Каналы (channels): Хотя каналы могут быть сравнены на равенство или неравенство, включая сравнение с nil, сложные сравнения структур, содержащих каналы, могут быть неоднозначными в зависимости от конкретной логики.

  5. Интерфейсы с динамическими типами, не поддерживающими сравнение: Если поле интерфейса содержит значение динамического типа, который сам по себе не поддерживает сравнение, то структура, содержащая такое поле, также не может быть сравнена.

Если вам нужно сравнить структуры, содержащие такие типы данных, вам придется реализовать собственную логику сравнения, которая будет обходиться с каждым полем индивидуально, в зависимости от его типа и предполагаемого значения.

В Go, когда вы передаете структуру в функцию, по умолчанию происходит её копирование. Это значит, что внутри функции создается локальная копия структуры, и любые изменения, сделанные в этой копии, не отразятся на оригинальной структуре. Это поведение соответствует передаче аргументов по значению.

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост
Показать полностью 5
[моё] Golang Программирование Структура IT Длиннопост
21
FreshAngry007
1 год назад

Карты и Go⁠⁠

Карты в Go — это встроенный тип данных, предоставляющий возможность хранения пар ключ-значение. Они похожи на словари или ассоциативные массивы в других языках программирования.

Особенности карт в Go:

  • Динамический размер: Карты могут расти и уменьшаться по мере добавления и удаления элементов, что делает их гибкими для использования в ситуациях, когда количество элементов заранее неизвестно.

  • Типизированные ключи и значения: В картах Go и ключи, и значения могут быть почти любого типа, за исключением тех, которые содержат срезы, карты или другие несравнимые типы. Тип ключа и тип значения для карты указываются при её объявлении.

  • Быстрый доступ: Карты предоставляют быстрый доступ к значениям по ключам. Время доступа к элементу карты не зависит от размера карты, что делает их эффективным выбором для поиска данных.

Работа с картами:

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"fmt"

)

func main() {

// Объявление карты

var myMap map[int]string

fmt.Println(myMap) // map[]

// Инициализация карты

myMap = make(map[int]string)

fmt.Println(myMap) // map[]

// Или можно объявить и инициализировать карту одновременно

myMap = make(map[int]string)

fmt.Println(myMap) // map[]

/*

Или можно объявить и инициализировать карту одновременно с емкостью


В отличие от слайсов, у карт нет понятия внешней емкости, доступной для проверки с помощью функции cap().

Это означает, что вызов cap(myMap) будет ошибочным, так как функция cap() не применима к картам.


Начальная емкость карты в Go служит лишь подсказкой для реализации о том, сколько элементов ожидается в карте.

Это может помочь оптимизировать ее внутреннюю структуру и уменьшить количество аллокаций памяти в

начальной фазе использования карты.

*/

myMap = make(map[int]string, 5)

fmt.Println(myMap, len(myMap)) // map[] 0


// Или можно объявить и инициализировать карту одновременно значениями

myMap = map[int]string{1: "a", 2: "b", 3: "c"}

fmt.Println(myMap) // map[1:a 2:b 3:c]

key := 5

// Добавить или изменить значение для ключа "apple"

myMap[key] = "apple"

fmt.Println(myMap) // map[1:a 2:b 3:c 5:apple]

// Получить значение. Если ключ существует, `ok` будет true, иначе false.

if _, ok := myMap[key]; !ok {

panic(fmt.Sprintf("myMap: key %d not found", key)) // panic: myMap: key 6 not found, если ключа 6 не окажется в карте

}

// Удалить элемент с ключом "apple"

// Ну и ни чего не происходит если такого ключа нет

delete(myMap, 6)

fmt.Println(myMap) // map[1:a 2:b 3:c 5:apple]

// Итерация по карте

for k, v := range myMap {

fmt.Println(k, v)

}

}

Подводные камни

1. Изменение карты во время итерации

Модификация карты во время итерации по ней может привести к непредсказуемому поведению. Например, вы можете пропустить некоторые элементы или столкнуться с паникой в рантайме.

Это допустимо в Go и не приведет к непосредственной ошибке или панике.

В случае с Go, поведение при удалении элементов из карты во время итерации по ней довольно определенное и безопасное. Итератор карты в Go спроектирован так, чтобы обрабатывать изменения карты во время итерации, но это может привести к тому, что некоторые элементы будут пропущены в процессе итерации, если вы удаляете элементы впереди текущего элемента итерации.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

func main() {

m := map[int]string{1: "a", 2: "b", 3: "c"}

for k := range m {

if k == 2 {

delete(m, 3) // Попытка удалить ключ во время итерации

}

}

// Поведение не определено. Возможно пропуск элементов или другие непредвиденные результаты.

}

Решение.

Для безопасного удаления элементов из карты во время итераций: сначала соберите ключи элементов, которые вы хотите удалить, в отдельный слайс, а затем удалите их в отдельном цикле после завершения итерации по карте. Это гарантирует, что итерация по карте не будет нарушена изменениями карты.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import "fmt"

func main() {

m := map[int]string{1: "a", 2: "b", 3: "c"}

var keysToDelete []int

for k := range m {

if k == 2 {


// добавляем ключи для удаления

keysToDelete = append(keysToDelete, 3)

}

}

// Удаляем элементы после итерации

for _, k := range keysToDelete {

delete(m, k)

}

fmt.Println(m) // Вывод: map[1:a 2:b], элемент с ключом 3 удален

}

2. Неинициализированная карта

Попытка использования неинициализированной карты вызовет панику во время выполнения.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

func main() {

// Попытка использования неинициализированной карты вызовет панику во время выполнения.

var m map[int]string

m[1] = "apple" // panic: assignment to entry in nil map

// Чтобы избежать этого, карта должна быть инициализирована перед использованием

m = make(map[int]string)


m[1] = "apple" // OK

}

3. Проверка наличия ключа

При получении значения из карты, если ключ не существует, возвращаемое значение будет нулевым для типа значения карты. Это может ввести в заблуждение, если не проверять, действительно ли ключ существует.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"fmt"

)

func main() {

m := make(map[int]int)

k := 123

val := m[1] // val == 0, ключ 123 не существует, но 0 также может быть допустимым значением

fmt.Println(val)

// Лучше использовать двухзначную форму получения элемента:

if _, ok := m[k]; !ok {

fmt.Printf("map key: %d, not found\n", k)

}

}

4. Сортировка

В Go карты не имеют внутреннего порядка, и их элементы хранятся в случайном порядке. Однако вы можете отсортировать ключи (или значения) карты, используя дополнительные шаги. Вот как можно отсортировать карту по ключам:

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"fmt"

"sort"

)

func main() {

m := map[string]int{

"banana": 3,

"apple":  5,

"pear":  2,

"orange": 4,

}

// Создаем слайс для хранения ключей

var keys []string

// Добавляем ключи в слайс

for k := range m {

keys = append(keys, k)

}

// Сортируем ключи

sort.Strings(keys)


// Выводим отсортированную карту

for _, k := range keys {

fmt.Println(k, m[k])

}

}

5. Конкурентная модификация

Карты в Go не являются потокобезопасными, и одновременная модификация карты из разных горутин может привести к состоянию гонки и панике.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import "time"

func main() {

m := make(map[int]int)

// Имитация конкурентной модификации

go func() {

for i := 0; i < 10000; i++ {

m[i] = i

}

}()

go func() {

for i := 0; i < 10000; i++ {

delete(m, i)

}

}()

go func() {

for i := 0; i < 10000; i++ {

delete(m, i)

}

}()

// Возможна паника или другое непредсказуемое поведение

time.Sleep(time.Second * 100)

}

Решение.

Для предотвращения конкурентной модификации карты и обеспечения потокобезопасности, можно использовать мьютекс из пакета sync. Мьютекс позволяет блокировать доступ к ресурсу (в данном случае к карте) для всех горутин, кроме одной, которая в данный момент владеет мьютексом. Это гарантирует, что только одна горутина может изменять карту в любой момент времени.

Пример решения с использованием мьютекса:

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"sync"

"time"

)

func main() {

var m = make(map[int]int) // Создаем карту

var mutex sync.Mutex // Создаем мьютекс

// Горутина для добавления элементов в карту

go func() {

for i := 0; i < 10000; i++ {

mutex.Lock() // Блокируем мьютекс перед модификацией карты

m[i] = i

mutex.Unlock() // Разблокируем мьютекс после модификации карты

}

}()

// Горутина для удаления элементов из карты

go func() {

for i := 0; i < 10000; i++ {

mutex.Lock() // Блокируем мьютекс перед модификацией карты

delete(m, i)

mutex.Unlock() // Разблокируем мьютекс после модификации карты

}

}()

// Даем горутинам время на выполнение

time.Sleep(time.Second)

}

В данном примере перед каждым изменением карты мьютекс блокируется вызовом mutex.Lock(), и разблокируется вызовом mutex.Unlock() после изменения. Это гарантирует, что в любой момент времени карта может быть изменена только одной горутиной, предотвращая конкурентную модификацию и потенциальные ошибки в программе.

Использование каналов: Каналы в Go могут использоваться не только для обмена данными между горутинами, но и как средство синхронизации. Вы можете создать канал, через который будут передаваться операции чтения и записи карты, а одна горутина будет отвечать за выполнение этих операций. Это гарантирует, что все операции с картой выполняются последовательно.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"sync"

)

type command struct {

key  int

value int

op  string // "set", "delete", "get"

}

func main() {

var opChan = make(chan command)

var wg sync.WaitGroup

// Горутина для управления картой

go func() {

m := make(map[int]int)

for op := range opChan {

switch op.op {

case "set":

m[op.key] = op.value

case "delete":

delete(m, op.key)

}

}

}()

// Горутина для добавления элементов

wg.Add(1)

go func() {

defer wg.Done()

for i := 0; i < 100; i++ {

opChan <- command{key: i, value: i, op: "set"}

}

}()

// Горутина для удаления элементов

wg.Add(1)

go func() {

defer wg.Done()

for i := 0; i < 100; i++ {

opChan <- command{key: i, op: "delete"}

}

}()

wg.Wait()

close(opChan)

}

sync.Map: В пакете sync есть специальная структура Map, предназначенная для использования в многопоточных средах без явного использования мьютексов для синхронизации. sync.Map имеет встроенные методы для безопасной работы с данными в конкурентных ситуациях.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"fmt"

"sync"

)

func main() {

var m sync.Map

// Установка значения

m.Store(1, "a")

// Получение значения

if val, ok := m.Load(1); ok {

fmt.Println("Value:", val)

}

// Удаление значения

m.Delete(1)

}

Оба подхода имеют свои преимущества и недостатки. Использование каналов может быть более наглядным и понятным, но потребует больше кода для управления операциями. sync.Map более прост в использовании для простых случаев, но может быть менее гибким, чем полное управление синхронизацией через мьютексы или каналы.

Показать полностью 10
[моё] Golang Программирование Карты Длиннопост
4
FreshAngry007
1 год назад

Массивы и go⁠⁠

В Go массивы являются одной из базовых структур данных. Они представляют собой упорядоченную последовательность элементов одного типа, фиксированной длины. Важно понимать, что размер массива является частью его типа, что отличает массивы в Go от динамических слайсов, которые могут изменять свой размер во время выполнения программы.

Основные характеристики массивов в Go:

  • Фиксированный размер: Длина массива определяется при его объявлении и не может быть изменена. Это означает, что добавление элементов в массив или удаление из него невозможно без создания нового массива.

  • Однородность: Все элементы массива должны быть одного и того же типа.

  • Инициализация: Массивы могут быть инициализированы явно при объявлении или позже в коде. Также Go поддерживает инициализацию массива с помощью синтаксиса литералов.

  • Производительность: Доступ к элементам массива происходит очень быстро, так как элементы хранятся в непрерывной области памяти.

Объявление и инициализация массива:

var arr [5]int // Объявление массива из 5 целых чисел, инициализированного нулями

arr2 := [3]int{1, 2, 3} // Объявление и инициализация массива с тремя элементами

arr3 := [...]int{1, 2, 3, 4, 5} // Использование ... позволяет компилятору определить длину массива

Работа с элементами массива:

arr[0] = 10 // Присвоение значения первому элементу массива

fmt.Println(arr[1]) // Доступ к элементу массива

Передача массивов в функции:

Важно помнить, что массивы в Go передаются в функции по значению, что означает создание их копии. Для больших массивов это может быть неэффективно по памяти и времени. В таких случаях лучше использовать слайсы, которые являются ссылками на массивы.

func printArray(arr [3]int) {

for _, value := range arr {

fmt.Println(value)

}

}

myArray := [3]int{10, 20, 30}

printArray(myArray) // Передача массива в функцию

Использование массивов в Go имеет несколько нюансов и потенциальных "подводных камней", на которые стоит обратить внимание:

1. Фиксированный размер

Основное ограничение массивов заключается в их фиксированном размере. Размер массива является частью его типа, что означает, что вы должны знать размер массива на этапе компиляции. Это может быть неудобно в ситуациях, когда размер данных заранее неизвестен или может изменяться во время выполнения программы. В таких случаях лучше использовать слайсы, которые являются более гибкой альтернативой массивам.

2. Передача массива функции по значению

При передаче массива в функцию Go копирует все его элементы. Для больших массивов это может привести к значительным затратам памяти и времени выполнения. Чтобы избежать этого, рекомендуется использовать слайсы, поскольку они передаются по ссылке, что гораздо эффективнее.

3. Инициализация массивов неявными значениями

При объявлении массива без явной инициализации все его элементы инициализируются нулевыми значениями для типа элементов массива. Это поведение может быть неочевидным для начинающих и привести к ошибкам, если предполагается, что массив изначально заполнен другими значениями.

4. Ограниченная поддержка встроенных операций

В отличие от слайсов, для массивов в Go доступно меньше встроенных операций. Например, нельзя использовать функцию append для добавления элементов в массив, поскольку это изменит его размер, что невозможно для массивов.

5. Неудобство в использовании

Из-за строгих ограничений на размер и тип, работа с массивами может быть менее удобной, чем с слайсами, особенно когда требуется динамическое изменение размера коллекции или её частая передача между функциями.

Решения

Для избежания вышеупомянутых подводных камней рекомендуется использовать слайсы вместо массивов, когда это возможно. Слайсы обеспечивают большую гибкость и удобство при работе с коллекциями данных, позволяют динамически изменять размер и более эффективны с точки зрения использования памяти при передаче в функции.

Показать полностью
[моё] Программирование Golang IT Текст Массивы
0
FreshAngry007
1 год назад

Слайсы и Go⁠⁠

Слайсы в Go — это удобный, динамический интерфейс к массивам. Они обеспечивают большую гибкость, по сравнению с традиционными массивами. В отличие от массивов, размер слайса не является частью его типа, что позволяет слайсам быть гораздо более динамичными.

Слайс состоит из трех компонентов:

  • Указатель на начальный элемент массива, к которому применяется слайс.

  • Длина слайса (len), указывающая на количество элементов в слайсе.

  • Емкость слайса (cap), которая указывает на максимальное количество элементов, начиная с текущего указателя, до конца базового массива.

Как слайсы работают

Когда вы создаете слайс, Go автоматически выделяет под него массив в памяти. Слайс предоставляет ссылку на начало этого массива и позволяет работать с его частью. Изменение элементов слайса напрямую влияет на соответствующие элементы массива, так как слайс является просто "окном" в массив.

Динамическое изменение размера

Одной из ключевых особенностей слайсов является их способность динамически изменять размер. Функция append позволяет добавлять элементы в слайс. Если при добавлении элемента емкость слайса недостаточна для размещения нового элемента, Go автоматически выделяет новый, больший массив и копирует в него элементы из старого. В результате, слайс начинает ссылаться на новый массив.

Создание и инициализация

Слайсы можно создать несколькими способами:

  • Используя оператор make, который позволяет указать тип, начальную длину и (необязательно) емкость слайса. Например, make([]int, 5, 10).

  • Через литерал слайса, например, []int{1, 2, 3}.

  • Оператором [:], [low:high], [low:high:max] для создания слайса из массива или другого слайса.

Подводные камни

Необходимо быть осторожным при работе со слайсами, особенно при передаче их в функции или возвращении из них, так как они могут ссылаться на одни и те же данные в памяти. Также, стоит помнить, что увеличение емкости слайса происходит путем выделения нового массива и копирования в него данных, что может быть дорогостоящей операцией с точки зрения производительности при работе с большими объемами данных.

Пара примеров с подводными камнями при работе со слайсами в Go:

1. Непреднамеренное изменение базового массива

При передаче слайса в функцию или возвращении его из функции, вы фактически передаете ссылку на базовый массив. Это значит, что изменения, сделанные в слайсе внутри функции, отразятся на исходном слайсе.

package main

import "fmt"

func modifySlice(s []int) {

s[0] = 999 // Изменяет первый элемент базового массива

}

func main() {

originalSlice := []int{1, 2, 3}

modifySlice(originalSlice)

// Вывод: [999 2 3]

fmt.Println(originalSlice)

}

В этом примере, изменяя слайс внутри функции modifySlice, мы также изменяем исходный слайс originalSlice, потому что оба они ссылаются на один и тот же базовый массив.

1.1. Решение

Нужно модифицировать функцию так, чтобы она возвращала новый слайс вместо изменения переданного ей слайса. Это дает вызывающему коду контроль над тем, должен ли исходный слайс быть изменен.

package main

import "fmt"

func CopyAndModifySlice(s []int) []int {

// Создаем новый слайс и копируем в него данные из s

newSlice := make([]int, len(s))

copy(newSlice, s)

// Изменяем новый слайс

newSlice[0] = 999

return newSlice

}

func main() {

originalSlice := []int{1, 2, 3}

modifynewSlice := CopyAndModifySlice(originalSlice)

// Вывод: [1 2 3]

fmt.Println(originalSlice)

// Вывод: [999 2 3]

fmt.Println(modifynewSlice )

}

2. Утечка памяти из-за неправильного срезания слайсов

Слайс хранит ссылки на базовый массив, и если вы создаете новый слайс на основе части большого массива, но при этом больше не используете исходный большой массив, он всё равно не будет собран сборщиком мусора до тех пор, пока существует хотя бы один слайс, ссылающийся на его часть.

package main

import "fmt"

func getSmallSlice() []int {

// Большой массив

bigSlice := make([]int, 1000000)

// Маленький слайс, ссылается на начало большого массива

smallSlice := bigSlice[:3]

return smallSlice

}

func main() {

smallSlice := getSmallSlice()

fmt.Println(cap(smallSlice)) // Вывод: 1000000

// В этот момент вся память, занимаемая bigSlice, все еще занята,

// несмотря на то что мы работаем только с маленьким сегментом.

}

В этом примере, bigSlice больше недоступен после выполнения getSmallSlice, но память, которую он занимает, остается занятой из-за smallSlice, который ссылается на часть bigSlice. Это может привести к неэффективному использованию памяти, особенно если подобный код выполняется многократно.

2.1. Решение

package main

import "fmt"

func getSmallSlice() []int {

// Большой массив

bigSlice := make([]int, 1000000)

// Создаем новый слайс того же размера, что и нужный маленький слайс

smallSlice := make([]int, 3)

// Копируем данные из большого массива в маленький слайс

copy(smallSlice, bigSlice[:3])

return smallSlice

}

func main() {

smallSlice := getSmallSlice()

fmt.Println(cap(smallSlice)) // Вывод: 3

// Теперь большой массив может быть собран сборщиком мусора,

// так как на него нет ссылок в виде слайса.

}

3. Увеличение емкости (capacity) слайса неявно

При добавлении элементов к слайсу его емкость может быть неявно увеличена, что приводит к созданию нового базового массива и копированию элементов из старого массива в новый. Этот процесс может быть неочевидным и привести к снижению производительности при неправильном использовании.

package main

import "fmt"

func main() {

// Слайс с начальной емкостью 1

s := make([]int, 0, 1)

prevCap := cap(s)

// Потенциально много раз увеличиваем емкость

for i := 0; i < 1024; i++ {

if cap(s) != prevCap {

fmt.Printf("Емкость изменилась с %d на %d\n", prevCap, cap(s))

prevCap = cap(s)

}

s = append(s, i)

}

}

// Емкость изменилась с 1 на 2

// Емкость изменилась с 2 на 4

// Емкость изменилась с 4 на 8

// Емкость изменилась с 8 на 16

// Емкость изменилась с 16 на 32

// Емкость изменилась с 32 на 64

// Емкость изменилась с 64 на 128

// Емкость изменилась с 128 на 256

// Емкость изменилась с 256 на 512

// Емкость изменилась с 512 на 848

// Емкость изменилась с 848 на 1280

В этом примере слайс s начинается с емкости 1 и увеличивается многократно по мере добавления элементов, что может привести к множественным аллокациям памяти и копированиям.

3.1. Решение

package main

import "fmt"

func main() {

// Заранее выделяем слайс с нужной емкостью

// Емкость сразу выделена под 1024 элемента

s := make([]int, 0, 1024)

// Добавляем элементы, не вызывая дополнительных аллокаций

for i := 0; i < 1024; i++ {

s = append(s, i)

}

// Вывод: 1024 1024

fmt.Println(len(s), cap(s))

}

// емкость не менялась не разу

4. Потенциальная потеря данных при неосторожном использовании append

Если append используется с исходным слайсом, который имеет недостаточную емкость для добавления новых элементов, будет создан новый слайс с увеличенной емкостью, и исходный слайс не будет изменен.

package main

import "fmt"

func main() {

s1 := make([]int, 0, 5)

// s2 указывает на новый слайс

s2 := append(s1, 1)

// s3 тоже указывает на новый слайс, но уже другой

s3 := append(s1, 2)

// [] [1] [2] - s1 остается без изменений

fmt.Println(s1, s2, s3)

}

Это поведение может привести к неожиданным результатам, если предполагается, что append изменит исходный слайс.

4.1. Решение

Чтобы избежать потенциальной потери данных при использовании append, особенно когда вы работаете с исходным слайсом, который может не иметь достаточной емкости для добавления новых элементов, важно убедиться, что вы корректно обрабатываете возвращаемое значение от append. append возвращает новый слайс, который может указывать на тот же или новый базовый массив в зависимости от емкости исходного слайса.

Если ваш код полагается на изменение исходного слайса, вы должны всегда присваивать результат append обратно исходному слайсу:

package main

import "fmt"

func main() {

s1 := make([]int, 0, 5)

// Присваиваем результат append обратно s1

s1 = append(s1, 1)

// s2 теперь будет содержать [1 2]

s2 := append(s1, 2)

// Вывод: [1] [1 2] - s1 изменен корректно

fmt.Println(s1, s2)

}

Поиграться с примерами можно тут https://go.dev/play

Показать полностью
[моё] Программирование Golang IT Длиннопост Текст Слайсы
0
2
PENTEST.DNA
PENTEST.DNA
1 год назад
Web-технологии

Создание TCP-прокси⁠⁠

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Вы можете реализовать все 𝗧𝗖𝗣-взаимодействия, используя встроенный в 𝗚𝗼 пакет 𝗻𝗲𝘁. В предыдущем разделе мы сосредоточились главным образом на его применении с позиции клиента. В этом же разделе задействуем его для создания 𝗧𝗖𝗣-серверов и передачи данных. Изучение этого процесса начнется с создания эхо-сервера — сервера, который просто возвращает запрос обратно клиенту. Затем мы создадим две более универсальные в применении программы: переадресатор 𝗧𝗖𝗣-портов и 𝗡𝗲𝘁𝗰𝗮𝘁-функцию «зияющая дыра в безопасности», применяемую для удаленного выполнения команд.


Использование io.Reader и io.Writer



При создании примеров этого раздела вам потребуется задействовать два значимых типа: 𝗶𝗼.𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿. Они необходимы для всех задач ввода/вывода (𝗜/𝗢) вне зависимости от того, задействуете вы 𝗧𝗖𝗣, 𝗛𝗧𝗧𝗣, файловую систему или любые другие средства. Будучи частью встроенного в 𝗚𝗼 пакета 𝗶𝗼, эти типы являются краеугольным камнем любой передачи данных, как локальной, так и сетевой. В документации они определены так:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Оба типа определяются как интерфейсы, то есть напрямую их создать нельзя. Каждый тип содержит определение одной экспортируемой функции: 𝗥𝗲𝗮𝗱 или 𝗪𝗿𝗶𝘁𝗲. Можно рассматривать эти функции как абстрактные методы, которые должны быть реализованы в типе, чтобы он считался 𝗥𝗲𝗮𝗱𝗲𝗿 или 𝗪𝗿𝗶𝘁𝗲𝗿. Например, следующий искусственный тип выполняет это соглашение и может использоваться там, где приемлем 𝗥𝗲𝗮𝗱𝗲𝗿:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Давайте с помощью них создадим что-нибудь полуготовое: настраиваемый 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿, обертывающий 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁. Код для этого тоже будет несколько искусственным, так как типы 𝗚𝗼 𝗼𝘀.𝗦𝘁𝗱𝗶𝗻 и 𝗼𝘀.𝗦𝘁𝗱𝗼𝘂𝘁 уже действуют как 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿. Но если не пытаться изобрести колесо, то ничему и не научишься, ведь так?

Ниже показана полная реализация, а далее дано пояснение.

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Реализация reader и writer /io-example/main.go

Этот код определяет два пользовательских типа: 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿. В каждом типе вы определяете конкретную реализацию функции 𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) для 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 и функции 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲) для 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿. В этом случае обе функции считывают из 𝘀𝘁𝗱𝗶𝗻 и записывают в 𝘀𝘁𝗱𝗼𝘂𝘁.

Обратите внимание на то, что функции 𝗥𝗲𝗮𝗱 и в 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿, и в 𝗼𝘀.𝗦𝘁𝗱𝗶𝗻 возвращают длину данных и все ошибки. Сами эти данные копируются в срез 𝗯𝘆𝘁𝗲, передаваемый этой функции. Это согласуется с начальным определением интерфейса 𝗥𝗲𝗮𝗱𝗲𝗿, приведенным в данном разделе ранее. Функция 𝗺𝗮𝗶𝗻() создает этот срез с названием 𝗶𝗻𝗽𝘂𝘁 и затем использует его в вызовах к 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿.𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) и 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿.𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲).

При пробном запуске программы мы получим следующий вывод:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Копирование данных из 𝗥𝗲𝗮𝗱𝗲𝗿 в 𝗪𝗿𝗶𝘁𝗲𝗿 — это настолько распространенный шаблон, что пакет 𝗶𝗼 содержит специальную функцию 𝗖𝗼𝗽𝘆(), которую можно задействовать для упрощения функции 𝗺𝗮𝗶𝗻(). Вот ее прототип:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Эта удобная функция позволяет реализовывать то же поведение программы, что и ранее, заменив 𝗺𝗮𝗶𝗻() кодом, показанным ниже.

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Применение io.Copy /ch-2/copy-example/main.go

Обратите внимание, что явные вызовы 𝗿𝗲𝗮𝗱𝗲𝗿.𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝘄𝗿𝗶𝘁𝗲𝗿.𝗪𝗿𝗶𝘁𝗲([ ] 𝗯𝘆𝘁𝗲) были замещены одним вызовом 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿). Внутренне 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) вызывает в переданном ридере функцию 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲), в результате чего 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 выполняет считывание из 𝘀𝘁𝗱𝗶𝗻. Далее 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) вызывает в переданном райтере функцию 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲), что приводит к вызову 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿, записывающего данные в 𝘀𝘁𝗱𝗼𝘂𝘁. По сути, 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) обрабатывает последовательный процесс чтения/записи без лишних деталей.

Этот вводный раздел никак нельзя считать подробным рассмотрением системы 𝗜/𝗢 и интерфейсов в 𝗚𝗼. Многие вспомогательные функции и пользовательские ридеры/райтеры существуют как часть стандартных пакетов 𝗚𝗼. В большинстве случаев эти стандартные пакеты содержат все основные реализации, необходимые для реализации большинства распространенных задач. В следующем разделе мы рассмотрим применение всех этих основ к 𝗧𝗖𝗣-коммуникациям и в итоге применим полученные навыки для разработки реальных рабочих инструментов.



Создание эхо-сервера:

Как и во многих языках, изучение процесса чтения/записи данных с сокета мы начнем с построения эхо-сервера. Для этого будем использовать 𝗻𝗲𝘁.𝗖𝗼𝗻𝗻 — потокоориентированное соединение 𝗚𝗼, с которым вы уже познакомились при создании сканера портов. Как указано в документации для этого типа данных, 𝗖𝗼𝗻𝗻 реализует функции 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲) согласно определению для интерфейсов 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿. Следовательно, 𝗖𝗼𝗻𝗻 одновременно является и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿 (да, такое возможно). Это вполне логично, так как 𝗧𝗖𝗣-соединения двунаправленные и могут использоваться для отправки (записи) и получения (чтения) данных.

После создания экземпляра conn вы сможете отправлять и получать данные через TCP-сокет. Тем не менее TCP-сервер не может просто создать соединение, его должен установить клиент. В 𝗚𝗼 для начального открытия 𝗧𝗖𝗣-слушателя на конкретном порте можно использовать 𝗻𝗲𝘁.𝗟𝗶𝘀𝘁𝗲𝗻(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴). После подключения клиента метод 𝗔𝗰𝗰𝗲𝗽𝘁() создает и возвращает объект 𝗖𝗼𝗻𝗻, который вы можете применять для получения и отправки данных.

Ниже показан полноценный пример реализации сервера. Для большей ясности мы добавили в код комментарии. Не стремитесь сразу понять весь код, так как позже мы подробно его поясним.

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Базовый эхо-сервер /ch-2/echo-server/main.go

Базовый эхо-сервер начинается с определения функции 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻), которая принимает в качестве параметра экземпляр 𝗖𝗼𝗻𝗻. Он выступает в роли обработчика соединения, выполняя все необходимые операции 𝗜/𝗢. Эта функция повторяется бесконечно, используя буфер для считывания данных из соединения и их записи в него. Данные считываются в переменную 𝗯, после чего записываются обратно в соединение.

Теперь нужно настроить слушатель, который будет вызывать обработчик. Как ранее говорилось, сервер не может сам создать соединение и должен прослушивать подключение клиента. Следовательно, слушатель, определенный как 𝘁𝗰𝗽, привязанный к порту 𝟮𝟬𝟬𝟴𝟬, запускается во всех интерфейсах посредством функции 𝗻𝗲𝘁.𝗟𝗶𝘀𝘁𝗲𝗻(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴).

Далее бесконечный цикл обеспечивает, чтобы сервер продолжал прослушивание соединений даже после того, как оно было установлено. В этом цикле происходит вызов 𝗹𝗶𝘀𝘁𝗲𝗻𝗲𝗿.𝗔𝗰𝗰𝗲𝗽𝘁() ❻ — функции, блокирующей выполнение при ожидании подключений. Когда клиент подключается, эта функция возвращает экземпляр 𝗖𝗼𝗻𝗻. Напомним, что 𝗖𝗼𝗻𝗻 является и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿 (реализует методы интерфейса 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲)).

После этого экземпляр 𝗖𝗼𝗻𝗻 передается в функцию обработки 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻). Перед ее вызовом указано ключевое слово 𝗴𝗼, делающее этот вызов многопоточным, в результате чего другие подключения в ожидании завершения функции-обработчика не блокируются. Это может показаться излишним для столь простого сервера, но мы добавили эту функциональность для демонстрации простоты паттерна многопоточности 𝗚𝗼 на случай, если вы еще не до конца его поняли. В данный момент у вас есть два легковесных параллельно выполняющихся потока.

  • Основной поток зацикливается и блокируется функцией listener.Accept() на время ожидания ею следующего подключения.

  • Горутина обработки, чье выполнение было передано в функцию echo(net.Conn), возобновляется и обрабатывает данные.


Далее показан пример использования 𝗧𝗲𝗹𝗻𝗲𝘁 в качестве подключающегося клиента:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Сервер производит следующий стандартный вывод:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост


Революционно, не правда ли? Сервер, возвращающий клиенту в точности то, что клиент ему отправил. Очень полезный и сильный пример!



Создание буферизованного слушателя для улучшения кода:



Пример в коде Базового эхо-сервера работает прекрасно, но он опирается на чисто низкоуровневые вызовы функции, отслеживание буфера и повторяющиеся циклы чтения/записи. Это довольно утомительный и подверженный ошибкам процесс. К счастью, в 𝗚𝗼 есть и другие пакеты, которые могут его упростить и уменьшить сложность кода. Говоря конкретнее, пакет 𝗯𝘂𝗳𝗶𝗼 обертывает 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 для создания буферизованного механизма 𝗜/𝗢. Далее приведена обновленная функция 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻) с сопутствующим описанием изменений:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

В экземпляре 𝗖𝗼𝗻𝗻 больше не происходит прямого вызова функций 𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲). Вместо этого вы инициализируете новые буферизованные 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 через 𝗡𝗲𝘄𝗥𝗲𝗮𝗱𝗲𝗿(𝗶𝗼.𝗥𝗲𝗮𝗱𝗲𝗿) и 𝗡𝗲𝘄𝗪𝗿𝗶𝘁𝗲𝗿(𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿). Оба вызова в качестве параметра получают существующие 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 (помните, что тип 𝗖𝗼𝗻𝗻 реализует необходимые функции, чтобы считаться 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿).

Оба буферизованных экземпляра содержат вспомогательные функции для чтения и сохранения данных. 𝗥𝗲𝗮𝗱𝗦𝘁𝗿𝗶𝗻𝗴(𝗯𝘆𝘁𝗲) получает символ-разграничитель, обозначая, до какой точки выполнять считывание, а 𝗪𝗿𝗶𝘁𝗲𝗦𝘁𝗿𝗶𝗻𝗴(𝗯𝘆𝘁𝗲) записывает строку в сокет. При записи данных вам нужно явно вызывать 𝘄𝗿𝗶𝘁𝗲𝗿.𝗙𝗹𝘂𝘀𝗵() для сброса всех данных внутреннему райтеру (в данном случае экземпляру 𝗖𝗼𝗻𝗻).

Несмотря на то что предыдущий пример упрощает процесс, применяя буферизацию 𝗜/𝗢, вы можете переработать его под использование вспомогательной функции 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿). Напомним, что функция получает в качестве ввода целевой 𝗪𝗿𝗶𝘁𝗲𝗿 и исходный 𝗥𝗲𝗮𝗱𝗲𝗿, просто выполняя копирование из источника в место назначения.

В этом примере вы передаете переменную 𝗰𝗼𝗻𝗻 и как источник, и как место назначения, так как в итоге будете отражать содержимое обратно в установленное соединение:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Вот вы и познакомились с основами системы 𝗜/𝗢, попутно применив ее к 𝗧𝗖𝗣-серверам. Пришло время перейти к более полезным и представляющим для вас интерес примерам.



Проксирование TCP-клиента:



Теперь, когда у вас под ногами есть твердая почва, можете применить полученные навыки для создания простого переадресатора портов для проксирования соединения через промежуточный сервис или хост. Как уже говорилось, это пригождается для обхода ограничивающего контроля исходящего трафика или использования системы с целью обхода сегментации сети. Прежде чем перейти к коду, рассмотрите вымышленную, но вполне реалистичную задачу: Андрей является малоэффективным сотрудником компании 𝗔𝗖𝗠𝗘 𝗜𝗻𝗰., работая на должности бизнес-аналитика и получая приличную зарплату просто потому, что слегка приукрасил данные своего резюме. (Неужели он реально учился в школе Лиги плюща? Андрей, такой обман неэтичен.) Недостаток мотивации Андрея может по силе сравниться разве что с его любовью к кошкам — такой сильной, что он даже установил дома специальные видеокамеры и создал сайт 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲, через который удаленно следил за своими мохнатыми питомцами. Тем не менее здесь была одна сложность: 𝗔𝗖𝗠𝗘 следит за Андреем. Им не нравится, что он круглые сутки передает потоковое видео своих кошек в ультравысоком разрешении 𝟰𝗞, занимая ценный пропускной канал сети. Компания даже заблокировала своим сотрудникам возможность посещать его кошачий сайт.

Но у хитрого Андрея и здесь возник план: «А что, если я настрою переадресатор портов в подконтрольной мне интернет-системе и буду перенаправлять весь трафик с этого хоста на 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲?» На следующий день Андрей отмечается на работе и убеждается в возможности доступа к личному сайту, размещенному на домене 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺. Он пропускает все встречи после обеда и отправляется в кафетерий, где быстро пишет код для своей задачи, подразумевающей перенаправление на 𝗵𝘁𝘁𝗽://𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 всего входящего на 𝗵𝘁𝘁𝗽://𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺 трафика.

Вот код Андрея, который он запускает на сервере 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Начнем с рассмотрения функции 𝗵𝗮𝗻𝗱𝗹𝗲(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻). Андрей подключается к 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 (вспомните, что этот хост недоступен напрямую с его рабочего места). Затем он использует 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) в двух разных местах. Первый экземпляр обеспечивает копирование данных из входящего соединения в соединение 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲. Второй же обеспечивает, чтобы считанные из 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 данные записывались обратно в соединение подключающегося клиента. Так как 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) является блокирующей функцией и будет продолжать блокировать выполнение, пока сетевое соединение открыто, Андрей предусмотрительно обертывает первый вызов 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) в новую горутину. Это гарантирует продолжение выполнения в функции 𝗵𝗮𝗻𝗱𝗹𝗲(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻) и дает возможность выполнить второй вызов 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿).

Прокси-сервер Андрей прослушивает порт 𝟴𝟬 и ретранслирует весь трафик, получаемый через это соединение, на порт 𝟴𝟬 сайта 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 и обратно. Этот безумный и расточительный парень убеждается, что может подключаться к 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 через 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺 с помощью 𝗰𝘂𝗿𝗹:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Так Андрей успешно реализует коварный замысел. Он прекрасно устроился, получив возможность в оплачиваемое 𝗔𝗖𝗠𝗘 время использовать их же канал связи для наблюдения за жизнью своих питомцев.



Воспроизведение функции Netcat для выполнения команд:



В этом разделе мы воспроизведем одну из наиболее интересных функций 𝗡𝗲𝘁𝗰𝗮𝘁 — «зияющую дыру в безопасности».

𝗡𝗲𝘁𝗰𝗮𝘁 — это как швейцарский армейский нож для 𝗧𝗖𝗣/𝗜𝗣, который представляет собой более гибкую версию 𝗧𝗲𝗹𝗻𝗲𝘁 с поддержкой сценариев. Эта утилита имеет возможность перенаправлять 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 любой произвольной программы через 𝗧𝗖𝗣, позволяя атакующему, например, превратить уязвимость к выполнению одной команды в доступ к оболочке операционной системы. Взгляните:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Эта команда создает прослушивающий сервер на порте 𝟭𝟯𝟯𝟯𝟳. Любой подключающийся, возможно, через 𝗧𝗲𝗹𝗻𝗲𝘁, клиент сможет выполнить любые команды 𝗯𝗮𝘀𝗵 — вот почему данную функцию и называют зияющей дырой в безопасности. 𝗡𝗲𝘁𝗰𝗮𝘁 позволяет при желании включить такую возможность в процессе компиляции программы. (По понятным причинам большинство исполняемых файлов 𝗡𝗲𝘁𝗰𝗮𝘁 в стандартных сборках 𝗟𝗶𝗻𝘂𝘅 ее не включают.) Эта функция настолько потенциально опасна, что мы покажем, как воссоздать ее в 𝗚𝗼.

Для начала загляните в пакет 𝗚𝗼 𝗼𝘀/𝗲𝘅𝗲𝗰. Он будет использоваться для выполнения команд операционной системы. Этот пакет определяет тип 𝗖𝗺𝗱, который содержит необходимые методы и свойства для выполнения команд и управления 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁. Вы будете перенаправлять 𝘀𝘁𝗱𝗶𝗻 (𝗥𝗲𝗮𝗱𝗲𝗿) и 𝘀𝘁𝗱𝗼𝘂𝘁 (𝗪𝗿𝗶𝘁𝗲𝗿) в экземпляр 𝗖𝗼𝗻𝗻, представляющий и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿.

При получении нового подключения создать экземпляр 𝗖𝗺𝗱 можно с помощью функции 𝗖𝗼𝗺𝗺𝗮𝗻𝗱(𝗻𝗮𝗺𝗲 𝘀𝘁𝗿𝗶𝗻𝗴, 𝗮𝗿𝗴 ...𝘀𝘁𝗿𝗶𝗻𝗴) из 𝗼𝘀/𝗲𝘅𝗲𝗰. Эта функция получает в качестве параметров команды ОС и любые аргументы. В данном примере нужно жестко закодировать в качестве команды /𝗯𝗶𝗻/𝘀𝗵 и передать в качестве аргумента -𝗶, чтобы перейти в интерактивный режим, из которого можно будет управлять потоками 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 более уверенно:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Эта инструкция создает экземпляр 𝗖𝗺𝗱, но команду еще не выполняет. Здесь для управления 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 есть два варианта: использовать 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿), как говорилось ранее, или напрямую присвоить 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 экземпляру 𝗖𝗺𝗱. Давайте непосредственно присвоим объект 𝗖𝗼𝗻𝗻 экземплярам 𝗰𝗺𝗱.𝗦𝘁𝗱𝗶𝗻 и 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

После настройки команды и потоков запустить ее можно с помощью 𝗰𝗺𝗱.𝗥𝘂𝗻():

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Такая логика прекрасно работает для систем 𝗟𝗶𝗻𝘂𝘅. Тем не менее при настройке и запуске этой программы под 𝗪𝗶𝗻𝗱𝗼𝘄𝘀 с помощью 𝗰𝗺𝗱.𝗲𝘅𝗲, а не /𝗯𝗶𝗻/𝗯𝗮𝘀𝗵, подключающийся клиент не получает вывод команды из-за специфичной для 𝗪𝗶𝗻𝗱𝗼𝘄𝘀 обработки анонимных каналов. Далее описаны два решения этой проблемы.

Во-первых, можно настроить код для принудительного сброса 𝘀𝘁𝗱𝗼𝘂𝘁. Вместо непосредственного присваивания 𝗖𝗼𝗻𝗻 экземпляру 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁 нужно реализовать собственный 𝗪𝗿𝗶𝘁𝗲𝗿, который обертывает 𝗯𝘂𝗳𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿 (буферизованный райтер) и явно вызывает его метод 𝗙𝗹𝘂𝘀𝗵 для принудительного сброса буфера. Пример использования 𝗯𝘂𝗳𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿 можно найти в разделе «Создание эхо-сервера» ранее в этой главе. Вот определение пользовательского райтера, 𝗙𝗹𝘂𝘀𝗵𝗲𝗿:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Тип 𝗙𝗹𝘂𝘀𝗵𝗲𝗿 реализует функцию 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲), которая записывает данные во внутренний буферизованный райтер, а затем сбрасывает вывод.

С помощью этой реализации пользовательского райтера можно настроить обработчик подключений на создание экземпляра и применение типа 𝗙𝗹𝘂𝘀𝗵𝗲𝗿 для 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Продолжение кода выше

Это решение хотя и вполне пригодно, но не очень элегантно. Несмотря на то что рабочий код для нас важнее, чем аккуратный, мы используем эту проблему как возможность рассказать о функции 𝗶𝗼.𝗣𝗶𝗽𝗲(). Она представляет собой синхронный канал в памяти 𝗚𝗼, который можно задействовать для подключения 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Применение 𝗣𝗶𝗽𝗲𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗣𝗶𝗽𝗲𝗪𝗿𝗶𝘁𝗲𝗿 позволяет избежать необходимости явного сброса райтера и синхронного подключения 𝘀𝘁𝗱𝗼𝘂𝘁 и 𝗧𝗖𝗣-соединения. Опять же понадобится переписать функцию обработчика:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Вызов 𝗶𝗼.𝗣𝗶𝗽𝗲 создает ридер и райтер, подключаемые синхронно, — любые данные, записываемые в райтер (в данном примере 𝘄𝗽), будут считаны ридером (𝗿𝗽). Поэтому сначала происходит присваивание райтера экземпляру 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁, после чего используется 𝗶𝗼.𝗖𝗼𝗽𝘆(𝗰𝗼𝗻𝗻, 𝗿𝗽) для присоединения 𝗣𝗶𝗽𝗲𝗥𝗲𝗮𝗱𝗲𝗿 к 𝗧𝗖𝗣-соединению. Это делается с помощью горутины, предотвращающей блокирование кода. Любой стандартный вывод команды отправляется райтеру, после чего передается ридеру и далее через 𝗧𝗖𝗣-соединение. Как вам такая элегантность?

Таким образом, мы успешно реализовали «зияющую дыру безопасности» 𝗡𝗲𝘁𝗰𝗮𝘁 с позиции 𝗧𝗖𝗣-слушателя, ожидающего подключения. По тому же принципу можно реализовать эту функцию с позиции подключающегося клиента, перенаправляющего 𝘀𝘁𝗱𝗼𝘂𝘁 и 𝘀𝘁𝗱𝗶𝗻 локального исполняемого файла удаленному слушателю. Детали этого процесса мы оставим вам для самостоятельной реализации, но в общем они будут включать следующее:

  • установку подключения к удаленному слушателю через net.Dial(network, address string);

  • инициализацию Cmd через exec.Command(name string, arg ...string);

  • перенаправление свойств Stdin и Stdout для использования объекта net.Conn;

  • выполнение команды.

На этом этапе слушатель должен получить подключение. Любые передаваемые клиенту данные должны интерпретироваться на клиенте как 𝘀𝘁𝗱𝗶𝗻, а данные, получаемые слушателем, — как 𝘀𝘁𝗱𝗼𝘂𝘁.

На этом всё, ждите в ближайшее время больше статей.

ССЫЛКА НА ТЕЛЕГРАМ КАНАЛ АВТОРА

Показать полностью 21
[моё] Информационная безопасность Хакеры Программирование Golang Программист Интернет Длиннопост
0
Посты не найдены
О нас
О Пикабу Контакты Реклама Сообщить об ошибке Сообщить о нарушении законодательства Отзывы и предложения Новости Пикабу Мобильное приложение RSS
Информация
Помощь Кодекс Пикабу Команда Пикабу Конфиденциальность Правила соцсети О рекомендациях О компании
Наши проекты
Блоги Работа Промокоды Игры Курсы
Партнёры
Промокоды Биг Гик Промокоды Lamoda Промокоды Мвидео Промокоды Яндекс Директ Промокоды Отелло Промокоды Aroma Butik Промокоды Яндекс Путешествия Постила Футбол сегодня
На информационном ресурсе Pikabu.ru применяются рекомендательные технологии