Русский
Русский
English
Статистика
Реклама

Блог компании karuna

Дженерики в языке Go

02.06.2021 14:09:29 | Автор: admin
func Map[F, T any](s []F, f func(F) T) []T {    r := make([]T, len(s))    for i, v := range s {        r[i] = f(v)    }    return r}

Как вы уже наверняка знаете, proposal по дженерикам в Golang принят (официально это называется type parameters) и будет имплементирован в go 1.18. Бета будет доступна уже в конце этого года. А это значит, что пора разобраться, на чём в итоге остановились разработчики языка ведь черновик type parameters постоянно менялся в течение последних лет.


Технология новая, на практике толком никто не использовал. Поэтому если увидите какую-то неточность в статье, не стесняйтесь указать это в комментариях.


Самостоятельно поиграться с дженериками можно здесь


Итак, поехали.


Зачем нужны дженерики в Go?


Несколько лет назад у нас в Каруне возникла необходимость перевести несколько сервисов с PHP на GO. Как сейчас помню, в первой же программе потребовалось проверить, существует ли строка в некотором слайсе строк. Беглый гуглёж показал, что встроенной функции, аналогичной пхпшной in_array() в языке нет. Поэтому пришлось написать свою, что-то типа такого:


func stringExistsInSlice(val string, values []string) bool {    for _, v := range values {        if val == v {            return true        }    }    return false}

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


func existsInSlice(val int, values []int) bool {    for _, v := range values {        if val == v {            return true        }    }    return false}

Написать универсальную функцию под все типы задача не очень простая. Можно использовать reflect и interface{}, как в примере на stackoverflow, но это, понятное дело, выглядит не очень и подвержено ошибкам, не проверяемым в момент компиляции. Или же можно использовать кодогенерацию, что тоже в общем-то так себе, так как это лишний шаг при билде.


Забегая вперёд, в go 1.18 это будет решаться так:


func existsInSlice[T comparable](val T, values []T) bool {    for _, v := range values {        if val == v {            return true        }    }    return false}

Нужно ли усложнять язык дженериками?


Вопрос дискуссионный. Мнения разделились.


Как известно, язык Go изначально был заточен под максимальную простоту, и обобщение типов может усложнить читабельность кода. Многие противопоставляют Go языку Java, традиционно наполненному обобщениями различного рода, и дженерики это как первый шаг в эту сторону. Теряется этакая гошная "дубовость" (в хорошем смысле), прямолинейность. Тем более, что в обычном продуктовом коде люди годами обходятся и без этого. Порой проще скопипастить немного кода и не париться. Как говорили в одном докладе, копипаста это "хорошее медитативное занятие".


С другой стороны, если надо написать универсальную библиотеку для каких-то универсальных целей, то придётся использовать interface{} или кодогенерацию, а это тоже в общем-то читабельности и надёжности не добавляет. Также необходимо отметить, что разработчики языка сделали всё возможное, чтобы дженерики выглядели и использовались как можно проще. Намного проще, чем в других языках.


Согласно результатам опроса 88% респондентов назвали отсутствие дженериков критической проблемой. 18% опрошенных сказали, что не используют Go именно из-за отсутствия этой функциональности (цитата: "18% of respondents are prevented from using Go because of a lack of generics").


Синтаксис функции с type parameters


Вот простейший пример. Функция, построчно печатающая элементы слайса любого типа.


func PrintSlice[T any](s []T) {    for _, v := range s {        fmt.Println(v)    }}

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


А вот так происходит вызов такой функции, уже с конкретным типом string:


greatings := []string{"Hello", "world"};PrintSlice[string](greatings)

Т.е. синтаксически по сути мы передаём в функцию тип как обычный аргумент (параметр). Просто такие "аргументы" передаются и описываются в сигнатуре в отдельных квадратных скобках вместо круглых. Поэтому функциональность так и называется: type parameters.


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


greatings := []string{"Hello", "world"};PrintSlice(greatings)

Констрейнты (ограничения типов)


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


func [T MyConstraint] (...

, где MyConstraint это интерфейс, который описывает, каким может быть тип. Этот интерфейс может быть обычным go-интерфейсом, описывающим требуемые методы.


type MyConstraint interface {    String() string}

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


type MyConstraint interface {    type int, int8, int16, int32, int64}

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


Есть встроенные в язык констрейнты, например any (синоним interface{}) и comparable (ограничивающий типы, для которых определены операторы сравнения).


Также в стандартную библиотеку планируется добавить пакет constraints, где будут добавлены различные полезняшки. Например, constraints.Number (под это подходят любые типы а ля int, float32 и т.д. )


Типы с обобщениями


Помимо функций подобным образом можно работать и с описанием типа.


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


type Vector[T constraints.Number] []T

При использовании такого типа нужно указать в квадратных скобках его конкретный уже тип:


var myVec Vector[int]

Вот более-менее полный пример функции сложения векторов


type Number interface {    type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64}type Vector[T Number] []Tfunc AddVector[T Number](vec1 Vector[T], vec2 Vector[T]) Vector[T] {    var result Vector[T]    for i := range vec1 {        result = append(result, vec1[i]+vec2[i])    }    return result}func main() {    v1 := Vector[int]{1, 2, 3}    v2 := Vector[int]{3, 4, 5}    result := AddVector(v1, v2)    fmt.Println(result)}

поиграться с примером можно здесь: https://go2goplay.golang.org/p/n05eSb5uFXS


(в примере я не использовал встроенный интерфейс constraints.Number, так как на go2goplay.golang.org это почему-то не работает. Пришлось делать свой доморощенный interface Number)


Обратите внимание на то, что здесь нельзя использовать констрейнт any, так как операция сложения определена далеко не для всех типов, и вы получите соответствующую ошибку "operator + not defined".


Некоторые замечания по реализации


Пакеты


В стандартную библиотеку планируется добавить несколько пакетов, таких как slices, maps, chans и т.д., которые будут предоставлять универсальные функции для работы со слайсами, каналами и т.д.


Пакеты container/list, container/ring, sync и другие будут доработаны с точки зрения типобезопасности. Math получит новые универсальные функции для любых чисел (например Min и Max)


Эффективность


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


Цитата:


Generic functions, rather than generic types, can probably be compiled using an interface-based approach. That will optimize compile time, in that the function is only compiled once, but there will be some run time cost.

Generic types may most naturally be compiled multiple times for each set of type arguments. This will clearly carry a compile time cost, but there shouldn't be any run time cost. Compilers can also choose to implement generic types similarly to interface types, using special purpose methods to access each element that depends on a type parameter.

Отличие от java


Как известно, java удаляет информацию о дженериках после компиляции. В golang это не так.


Также, в Джаве реализована ковариантность и контрвариантность (List<? extends Number>, List<? super Number>), в Go всё намного проще: мы просто передаём тип как параметр, а тип ограничен интерфейсом.


Кстати, многие спрашивают, почему нельзя было сделать стандартные для многих языков (включая Java) скобки <>, а ввели новый вариант в виде квадратных скобок?


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


a, b = w < x, y > (z)

можно трактовать как


a, b = (w<x), (y<z)//илиa, b = w<x,y> (z)

Ещё примеры


Типобезопасная функция, объединяющая два канала в один


func Merge[T any](c1, c2 <-chan T) <-chan T {    r := make(chan T)    go func(c1, c2 <-chan T, r chan<- T) {        defer close(r)        for c1 != nil || c2 != nil {            select {            case v1, ok := <-c1:                if ok {                    r <- v1                } else {                    c1 = nil                }            case v2, ok := <-c2:                if ok {                    r <- v2                } else {                    c2 = nil                }            }        }    }(c1, c2, r)    return r}

Делаем свой Set на основе map


package setstype Set[T comparable] map[T]struct{}func Make[T comparable]() Set[T] {    return make(Set[T])}func (s Set[T]) Add(v T) {    s[v] = struct{}{}}func (s Set[T]) Delete(v T) {    delete(s, v)}func (s Set[T]) Contains(v T) bool {    _, ok := s[v]    return ok}func (s Set[T]) Len() int {    return len(s)}func (s Set[T]) Iterate(f func(T)) {    for v := range s {        f(v)    }}

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


s := sets.Make[int]()s.Add(1)if s.Contains(2) { panic("unexpected 2") }

Что дальше?


Дальше больше. Есть куча предложений, как улучшить дженерики: в частности автоматический вывод типов в различных ситуациях или упрощённая работа с zero value. Но это всё по сути ждёт, когда основная функциональность попадёт в язык.


В общем, поживём увидим. Осталось уже недолго.

Подробнее..

Опыт хранения IP-адресов в PostgreSQL

16.06.2021 14:10:53 | Автор: admin

Описание проблемы

Не раз наша команда в Каруне сталкивались с задачей, связанной с хранением и использованием IP-адресов в базе данных. Предположим, что есть типичная задача: необходимо парсить огромное количество диапазонов адресов (~300k) сизвестного ресурса, а далее определять страну по IP-адресу клиента. Кажется, ничего особенного. Это довольно просто решается любым ниже описанным способом при малых нагрузках. Но если у нас тысячи пользователей, или наш сервис является прокси перед всеми остальными? В этом случае не хочется быть бутылочным горлышком и приходится бороться за каждую долю секунды.

Немного про адресацию

Существует 2 типа адресации в сети

INET (Классовая адресация IP-сетей) архитектура сетевой адресации, которая использовалась в Интернете в период с 1981 по 1993 годы. Была вытеснена бесклассовой адресацией ввиду плохой гибкости и неэкономичного использования адресного пространства.

CIDR (Classless Inter-Domain Routing,Бесклассовая адресация) современный метод IP-адресации, при которой количество адресов в сети определяется маской подсети.

Диапазон адресов записывается в видеaddress/y, гдеy число бит маски подсети. Например, /28 означает, что 28 разряда IP-адреса отводятся под номер сети, а остальные 4 разряда полного адреса под адреса хостов этой сети, адрес этой сети и широковещательный адрес сети.

Например, запись192.168.5.0/24означает диапазон адресов от192.168.5.1до192.168.5.254, а также192.168.5.0 адрес сети и192.168.5.255 широковещательный адрес сети.

Типы inet и cidr по умолчанию

PostgreSQL предоставляет 2 типа по умолчанию для хранения IP-адресов и диапазонов:inetиcidr. Существует путаница между официальными названиями классовой и бесклассовой адресации и типамиinet/cidr.

Типinetсодержит адрес узла, а также может содержать подсеть. Вводимое значение должно иметь форматaddress/y. Если компонентyотсутствует, то маска сети считается равной 32 (для IPv4), так что это значение будет представлять один узел.

Типcidrсодержит определение сети IPv4 (или IPv6). Вводимое значение также имеет форматaddress/y. Но еслиyкомпонент отсутствует, то сеть вычисляется по старой классовой схеме нумерации сетей (INET).

Существенным отличием этих двух типов является в том, чтоinetпринимает значения с ненулевыми битами справа от маски сети, аcidrнет. Если у вас сетевая маска /8, то типcidrтребует, чтобы все 24 крайних правых бита были равны нулю,inetне имеет этого требования. Например,255.0.0.2/8будет ошибочным дляcidrт.к. справа от маски255.0.0.0имеются ненулевые значения (цифра 2 в последнем разряде адреса).255.128.128.7/24, 255.255.255.255/31 тоже ошибочны, а вот для типаinetявляются валидными.

А может уже померим что-нибудь?

Выполним несколько предварительных настроек на локальной машине (MacBook 16, 2019 2,6 GHz 6-Core Intel Core i7). Создадим таблицу и добавим индекс для поля с IP-адресом:

CREATE INDEX ON ip_ranges USING GIST (ip_range inet_ops);

Попробуем выполнить большое количество запросов (1.000.000) определения вхождения в диапазон IP-адреса клиента с помощью цикла:

DO$$DECLARE  i RECORD;BEGIN FOR i IN 1..1000000 LOOP  PERFORM country_id FROM ip_ranges WHERE ip_range >>= {random_ip}; end loop;END;$$;

и посчитаем среднее время определения адреса.

inet

cidr

749 мкс

891 мкс

Волшебный ip4r

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

Расширение гарантирует, что умеет в индексы лучше, чем встроенные типы PostgreSQL. И указывает на низкую производительность дефолтных типов даже в новых версиях СУБД. Кроме того, говорит о перегруженности дефолтных типов.

После тех же самых проверок получили значительное снижение времени определения страны пользователя до38 мкс.

Серебряная пуля (или нет?)

Если вдруг вы используетеnginx, то для него естьgeo модуль, позволяющий определять по IP-адресу нужный параметр. Создадим сервис черезdocker-compose.yml:

version: '3.7'services:  web:    image: nginx:latest    volumes:      - ./nginx.conf:/etc/nginx/nginx.conf      - ./GeoIP.dat:/var/geo/GeoIP.dat      - ./geo.conf:/var/geo/geo.conf    ports:      - "8080:80"    environment:      - NGINX_PORT=80

Конфигnginx:

http {        ...    geo $geo {        default        NONE;        include        /var/geo/geo.conf;    }    geoip_country /var/geo/GeoIP.dat;        ...    server {        ...        location / {            ...            add_header Geo-By-File $geo;            add_header Geo-By-Binary $geoip_country_code;        }    }}

Мы можем получать гео клиента, через переменную$geo, предварительно сгенерировав файлgeo.confтипа:

128.0.0.0/1 US;...

Или скачать бинарный файлGeoIP.datи использовать его без генерации, получая гео через переменные ($geoip_country_code).

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

Выводы

Стоит отметить, что измерения производились в клиенте PostgreSQL и свели к минимуму оверхед языка программирования. Если вы боретесь за милисекунды, то стоит учесть и этот факт.

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

В случае если вам не нужно хорошее быстродействие, мало клиентов, вы не боретесь за доли секунды, то вам подойдут типы по умолчаниюinetилиcidr, различие между которыми находятся в рамках статистической погрешности. Расширениеip4rпозволит сократить время в ~20 раз.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru