company_banner

Как работать с Postgres в Go: практики, особенности, нюансы


    Неожиданное поведение приложения в отношении работы с базой приводит к войне между DBA и разработчиками: DBA кричат: «Ваше приложение роняет базу», разработчики — «Но ведь до этого всё работало!». Хуже всего, что DBA и разработчики не могут помочь друг другу: одни не знают про нюансы работы приложения и драйвера, другие не знают про особенности, связанные с инфраструктурой. Было бы неплохо такой ситуации избежать.


    Надо понимать, часто недостаточно полистать go-database-sql.org. Лучше вооружиться чужим опытом. Еще лучше, если это будет опыт, полученный кровью и потерянными деньгами.



    Меня зовут Рябинков Артемий и эта статья — вольная интерпретация моего доклада с конференции Saints HighLoad 2019.


    Инструменты


    Минимально необходимую информацию о том, как в Go работать с любой SQL-подобной базой данных, вы сможете найти на go-database-sql.org. Если еще не читали — прочитайте.


    sqlx


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


    Поэтому, взяв стандартный пакет языка database/sql, вы захотите расширить его интерфейсы. Как только это произойдёт, взгляните на github.com/jmoiron/sqlx. Покажу несколько примеров, как это расширение может упростить вам жизнь.


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


    type Place struct {
        Country       string
        City          sql.NullString
        TelephoneCode int `db:"telcode"`
    }
    
    var p Place
    err = rows.StructScan(&p)

    Использование NamedQuery позволяет в качестве плейсхолдеров в запросе использовать свойства структуры.


    p := Place{Country: "South Africa"}
    
    sql := `.. WHERE country=:country`
    rows, err := db.NamedQuery(sql, p)

    Использование Get и Select позволяет избавиться от необходимости руками писать циклы, которые достают строки из базы.


    var p Place
    var pp []Place
    
    // Get записывает в p данные из первой строки
    err = db.Get(&p, ".. LIMIT 1")
    
    // Select записывает в pp массив полученных строк.
    err = db.Select(&pp, ".. WHERE telcode > ?", 50)

    Драйверы


    database/sql — это набор интерфейсов для работы с базой, а sqlx — их расширение. Чтобы эти интерфейсы работали, для них нужна реализация. Именно за реализацию и отвечают драйверы.


    Наиболее популярные драйверы:


    • github.com/lib/pqpure Go Postgres driver for database/sql. Этот драйвер долгое время оставался стандартом по умолчанию. Но на сегодняшний день он потерял свою актуальность и не развивается автором.
    • github.com/jackc/pgxPostgreSQL driver and toolkit for Go. Сегодня лучше выбрать этот инструмент.

    github.com/jackc/pgx — именно этот драйвер вы хотите использовать. Почему?


    • Активно поддерживается и развивается.
    • Может быть производительнее в случае использования без интерфейсов database/sql .
    • Поддержка более 60 типов PostgreSQL, которые PostgreSQL реализует вне стандарта SQL.
    • Возможность удобно реализовать логирование того, что происходит внутри драйвера.
    • У pgx человекопонятные ошибки, в то время как просто lib/pq бросает паники. Если не поймать панику, программа упадет. (Не стоит использовать паники в Go, это не то же самое, что исключения.)
    • С pgx у нас есть возможность независимо конфигурировать каждое соединение.
    • Есть поддержка протокола логической репликации PostgreSQL.

    4KB


    Типично мы пишем вот такой цикл, чтобы получать данные из базы:


    rows, err := s.db.QueryContext(ctx, sql)
    
    for rows.Next() {
        err = rows.Scan(...)
    }

    Внутри драйвера мы получаем данные, накапливая их в буфер размером 4KB. rows.Next() порождает поход в сеть и наполняет буфер. Если буфера не хватает, то мы идём в сеть за оставшимися данными. Больше походов в сеть – меньше скорость обработки. С другой стороны, так как предел буфера – 4KB, не забьём всю память процесса.


    Но, конечно, хочется выкрутить объём буфера на максимум, чтобы уменьшить кол-во запросов в сеть и снизить latency нашего сервиса. Добавляем такую возможность и попробуем выяснить ожидаемое ускорение на синтетических тестах:


    $ go test -v -run=XXX -bench=. -benchmem
    goos: linux
    goarch: amd64
    pkg: github.com/furdarius/pgxexperiments/bufsize
    BenchmarkBufferSize/4KB                    5     315763978 ns/op    53112832 B/op      12967 allocs/op
    BenchmarkBufferSize/8KB                    5     300140961 ns/op    53082521 B/op       6479 allocs/op
    BenchmarkBufferSize/16KB                   5     298477972 ns/op    52910489 B/op       3229 allocs/op
    BenchmarkBufferSize/1MB                    5     299602670 ns/op    52848230 B/op         50 allocs/op
    PASS
    ok      github.com/furdarius/pgxexperiments/bufsize 10.964s

    Видно, что большой разницы по скорости обработки нет. Почему так?


    Оказывается, мы упираемся в размер буфера на отправку данных внутри самого Postgres. Этот буфер имеет жестко заданный размер в 8KB. Используя strace можно увидеть, что ОС возвращает 8192 байта в системном вызове read. А tcpdump это подтверждает размером пакетов.


    Tom Lane (один из основных разработчиков ядра Postgres) это комментирует так:


    Traditionally, at least, that was the size of pipe buffers in Unix machines, so in principle this is the most optimal chunk size for sending data across a Unix socket.

    Andres Freund (разработчик Postgres от EnterpriseDB) считает, что буфер в 8KB не лучший вариант реализации на сегодняшний день и нужно тестировать поведение на других размерах и с другой конфигурацией сокета.


    Отдельно надо вспомнить, что у PgBouncer тоже есть буфер и его размер можно конфигурировать параметром pkt_buf.


    OIDs


    Другая особенность драйвера pgx (v3): на каждую установку соединения он делает запрос в базу для получения информации об Object ID (OID).


    Эти идентификаторы были добавлены в Postgres, чтобы уникально идентифицировать внутренние объекты: строки, таблицы, функции и т.д.


    Драйвер использует знание об OIDs, чтобы понимать, из какого столбца базы в какой примитив языка складывать данные. Для этого pgx поддерживает такую таблицу (ключ – название типа, значение – Object ID)


    map[string]Value{
        "_aclitem": 2,
        "_bool": 3,
        "_int4": 4,
        "_int8": 55,
        ...
    }

    Эта реализация приводит к тому, что драйвер на каждое устанавливаемое соединение с базой делает около трех запросов, чтобы сформировать таблицу с Object ID. При штатном режиме работы базы и приложения пул соединений в Go позволяет не порождать новые соединения к базе. А вот при малейшей деградации базы данных пул соединений на стороне приложения исчерпывается и кол-во порождаемых соединений на единицу времени в разы возрастает. Запросы для получения OIDs достаточно тяжёлые, в итоге, драйвер может довести базу до критического состояния.


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



    15 транзакций в минуту в нормальном режиме, скачок до 6500 транзакций при деградации.


    Что делать?


    Первое и основное — ограничивайте сверху размер вашего пула.


    Для database/sql это можно сделать функцией DB.SetMaxOpenConns. Если откажетесь от интерфейсов database/sql и будете использовать pgx.ConnPool (пул соединений, реализуемый самим драйвером), то в ConnPoolConfig можно указать MaxConnections (по умолчанию 5).


    Кстати, при использовании pgx.ConnPool драйвер будет переиспользовать информацию о полученных OIDs и не будет на каждое новое соединение делать запросы к базе.


    Если от database/sql отказываться не хочется, то можно кешировать информацию об OIDs самим.


    github.com/jackc/pgx/stdlib.OpenDB(pgx.ConnConfig{
        CustomConnInfo: func(c *pgx.Conn) (*pgtype.ConnInfo, error) {
            cachedOids = // Кешируем OIDs при первом запросе.
    
            info := pgtype.NewConnInfo()
            info.InitializeDataTypes(cachedOids)
    
            return info, nil
        }
    })

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


    • вы используете enum или доменные типы в Postgres;
    • в случае отказа мастера вы переключаете приложение на реплику, которая наливается логической репликацией.

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


    В мире Postgres для организации высокой доступности обычно используют физическую репликацию, которая побитово копирует инстансы базы, поэтому проблемы из-за кеширования OIDs редко можно встретить в дикой природе. (Но лучше уточнить у своих DBA, как у вас standby работает).


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


    Логирование и мониторинг


    Мониторинг и логирование помогут заметить проблемы раньше, чем упадет база.


    database/sql предоставляет метод DB.Stats(). Возвращаемый снепшот состояния даст вам понимание того, что происходит внутри драйвера.


    type DBStats struct {
            MaxOpenConnections int
    
            // Pool Status
            OpenConnections int
            InUse           int
            Idle            int
    
            // Counters
            WaitCount         int64
            WaitDuration      time.Duration
            MaxIdleClosed     int64
            MaxLifetimeClosed int64
    }

    Если используете пул в pgx напрямую, то похожую информацию вам даст метод ConnPool.Stat():


    type ConnPoolStat struct {
        MaxConnections       int
        CurrentConnections   int
        AvailableConnections int
    }

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


    type Logger interface {
        // Log a message at the given level with data key/value pairs.
        // data may be nil.
        Log(level LogLevel, msg string, data map[string]interface{})
    }

    Скорее всего, вам даже не придется самому реализовывать этот интерфейс. В pgx из коробки есть набор адаптеров для наиболее популярных логеров, например uber-go/zap, sirupsen/logrus, rs/zerolog.


    Инфраструктура


    Почти всегда при работе с Postgres вы будете использовать connection pooler, и это будет PgBouncer (или odyssey – если вы Yandex).


    Почему так, можно почитать в отличной статье brandur.org/postgres-connections. Если кратко, то при кол-ве клиентов больше 100 скорость обработки запросов начинает деградировать. Происходит это из-за особенностей реализации самого Postgres: запуск отдельного процесса на каждое соединение, механизм снятия снапшотов и использование общей памяти для взаимодействия — всё это влияет.


    Вот benchmark различных реализаций connection pooler'ов:


    И benchmark пропускной способности с PgBouncer и без него.



    В результате ваша инфраструктура примет такой вид:



    Где Server — процесс, обрабатывающий запросы пользователей. Этот процесс крутится в kubernetes в 3-х экземплярах (как минимум). Отдельно, на железном сервере, стоит Postgres, прикрытый PgBouncer'ом. Сам PgBouncer однопоточный, поэтому запускаем несколько баунсеров, трафик на которые балансируем, используя HAProxy. В итоге получаем такую цепочку выполнения запроса в базу: пул соединений приложения → HAProxy → PgBouncer → Postgres.


    PgBouncer умеет работать в трех режимах:


    • Session pooling — каждой сессии выдается одно соединение и закрепляется за ней на всё время жизни.
    • Transaction pooling — соединение живёт, пока работает транзакция. Как только транзакция завершилась, PgBouncer забирает это соединение и отдает другой транзакции. Этот режим позволяет очень хорошо утилизировать соединения.
    • Statement poolingdeprecated режим. Он был создан только для того, чтобы поддерживать PL/Proxy.

    Можно посмотреть матрицу того, какие свойства в каждом режиме доступны. Мы выбираем Transaction Pooling, но у него есть ограничения по работе с Prepared Statements.


    Transaction Pooling + Prepared Statements


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



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



    В режиме Transaction Pooling две транзакции могут быть выполнены в разных соединениях, но Statement ID действителен только в рамках одного соединения. Получаем ошибку prepared statement does not exist при попытке выполнить запрос.


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


    А теперь найдите Prepared Statements в таком коде:


    sql := `select * from places where city = ?`
    rows, err := s.db.Query(sql, city)

    Вы его не увидите! Подготовка запроса будет неявно происходить внутри Query(). При этом и подготовка, и выполнение запроса будут происходить в разных транзакциях и мы в полной мере получим все то, что я описал выше.


    Что делать?


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


    Второй вариант — готовить запрос на стороне клиента. Этого не хочется делать по двум причинам:


    • Потенциальные SQL-уязвимости. Разработчик может забыть или некорректно сделать экранирование.
    • Экранирование параметров запроса приходится каждый раз писать руками.

    Еще один вариант — явно оборачивать каждый запрос в транзакцию. Ведь пока транзакция живет, PgBouncer не забирает соединение. Это работает, но, кроме многословности в нашем коде, мы вдобавок получаем большее количество сетевых вызовов: Begin, Prepare, Execute, Commit. Итого 4 сетевых вызова на один запрос. Latency растёт.


    Но хочется и безопасно, и удобно, и эффективно. И такой вариант есть! Можно явно указать драйверу, что хочешь использовать режим Simple Query. В этом режиме не будет подготовки и весь запрос пройдёт в одном сетевом вызове. При этом драйвер сам сделает экранирование каждого из параметров (standard_conforming_strings должен быть активирован на уровне базы или при установке соединения).


    cfg := pgx.ConnConfig{
        ...
        RuntimeParams: map[string]string{
            "standard_conforming_strings": "on",
        },
        PreferSimpleProtocol: true,
    }

    Отмена запросов


    Следующие проблемы связаны с отменой запросов на стороне приложения.


    Взгляните на этот код. Где тут подводные камни?


    rows, err := s.db.QueryContext(ctx, ...)

    В языке Go есть метод контроля потока выполнения программы – context.Context. В этом коде мы передаём ctx драйверу, чтобы при закрытии контекста драйвер отменил запрос на уровне базы данных.


    При этом ожидается, что мы сэкономим ресурсы, отменив запросы, ответ на которые никто не ждёт. Но при отмене запроса PgBouncer версии 1.7 в соединение отправляет информацию о том, что это соединение готово к использованию, и уже после этого возвращает его в пул. Такое поведение PgBouncer'а вводит в заблуждение драйвер, который при отправке следующего запроса мгновенно получает в ответ ReadyForQuery. В итоге мы ловим ошибки unexpected ReadyForQuery.


    Начиная с PgBouncer версии 1.8 это поведение было исправлено. Используйте актуальную версию PgBouncer.


    Отложенная отмена


    Но самое интересное в том, как работает отмена запросов. Чтобы отменить запрос, нам нужно создать новое соединение с базой и запросить отмену. На каждое соединение Postgres создаёт отдельный процесс. Мы отправляем команду на отмену текущего запроса в конкретном процессе. Для этого создаём новое соединение и в нём передаём ID процесса (PID), интересующего нас. Но пока команда на отмену летит до базы, отменяемый запрос может завершиться самостоятельно.



    Postgres выполнит команду и отменит текущий запрос в заданном процессе. Но текущим запросом будет уже не тот, который мы хотели отменить изначально. Из-за такого поведения при работе с Postgres вместе с PgBouncer безопаснее будет не отменять запрос на уровне драйвера. Для этого можно задать функцию CustomCancel, которая не будет отменять запрос, даже если используется context.Context.


    cfg := pgx.ConnConfig{
        ...
        CustomCancel: func(_ *pgx.Conn) error { return nil },
    }

    Чеклист по работе с Postgres


    Вместо выводов решил сделать чеклист по работе с Postgres. Это должно помочь статье уложиться в голове.


    • Используйте github.com/jackc/pgx как драйвер для работы с Postgres.
    • Ограничивайте сверху размер пула соединений.
    • Кешируйте OIDs или используйте pgx.ConnPool, если работаете с pgx версии 3.
    • Собирайте метрики по пулу соединений, используя DB.Stats() или ConnPool.Stat().
    • Логируйте происходящее в драйвере.
    • Используйте режим Simple Query, чтобы избежать проблем с подготовкой запросов в транзакционном режиме PgBouncer.
    • Обновляйте PgBouncer до актуальной версии.
    • Будьте аккуратны с отменой запросов со стороны приложения.
    Конференции Олега Бунина (Онтико)
    755,93
    Конференции Олега Бунина
    Поделиться публикацией

    Похожие публикации

    Комментарии 27

      0
      На мой взгляд, сила Go в простоте. И это выражается, например, в том, что в Go принято писать запросы на голом SQL (ORM не в чести). Это и преимущество, и источник дополнительных трудностей.

      Кем принято, если не секрет? Насколько я вижу — у gorm, к примеру, очень активная и большая аудитория.
        +4
        Аудитория у gorm, действительно, большая. Я это объясняю только тем, что люди переходя с других языков тащат за собой практику использовать orm.

        Но, по разным причинам, в Go чуть меньше распространены orm.

        А в целом, у постгри много функциональности, которая слабо покрывается orm'ками: мощные cte, насыщенная система типов, функции для работы индексами, мощная индексация по json (а в 12 версию закоммитили jsonpath www.postgresql.org/docs/12/datatype-json.html#DATATYPE-JSONPATH) и большое кол-во других вещей, которые делают использование orm неразумным.

        orm-холивар предлагаю не разводить. Отличный вопрос, мне понравился, спасибо!)
          +1
          мощная индексация по json

          Стоит обратить внимание, что по json просто нельзя собрать статистику.
          А база, планер которой работает по весовой схеме, просто не может нормально работать без статистики.


          Но надеюсь скоро исправят, здесь smagen писал: http://akorotkov.github.io/blog/2015/09/07/jsonb_statistics/ для 10-ки точно еще актуально.

            0
            Спасибо за такое хорошее уточнение!)
            0
            мощные cte, насыщенная система типов, функции для работы индексами, мощная индексация по json
            Плюс замечательный PL/pgSQL. Мы так вообще отказались от идеи разрешать приложениям лезть в базу с запросами напрямую, только через API, благо и JDBC, и psycopg2 умеют в callable statements/procedures. В результате вся логика изолирована, больше не надо заворачивать семантику в строку (ура!), можно вообще не париться насчет кавычек и экранирования (тайпчекер постгреса сам проверит корректность аргументов), многие вещи можно фиксить в продакшне без пересборки и перезапуска приложений и пр.

            Transaction Mode и Prepared Statements это известная проблема, мы просто запретили их на клиенте через prepareThreshold=0.
          0
          github.com/lib/pq — pure Go Postgres driver for database/sql. Этот драйвер долгое время оставался стандартом по умолчанию. Но на сегодняшний день он потерял свою актуальность и не развивается автором.


          С чего вы это взяли? Кодовая база обновляется, последние обновления несколько дней назад.
          На Гитхабе никакой информации о прекращении поддержки нет, да и авторов там не один.
            +1
            Не развивается. Либо очень медленно.
            Вот фикс который я очень давно ждал, так и не дождался. Сильно влияет на производительность.
            Использую в результате pgx.
              +1
              Уходя немного в сторону: я нажал в том PR Approve и одобрил изменения (полагая, что ничего не произойдёт, коненчо). Но нет, моё одобрение появилось в списке ревьюверов.

              Это Гитхаб так работает и любой может жать кнопки в любом публичном репозитории?
                0
                Не во всех. Видимо так настроили :)
                  0
                  Любой может одобрить PR, но они показываются отдельным списком и не учитываются в подсчете количества необходимых одобрений для мержа
                +2

                Кроме примера из треда, могу еще вот такой PR показать https://github.com/lib/pq/pull/714. О том, что библиотека заброшена пишут даже люди имеющие статус Collaborator.


                Основными людьми, поддерживающими жизнь в библиотеке остаются ребята из cockroachdb. Но, например mjibson пишет


                It is indeed sad this has no active maintainers. Open source has its problems.
                I don't have permissions to grant permissions so I'm not able to do anything. A few of us from cockroach just asked the last time this was a problem and they gave us commit here. We just keep limping along.

                Мне кажется, правильным решением будет пометить библиотеку как deprecated, заархивировать проект и забыть о нём. pgx давно обогнал. А с 4 версии станет совсем хорошо)

                  +1
                  Значит буду пробовать новый драйвер. Спасибо.
                +3
                хочется сказать спасибо Артемию за интересный доклад, т.к. сами внутри прошли весь тот же путь в прошлом году, и добавить, что с момент доклада произошло радостное событие — он подготовил и закинул ПР с поддержкой многосерверности в DSN (https://github.com/jackc/pgx/pull/545) c автопереключением на текущего мастера при проблемах и этот ПР был принят и вмержен в pgx.
                  0

                  Спасибо вам)

                  –1
                  github.com/jackc/pgx — именно этот драйвер вы хотите использовать.


                  Хммм… а GORM использует github.com/lib/pq. Интересно, почему не перешли.
                    0

                    В issue глянул — с ходу не нашел. Возможно еще не осознали, что пора.

                    +3
                    С одной стороны, после стольких лет использования Sequel и ActiveRecord — так больно смотреть на гошников, жующих свой кактус простоты с самого нуля.
                    С другой стортоны, этот же кактус заставляет лучше и глубже разбираться в том, как все работает внутри, что явно плюс!
                      0
                      Добрый день, не понял, почему нужно использовать дополнительно pgbouncer, если в пакете БД для Го уже идет свой пуллер соединений с БД.
                        0
                        Просто потому что приложении много и, даже при использовании пула внутри, все вместе они могут породить соединений больше чем порог деградации.
                        0
                        При штатном режиме работы базы и приложения пул соединений в Go позволяет не порождать новые соединения к базе. А вот при малейшей деградации базы данных пул соединений на стороне приложения исчерпывается и кол-во порождаемых соединений на единицу времени в разы возрастает.

                        А можете прояснить, какая тут причинно-следственная связь?
                          0
                          Впрочем, я понял. Имеется в виду, что пул не исчерпывается (если он ограничен сверху, то какие проблемы?), а наоборот, пул разрастается.
                            0
                            Я воспринимаю пул как ведро) Мы в него кладём соединения если их не используем и забираем оттуда, когда нужно заиспользовать. Но когда в ведре нет соединений — приходится устанавливать новое соединение с базой.
                          +1
                          Потенциальные SQL-уязвимости. Разработчик может забыть или некорректно сделать экранирование.

                          Вероятность этого остается в pgx. Дело в том, что pgx честно делает Simple Query внутри себя, выполняя экранирование аргументов.
                          github.com/jackc/pgx/blob/0151aeb3077d63ed90110bc083521f709a5d4fef/query.go#L524
                          Если в этой функции пойдет что-то не так, то это прямой путь к уязвимостям.
                          lib/pq делает иначе. При указании в connection string параметра binary_parameters=yes драйвер отправит один запрос, в котором сначала идет Parse query string, затем Bind параметров в бинарном виде.
                          github.com/lib/pq/blob/master/conn.go#L835
                          Видно, что далее драйвер честно читает Parse и Bind response от базы, как в сценарии с Extended Query.
                          Это позволяет разработчику или драйверу не собирать запрос в строку самостоятельно, а передать это все базе.
                            0
                            Спасибо! Вы правы, в данном случае экранирование делает сам драйвер. Я надеюсь найти время, закоммитить в pgx передачу параметров в бинарном виде. Но, кстати, этот протокол не стандартизирован и может быть изменен в следующих версиях постгреса. Впрочем, это не так страшно, так как переезд на новую версию дело долгое и можно будет подготовить драйвер к этому.

                            Как преимущество, с simple query меньше запросов в сеть и ниже latency)
                            0
                            Спасибо за доклад! Скажите, пожалуйста, рассматривали ли вы для себя пакет go pg? По большей части — это пакет, заточенный именно под Postgres. В нем есть очень приятная фича — это внутренний пакет ORM, который дает вам общий для транзакций и обычных соединений интерфейс, за счет которого можно абстрагировать бизнес логику от инфраструктурного слоя.
                              0
                              > абстрагировать бизнес логику от инфраструктурного слоя.
                              Так как мы работу с базой делаем сразу отдельным слоем (что-то вроде паттерна repository, но без фанатизма), то необходимости в ORM не возникает. Собственно, из-за того, что gopg чуть переусложнена, не хочется её использова. Ну и опыта с pgx просто больше)
                              0
                              Flaker, спасибо за материал, не подскажете, как с помощью sqlx импортировать csv в таблицу postgres?

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

                              Самое читаемое