Задать вопрос
Ответы пользователя по тегу Go
  • Кто нибудь применял или видел generics в серьезных Go проектах?

    Одно из самых полезных применений дженериков, которое у меня было — это написание врапперов, которые добавляют строгую типизацию в обработку чего-либо и уменьшают бойлерплейт. Например, обработка http-запросов. Обычно используется роутер и в него регистрируются хендлеры для разных путей, например:
    router := httprouter.New()
    
    router.POST("/api/products", productsHandler.Handle)
    router.POST("/api/get_free_slots", getFreeTimeSlotsHandler.Handle)
    router.POST("/api/create_visit", createVisitHandler.Handle)


    При этом, функция Handle у каждого хендлера это обычно что-то типа func(w http.ResponseWriter, r *http.Request)
    Хендлер уже внутри себя сам читает тело, парсит его из json, потом формирует ответ, итд.

    Хотелось добавить сюда типизацию и какую-то структуру, поэтому я написал
    простой враппер

    type Validatable interface {
    	Validate() error
    }
    
    func Wrap[Req Validatable, Res any](fn func(ctx context.Context, req Req) (Res, error)) httprouter.Handle {
    	handler := func(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
    		writeError := func(statusCode int, err error) {
    			writer.WriteHeader(statusCode)
    			_, _ = writer.Write([]byte(err.Error()))
    		}
    
    		var req Req
    
    		bytes, err := io.ReadAll(request.Body)
    		if err != nil {
    			writeError(http.StatusBadRequest, err)
    			return
    		}
    
    		if err := json.Unmarshal(bytes, &req); err != nil {
    			writeError(http.StatusBadRequest, err)
    			return
    		}
    
    		if err := req.Validate(); err != nil {
    			writeError(http.StatusBadRequest, err)
    			return
    		}
    
    		resp, err := fn(request.Context(), req)
    		if err != nil {
    			writeError(http.StatusInternalServerError, err)
    			return
    		}
    
    		respBytes, err := json.Marshal(resp)
    		if err != nil {
    			writeError(http.StatusInternalServerError, err)
    			return
    		}
    		writer.WriteHeader(http.StatusOK)
    		_, _ = writer.Write(respBytes)
    	}
    
    	return handler
    }



    Он берет на себя весь бойлерплейт по парсингу и валидации запроса, формированию ответа, работы с кодами, итд. Причем, через стандартный errors.Is иногда добавлял сюда еще возможность из хэндлера указать врапперу, какой http-код ответа отдавать. Обычно все функции враппера делал под конкретную задачу в конкретном проекте (можно, например, не только json тела парсить, но и урл и хедеры, итд).

    Использование враппера выглядит так.
    Мы пишем хэндлер, используя конкретные типы

    type Handler struct {
    	userService *usersvc.Service
    }
    
    type Request struct {
    	UserID int64 `json:"user_id"`
    }
    
    func (c Request) Validate() error {
    	if c.UserID == 0 {
    		return errors.New("empty user id")
    	}
    	return nil
    }
    
    type Response struct {
    	Name string `json:"name"`
    	Age  int    `json:"age"`
    }
    
    func New(userService *usersvc.Service) *Handler {
    	return &Handler{userService: userService}
    }
    
    func (h Handler) Handle(ctx context.Context, req *Request) (*Response, error) {
    	user, err := h.userService.UserByID(ctx, req.UserID)
    	if err != nil {
    		return nil, err
    	}
    
    	return &Response{
    		Name: user.Name,
    		Age:  user.Age,
    	}, nil
    }



    Через dependency injection даем хэндлеру все источники данных, изолируем хэндлер в отдельном пакете, четко прописываем ему типы Request и Response, пишем валидацию для запроса. Открыв хэндлер, разработчик сразу видит его контракты, весь бойлерплейт во враппере, остается написать только логику.

    В итоге использование такого хэндлера+враппер выглядит так:
    getUserHandler := getuser.New(userSvc)
    
    router := httprouter.New()
    
    router.POST("/api/get_user", wrapper.Wrap(productsHandler.getUserHandler))


    Причем, тут даже не видно, что Wrap() имеет дженерик-параметры, потому что гошка самостоятельно выводит эти параметры из типов Request и Response из хэндлера и проверяет, что у Request есть метод Validate.

    Подобные дженерик-врапперы применял в нескольких разных задачах. Кроме обработки запросов еще была система для запуска фоновых джоб по обработке данных, нужно было положить джобы в один слайс, но при этом, чтобы контракты джоб были типизированы. Поэтому враппером приводил конкретные типы структур из джоб к более универсальному виду с типами в interface{}. Получилось, что и типы все на компиляции проверяются и могу положить все джобы в одну коллекцию.
    Ответ написан
    Комментировать
  • Почему присвоение значения переменной не считается использованием переменной?

    Потому что вы должны хотя бы раз прочитать значение переменной. Во всех случаях, которые вы приводите, где ошибки нет — значение переменной где-то читается.
    Это сделано, чтобы исключить ряд багов, которые может допустить программист (например, зашедоуить переменную из скоупа выше и присвоить значение во временную переменную вместо оригинальной переменной).
    Ответ написан
    3 комментария
  • Почему так работают интерфесы в Го?

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

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

    Кстати, не рекомендую называть интерфейсы ISomething, это не принято в Го.
    Ответ написан
    Комментировать
  • Как исправить ошибку "cannot download, $GOPATH must not be set to $GOROOT"?

    1. Убедиться, что го установлен не в ~/go и что $GOPATH не смотрит на место его установки
    2. Убедиться, что код проекта находится не в ~/go или вложенных в него папках и не в $GOPATH и вложенных в него папках.
    3. Убедиться, что зависимости в проекте управляются через модули (что сделан go mod init)
    Ответ написан
    Комментировать
  • Golang нужно делать реконнект к дб или поднимать новое соединение?

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

    https://go.dev/doc/database/manage-connections
    Ответ написан
    2 комментария
  • Можно ли в структуре указать тип данных отличный от того что лежит в базе данных?

    Хорошей практикой является принимать данные из БД в структуру, которая соответствует по типам тому, что лежит в БД.

    Если вам нужно конвертировать потом эти данные, конвертируйте их в другую структуру, которая расположена на слое бизнес-логики, например.
    Ответ написан
    2 комментария
  • Как под капотом реализованы интерфейсы в go?

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

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

    Для таких случаев в пакете errors есть методы Is и As

    Is проверяет, что указанная ошибка соответствует возвращенной (включая возможность вложенности ошибок)

    As может заполнять структуру ошибки при соответствии типа (тоже включая возможную вложенность)

    https://go.dev/play/p/3j9O079sj97

    Развернуть код
    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    func main() {
    	err := someFunc()
    	{
    		keyExistsErr := &KeyExistError{}
    		if errors.As(err, keyExistsErr) {
    			fmt.Println("Мы получили ttl:", keyExistsErr.ttl)
    		}
    	}
    }
    
    type KeyExistError struct {
    	ttl string
    }
    
    func (e KeyExistError) Error() string {
    	return fmt.Sprint("Отправить код нельзя раньше чем через ", e.ttl, " сек.")
    }
    
    func someFunc() error {
    	return KeyExistError{
    		ttl: "4",
    	}
    }


    Причем, оно будет работать даже если обернуть ошибку через %w или через errors.Wrap, итд...
    https://go.dev/play/p/x2b_AZKI43t

    Развернуть код

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    func main() {
    	err := someFunc()
    	{
    		keyExistsErr := &KeyExistError{}
    		if errors.As(err, keyExistsErr) {
    			fmt.Println("Мы получили ttl:", keyExistsErr.ttl)
    		}
    	}
    }
    
    type KeyExistError struct {
    	ttl string
    }
    
    func (e KeyExistError) Error() string {
    	return fmt.Sprint("Отправить код нельзя раньше чем через ", e.ttl, " сек.")
    }
    
    func someFunc() error {
    	err := someOtherFunc()
    	if err != nil {
    		return fmt.Errorf("error calling someOtherFunc: %w", err)
    	}
    	return nil
    }
    
    func someOtherFunc() error {
    	return KeyExistError{
    		ttl: "4",
    	}
    }

    Ответ написан
  • Как найти утечку памяти?

    В го автоматическое управление памятью, поэтому утечек памяти в классическом смысле быть не может (если не использовать пакет unsafe). Но могут быть утечки горутин (когда вы запускаете горутины, но они не завершаются, а блокируются на каком-то io или мьютексе/канале/...)

    Такое легко ищется с помощью pprof. Добавьте в свою программу веб-интерфейс pprof-а

    Для этого запустите любой http-сервер из стандартной библиотеки и добавьте импорт import _ "net/http/pprof"

    Например так:
    import (
      "net/http"
      _ "net/http/pprof"
    )
    
    ...
    
    func main() {
      ...
      http.ListenAndServe("localhost:8080", nil)
    }


    После этого при запуске программы у вас должен открываться веб-интерфейс pprof-а по адресу 127.0.0.1:8080/debug/pprof

    Дождитесь, когда накопятся утечки и откройте страницу 127.0.0.1:8080/debug/pprof/goroutine?debug=1

    На ней будет список всех работающих горутин и их количество. Найдите группу с самым большим количеством, она и утекает. По стеку посмотрите, где горутина блокируется, тогда поймете, почему они накапливаются.
    Ответ написан
    3 комментария
  • Предложения по оптимизации названий пакетов в Golang?

    Если используете глобальные пакеты (не советую, кстати, лучше dependency injection использовать), то зачем вам там второй уровень вложенности?
    Выводите функции в этих пакетах в корень и тогда будет:
    logger.Setup("log.log")
    database.Connect()
    settings.Setup("options.json")
    Ответ написан
    2 комментария
  • Как решить проблему X does not implement Y при работе с интерфесами?

    По-хорошему, у вас репозиторий не должен никогда возвращать интерфейс. В Го принято возвращать конкретный тип, а не интерфейс. https://github.com/golang/go/wiki/CodeReviewCommen...

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

    Если сказать по-другому: в domain-пакетах у вас не должно быть импортов каких-то структур из пакета БД, только наоборот — из пакета БД импортить типы из domain.
    Ответ написан
    Комментировать
  • Как установить пакет в Golang?

    Инструкция у пакета супер-устаревшая. В Го уже давно стандартом считается использование модулей.
    Проекты теперь нельзя класть в GOPATH, нужно использовать другие папки.
    Перед началом работы над проектом вам нужно инициализировать модули через go mod init имя_проекта, именем обычно бывает путь к репозиторию проекта, например: go mod init github.com/myuser/someproject

    После этого можно устанавливать нужные вам зависимости через go get github.com/go-sql-driver/mysql@latest. Вместо latest можно указывать необходимую вам версию пакета.
    Ответ написан
    Комментировать
  • Как лучше всего замапить запрос в структуру?

    Для слоя базы данных лучше всего иметь отдельные структуры, из которых потом данные перекладывать в слой бизнес-логики. Причем, лучше всего чтобы перекладыванием занимался пакет, ответственный за базу данных.
    Для слоя АПИ тоже лучше свои отдельные структуры, в них перекладывать должен слой АПИ.
    Слой бизнес-логики (модель) должен быть чистым от любых транспортных имплементаций и не импортить никакие пакеты базы или АПИ. Это наоборот, база и АПИ должны импортить в себя структуры бизнес-слоя, чтобы их возвращать и принимать.
    Короче, это я кратко про букву D в слове SOLID.
    Ответ написан
    2 комментария
  • Как добавить к концу строки \r?

    Так как либа для записи требует интерфейс io.Writer, вы легко можете создать свой прокси-райтер, который будет при встрече каждого \n добавлять еще и \r

    В самой примитивной реализации это будет выглядеть так:
    package main
    
    import (
    	"bytes"
    	"io"
    	"os"
    
    	"github.com/olekukonko/tablewriter"
    )
    
    func main() {
    	data := [][]string{
    		{"1/1/2014", "Domain name", "2233", "$10.98"},
    		{"1/1/2014", "January Hosting", "2233", "$54.95"},
    		{"1/4/2014", "February Hosting", "2233", "$51.00"},
    		{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
    	}
    
    	w := New(os.Stdout)
    
    	table := tablewriter.NewWriter(w)
    	table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
    	table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer
    	table.SetBorder(false)                                // Set Border to false
    	table.AppendBulk(data)                                // Add Bulk Data
    	table.Render()
    }
    
    type ClrfAdder struct {
    	writer io.Writer
    }
    
    func New(w io.Writer) *ClrfAdder {
    	return &ClrfAdder{
    		writer: w,
    	}
    }
    
    func (c ClrfAdder) Write(p []byte) (n int, err error) {
    	replaced := bytes.ReplaceAll(p, []byte("\n"), []byte("\r\n"))
    	n, err = c.writer.Write(replaced)
    	if err != nil {
    		if n > len(p) {
    			n = len(p)
    		}
    		return n, err
    	}
    	return len(p), nil
    }


    Но можно использовать готовые стрим-реплейсеры, например https://github.com/icholy/replace
    Ответ написан
  • Как сделать болле корректно даный цикл?

    Вполне нормальное у вас решение. Разве что можно сделать так:
    func divide(x ...float64) float64 {
      if len(x) == 0 {
        return 0
      }
      if len(x) == 1 {
        return x[0]
      }
      a := x[0]
      for _, v := range x[1:] {
        a /= v
      }
      return a
    }
    Ответ написан
    1 комментарий
  • Можно ли изменять неимпортируемые поля структуры в других пакетах программы?

    Не понял, в чем проблема. Есть у вас пакет cache, внутри тип Cache и функция New

    Так как функция New экспортируемая, вызываете ее из любого пакета и создаете себе экземпляр кэша. Потом этот экземпляр через dependency injection кладете во все нужные вам экземпляры сервисов.
    Ответ написан
    Комментировать
  • Какую технологию выбрать для live трансляции go?

    Проще всего взять за основу ffmpeg, он очень гибкий есть куча примеров, как на его основе делать трансляции через протокол HLS.

    Go тут по сути будет использоваться как оркестратор для запуска ffmpeg и предоставления доступа к hls-кускам видео и плейлистам.

    В свое время ковырял эту связку, в целом так программу для трансляций можно собрать за день.
    Ответ написан
    2 комментария
  • Отправка загруженной фото в боте -> пользователю?

    Есть минимум два способа
    1. Выгрузить фото в файл или s3 и дать на него урл
    2. Выгрузить фото в качестве сырых данных в файл или в память и отправить эти данные другому пользователю через sendPhoto


    Если правильно помню, просто переслать файл-айди не получится, потому что у Телеги файл-айди разный для разных пользователей и ботов.
    Ответ написан
    Комментировать
  • Как использовать GOOS в exec.Command?

    Способ из инета костыльный, по-хорошему нужно делать так:

    cmd := exec.Command("go", "build", ".")
    cmd.Env = os.Environ()
    cmd.Env = append(cmd.Env, "GOOS=windows")
    Ответ написан
    3 комментария