Ответы пользователя по тегу Go
  • Slog. Нужно ли предавать логгер через параметры или объявить его на уровне модуля и обращаться к нему?

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

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

    Лично я предпочитаю пользоваться принципом "Бритвы Оккама" и не вводить новых сущностей без необходимости.

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

    Тем более, что даже для тестов вы можете написать для slog какой-то особый хэндлер, который будет работать так, как надо в тестах.

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

    Вот вам крайне увлекательное и крайне полезное видео, как из глобального slog можно сделать просто монстра логгирования на все случаи жизни.
    https://www.youtube.com/watch?v=p9XiOOU52Qw

    Вот пример того, как я настраивал логгер в одном из маленьких проектов. Он определяет по конфигу, с какого уровня надо логгировать, в каком формате, и в какой поток (stdout, stderr, файл)

    package app
    
    import (
    	"fmt"
    	"io"
    	"log/slog"
    	"os"
    )
    
    // SetupLogger создаёт логгер на основании конфига
    // Конфигурируем дефолтный slog и возвращаем функцию закрытия файла логов, 
    // если был выбран файл как место логгирования.
    func SetupLogger(cfg config.Config) func() error {
    	closer := func() error { return nil }
    
    	level := cfg.LogLevel
    
    	var w io.Writer
    
    	switch cfg.LogOutput {
    	case config.LogOutputStderr:
    		w = os.Stderr
    	case config.LogOutputStdout:
    		w = os.Stdout
    	case config.LogOutputFile:
    		w, closer = getFileWriter(cfg)
    	}
    
    	var handler slog.Handler
    	switch cfg.LogFormat {
    	case config.LogFormatText:
    		handler = slog.Handler(slog.NewTextHandler(w, &slog.HandlerOptions{Level: level}))
    	case config.LogFormatJSON:
    		handler = slog.Handler(slog.NewJSONHandler(w, &slog.HandlerOptions{Level: level}))
    	}
    
    	slog.SetDefault(slog.New(handler))
    
    	return closer
    }
    
    // getFileWriter возвращает файл, куда писать логи и функцию закрытия этого файла
    func getFileWriter(cfg config.Config) (*os.File, func() error) {
    	logDir := fmt.Sprintf("%s/%s", cfg.VarDir, cfg.LogDir)
    	err := os.MkdirAll(logDir, 0755)
    	if err != nil {
    		panic(fmt.Sprintf("error creating directory: %s;  %v", logDir, err))
    	}
    
    	filename := fmt.Sprintf("%s/%s.log", logDir, cfg.AppEnv)
    	f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    	if err != nil {
    		panic(fmt.Sprintf("error opening file: %s %v", filename, err))
    	}
    	return f, f.Close
    }


    package main
    
    import "app"
    
    func main() {
    
            // .....
    
    	closeLogFile := app.SetupLogger(cfg)
    	defer closeLogFile()
    
            // ...
    }
    Ответ написан
    5 комментариев
  • Как сделать импорт переменной из 1 пакета, когда она в другой?

    Go не любит лишних усложнений. У языка такая философия.
    Вы ведь всё равно все ошибки держите в одном пакете, т.е. они всё равно в одной куче с точки зрения программы. Почему бы тогда вместо папок не использовать просто файлы? Т.о. плоская структура с именами файлов вместо папок будет соответствовать вашим нуждам.

    err.go
    account.go
    auth.go
    json.go
    registation.go
    Ответ написан
    Комментировать
  • Где писать логику работы с БД?

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

    Однако, лично я считаю этот паттерн вредным, потому что он намешивает всё в одну кучу. Это моё субъективное мнение, и я вам его не навязываю. Я бы отделил мух от котлет, и оставил бы структуру чистой, т.е. только данные и какие-то методы, которые работают конкретно с этими данными из структуры, а взаимодействие с базой данных организовал бы через паттерн проектирования Repository. Т.е. мы делаем уже отдельную структуру ProductRepository, которая содержит в себе методы, принимающие и отдающие структуру Product, и уже в этих методах реализуем вызов базы данных.

    Я набросаю вам простенький интерфейс для использования этого паттерна, а конкретную структуру, реализующую этот интерфейс вы уже напишете сами. Интерфейсы использовать в таких случаях считаю вообще обязательным условием. Например, так нам будет гораздо проще тестировать код, всё становится гораздо гибче. Ведь структурку Product вы сможете без проблем использовать не только при работе с базой, но и при передаче данных в другие системы, не волнуясь о лишних методах, висящих на ней. Так же вы сможете легко менять реализации репозитория, не меняя бизнес-логику проекта. (Это позволит, например, легко заменить базу данных)

    package models
    
    type Product struct {
    	ID    int    `json:"id"`
    	Title string `json:"title"` //Название
    	Price int    `json:"price"` //Цена
    }
    
    type ProductGetter interface {
        GetAll() ([]Product, error)
        GetByID(id int) (Product, error)
    }
    
    type ProductCreator interface {
        Create(product Product) (Product, error)   
    }
    
    type ProductUpdater interface {
        Update(product Product) (Product, error)
    }
    
    type ProductDeleter interface {
        Delete(id int) error
    }
    
    type ProductRepository interface {
        ProductGetter
        ProductCreator
        ProductUpdater
    }
    Ответ написан
    4 комментария
  • Как маппить результат запроса бд в структуры со вложенностями Golang?

    - Я считаю, что свой кастомный маппер - это отличное решение. Чтобы быть уверенней, обязательно напишите тесты. Как unit, так и интеграционные, например, с помощью testcontainers.

    - Можете вместо pgx посмотреть в сторону sqlc. У него совершенно другой подход. Вы пишете запрос со всеми джойнами, а он вам автоматически генерирует код со всеми структурами и функциями. Правда, там по джойнам практически отсутствует документация, но уж погуглите.

    - Ну, и если совсем сложно... то... может одним глазком глянуть в сторону ORM? Я никому не расскажу...
    Ответ написан
    5 комментариев
  • Как устранить утечку памяти при множественных соединениях в net/http Golang?

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

    var client = &http.Client{
        Timeout: time.Millisecond * 100,
    }
    
    func scan(ip string) (error, string, string, string) {
        resp, err := client.Get("http://" + ip)
        // Здесь сразу желательно обработать ошибку ...
        
        // Закрываем соединение в конце выполнения
        // функции в любом случае,
        // даже если где-то возникнет ошибка
        defer resp.Body.Close()
    
        // Закрытие неиспользуемых соединений
        // Нужно ли это теперь?
        // client.CloseIdleConnections()
    
        // Проблема могла быть тут
        // if err != nil {
        //     return err, "", "", ""
        // }
    
        headers := buildHeaders(resp.Header)
        body, _ := io.ReadAll(resp.Body)
    
        // и это нам уже особо не нужно
        // resp.Body.Close()
    
        return err, headers, string(body), resp.Status
    }
    Ответ написан
    3 комментария
  • Почему я получаю ошибку invalid operation: err (variable of type Error) is not an interface при проверке типа переменной?

    Я полностью присоединюсь к ответу выше, просто хочу дополнить.

    Замечу, что вы проверяете не то, что нужно, шиворот-навыворот. Проверять при приведении типов лучше конкретные типы. Т.е. мы получаем из функции ошибку в виде интерфейса error, а уже в проверке проверяем её на наш кастомный тип MyError. Поэтому желательно (не обязательно) не проверять переменную ошибки в той же функции, где вы её создали, а возвращать откуда-то и уже тогда проверять на ошибку.

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

    package main
    
    import (
    	"fmt"
    )
    
    type MyError struct {
    	Message string
    }
    
    func (e MyError) Error() string {
    	return e.Message
    }
    
    func main() {
    	// Возвращаем из функции нашу кастомную ошибку, но в виде интерфейса error
    	err := foo()
    
    	if err == nil {
    		fmt.Println("Нет ошибки")
    	// И теперь тут приводим error к нашему типу MyError и проверяем
    	} else if myErr, ok := err.(MyError); ok {
    		fmt.Printf("Ура! Нужный нам тип ошибки: %v\n", myErr.Message)
    	} else {
    		fmt.Println("Какой-то другой тип ошибки:", err)
    	}
    
    	// Проверка одной из "подстав" Go
    
    	err = bad()
    	if err != nil {
    		fmt.Println("Упс... Как так... Не nil...")
    	} else {
    		fmt.Println("Должно вывестись это, но не выводится...")
    	}
    
    	err = good()
    	if err != nil {
    		fmt.Println("Это не должно выводиться, всё верно.")
    	} else {
    		fmt.Println("Ошибки нет, всё верно.")
    	}
    }
    
    func foo() error {
    	err := MyError{"Ой! Ошибка MyError!"}
    	// err := fmt.Errorf("Ой! Обычная ошибка!")
    	// var err error = nil
    	return err
    }
    
    func bad() error {
    	var p *MyError = nil // Вроде же nil, но не работает....
    	// p = &MyError{"Ой!"} // Пробуем создать ошибку, и всё работает.
    
    	if p == nil {
    		fmt.Println("Ну nil же-ж... Должно же-ж работать", p)
    	}
    
    	return p
    }
    
    func good() error {
    	// return MyError{"Ой!"}
    
            // Буквально пишем "nil", никаких указателей, которые равны nil, это прямой выстрел в ногу
    	return nil
    }


    https://go.dev/play/p/2YcWcH9oqel
    Ответ написан
    Комментировать
  • Насколько актуальна книга Марка Саммерфильда?

    Не читал, но не осуждаю)

    Я считаю, что если многие советуют, то можно брать, если хотите.

    Go - язык немного особенный в плане устаревания информации. Если, например, купить книгу по JavaScript того года, то это будет супер-вредно, потому что в те времена это был чуть ли не другой язык.

    А создатели Go с самого начала заявили, что обратная совместимость будет одной из основных характеристик языка. Роб Пайк вообще говорил, что Go 2 никогда не будет.

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

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

    P. S. Совет лично от меня: читайте книги по программированию на английском. Очень часто переводчики просто бредят как ChatGPT, и некоторые книги просто невозможно читать.
    Ответ написан
    2 комментария
  • Как в goшном SOA сервисе работать с базой данных?

    Не знаю, какая у вас архитектура, но когда какая-то библиотека обрастает огромным количеством методов, то Go - это один из редких языков программирования, который, благодаря своей утиной типизации позволяет использовать шаблон проектирования "consumer interfaces", т.е. объявлять интерфейсы в месте, где они используются и включать в эти интерфейсы только те методы из огромной структуры базы данных, которые именно тут, на месте, и используются. Таким образом ваши интерфейсы будут минимальными, понятными, и искать их не придётся. Ну, и в добавок, старый добрый Dependency Injection решит массу наших проблем

    База данных:
    package db
    
    import "fmt"
    
    // Стуктура базы данных. Здесь нам даже и не нужно объявлять никаких интерфейсов
    type Service struct {
    	// ...
    }
    
    func NewDbService() (*Service, error) {
    	return &Service{}, nil
    }
    
    func (d *Service) CreateUser(username string, email string) {
    	fmt.Println(fmt.Sprintf("user %s with email %s is created", username, email))
    }
    
    func (d *Service) CreateProduct(name string) {
    	fmt.Println(fmt.Sprintf("product  %s is created", name))
    }


    Пользователь:
    package user
    
    // Тот самый интерфейс, который нам позволяет выбрать из структуры базы данных
    // только нужные нам здесь методы
    type dbUser interface {
    	CreateUser(username string, email string)
    }
    
    type Service struct {
    	db dbUser
    	// ...
    }
    
    func NewService(db dbUser) (*Service, error) {
    	return &Service{db}, nil
    }
    
    func (c *Service) New(username string, email string) {
    	c.db.CreateUser(username, email)
    }


    Товар:
    package product
    
    // Тот самый интерфейс, который нам позволяет выбрать из структуры базы данных
    // только нужные нам здесь методы
    type dbProduct interface {
    	CreateProduct(name string)
    }
    
    type Service struct {
    	db dbProduct
    	// ...
    }
    
    func NewService(db dbProduct) (*Service, error) {
    	return &Service{db}, nil
    }
    
    func (p *Service) New(name string) {
    	p.db.CreateProduct(name)
    }


    main.go
    package main
    
    import (
    	"test2/db"
    	"test2/product"
    	"test2/user"
    )
    
    func main() {
            // Инициализируем базу данных
    	dbService, _ := db.NewDbService()
            // Структура базы данных реализует интерфейс dbProduct, инжектим её
    	productService, _ := product.NewService(dbService)
            // Структура базы данных реализует интерфейс dbUser, инжектим её
    	userService, _ := user.NewService(dbService)
    
            // Пользуемся на здоровье...
    	userService.New("user1", "uswr1@example.com")
    	productService.New("product1")
    }
    Ответ написан
    Комментировать
  • Как организовать структуру проекта с несколькими модулями Golang?

    Делаем три разных файла main.go под каждый сервис. Кладём их каждый в свою подпапку в директории, например "cmd".
    А для общих библиотек используем отдельную директорию, например "pkg", и будем импортировать отсюда функционал во все три сервиса.

    Т.е. у нас получается структура файлов:
    cmd/
          repeater/
                main.go
          controller/
                main.go
          executor/
                main.go
    pkg/
          c-library/
                clibrary.go
    go.mod


    И потом запускаем компиляцию:

    go build ./cmd/repeater
    go build ./cmd/controller
    go build ./cmd/executor


    Пример main.go
    package main
    
    import c_library "test/pkg/c-library"
    
    func main() {
    	c_library.HelloWorld()
    }


    И общая библиотека:
    package c_library
    
    import "fmt"
    
    func HelloWorld() {
    	fmt.Println("HELLO WORLD")
    }


    Весь проект у меня лежит в папочке test, и в файле go.mod надо бы указать следующее

    module test
    Ответ написан
    5 комментариев
  • Как предотвратить бесконечную загрузку страницы при отправке post запроса?

    Использовать SSE (Server Sent Events). Это на порядок проще, чем вебсокеты, но мощь практически такая же.
    Вот неплохая статья на английском.
    https://blog.stackademic.com/real-time-communicati...

    Кстати, библиотека HTMX позволяет это обрабатывать и без написания Javascript
    https://htmx.org/extensions/server-sent-events/
    Ответ написан
    Комментировать