Простое объяснение интерфейсов на Golang

Вкратце об интерфейсах

Интерфейсы в Golang похожи на электрические розетки со штепсельными вилками и требуют соответствующего с ними обращения (см. код дальше в статье).

Скучные подробности

Я категорически против такого подхода, когда создание интерфейсов происходит без веской на то причины. Ведь это сопряжено с расходом вычислительных ресурсов и возможным запутыванием кода, а мне очень не нравится скрытый код (особенно когда при вызове случается какой-то сбой и приходится распутывать этот код-спагетти). Создавать интерфейс просто потому, что есть такая возможность, еще не означает, что это нужно делать.

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

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

Интерфейсы в Golang облегчают жизнь программиста благодаря технологии plug-and-play («включил и играй (работай)»). Я считаю вескими только три причины создания интерфейса. Это когда нужно:

  1. Получить доступ к каким-то методам, не переписывая много кода.
  2. Сделать в модульных тестах имитацию баз данных или внешних API.
  3. Быстро подключить в код и отключить из него различные пакеты (например, логгер) без полной перезаписи логики.

Когда причины какие-то другие, интерфейс создавать не нужно.

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


Код

Вот простой интерфейс средства ведения журнала:

type Logger interface { 
Print(level string, msg string, data ...map[string]interface{}) error
}

Я подсмотрел код в презентации Криса Хайнса и пакете журналов в gokit, но согласен не со всем, что там изложено  —  чрезмерное упрощение слишком все запутывает. Мне нужен был логгер структурированный (который понятно как использовать), простой (который реализует лишь несколько методов) и при этом позволяющий обрабатывать всю сложность используемого пакета (например, zap, logrus и т. д.) в функции с New и единой функции вызова Print.

Вот подробная реализация добавления логгера zap в этот интерфейс:

package coolog

import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

// ZapLogger — это структура для реализации интерфейса логгера.
type ZapLogger struct {
Logger *zap.Logger
Level string
Atom zap.AtomicLevel
}

// NewZapLogger — это удобная функция для создания ZapLogger.
func NewZapLogger(level string, logLocations []string) (*ZapLogger, error) {
encoderCfg := zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
TimeKey: "@ts",
CallerKey: "caller",
FunctionKey: "func",
StacktraceKey: "stack",
LineEnding: ",\n",
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000000000 MST"),
EncodeDuration: zapcore.NanosDurationEncoder,
EncodeCaller: zapcore.FullCallerEncoder,
}
zapLevel := zap.InfoLevel
switch level {
case "trace", "debug":
zapLevel = zap.DebugLevel
case "info":
zapLevel = zap.InfoLevel
case "warn":
zapLevel = zap.WarnLevel
case "error":
zapLevel = zap.ErrorLevel
}
atom := zap.NewAtomicLevelAt(zapLevel)
cfg := zap.Config{
EncoderConfig: encoderCfg,
Level: atom,
Encoding: "json",
OutputPaths: logLocations,
ErrorOutputPaths: logLocations,
}
logger, err := cfg.Build()
if err != nil {
return nil, err
}
return &ZapLogger{Logger: logger, Atom: atom, Level: level}, nil
}

// Print — это главный метод, реализует интерфейс Logger.
func (z *ZapLogger) Print(level string, msg string, data ...map[string]interface{}) error {
var fields []zap.Field
for _, d := range data {
for k, v := range d {
field := zap.Any(k, v)
fields = append(fields, field)
}
}

switch level {
case "trace", "debug":
z.Logger.Debug(msg, fields...)
case "info":
z.Logger.Info(msg, fields...)
case "warn":
z.Logger.Warn(msg, fields...)
case "error":
z.Logger.Error(msg, fields...)
default:
z.Logger.Error(msg, fields...)
}
return nil
}

// Close — следует примерам в zap, ничто другое лучше
// с этим не справится, так как вызывающему функцию NewZapLogger необходимо отложить z.Close().
func (z *ZapLogger) Close() error {
return z.Logger.Sync()
}

И вот как она используется:

package main

import (
"fmt"

"github.com/cavebeavis/coolog"
)

func main() {
fmt.Println("Starting simple logging example...")

logLocations := []string{"stdout"}
logLevel := "info"

// это реализация логгера «zap», но любой логгер используется
// без необходимости вносить изменения в iNeedsLogger (см. чуть ниже), пока новый логгер
// реализует интерфейс coolog.Logger...
zapLogger, err := coolog.NewZapLogger(logLevel, logLocations)
if err != nil {
fmt.Println("well this is seriously embarrassing, zap:", err)
return
}
defer zapLogger.Close()

iNeedsLogger(zapLogger)

logrusLogger, err := coolog.NewLogrusLogger(logLevel, logLocations[0])
if err != nil {
fmt.Println("well this is seriously embarrassing, logrus:", err)
return
}

iNeedsLogger(logrusLogger)

fmt.Println("Finished")
}

func iNeedsLogger(log coolog.Logger) {
// помимо всего прочего, нужно зарегистрировать
log.Print("info", "I am info, hear me roar!")

// а также данные и поля...
log.Print("error", "I am an error with context data", map[string]interface{}{
"field1": "you really did it now",
"field2": 666,
"field3": []string{"slice", "is", "ok", "too"},
})

// или логи трассировки. Но они не нужны, пока Logger не в режиме трассировки
log.Print("trace", "You will only see me if trace or debug (for zap implementation) is enabled")
}

Ожидаемый результат:

$ go run example/simple/main.go

Starting simple logging example...
{"level":"info","@ts":"2021-07-11 15:56:55.282588058 EDT","caller":"./coolog/zap.go:70","func":"github.com/cavebeavis/coolog.(*ZapLogger).Print","msg":"I am info, hear me roar!"},
{"level":"error","@ts":"2021-07-11 15:56:55.282659964 EDT","caller":"./coolog/zap.go:74","func":"github.com/cavebeavis/coolog.(*ZapLogger).Print","msg":"I am an error with context data","field1":"you really did it now","field2":666,"field3":["slice","is","ok","too"],"stack":"github.com/cavebeavis/coolog.(*ZapLogger).Print\n\t/./coolog/zap.go:74\nmain.iNeedsLogger\n\t/./coolog/example/simple/main.go:35\nmain.main\n\t/./coolog/example/simple/main.go:25\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:225"},
Finished

Весь код находится здесь.

Хороших вам интерфейсов!🙂

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Brian Kovacs: Golang Interfaces Simplified

Предыдущая статьяСайты для генерации верстки HTML/CSS, которые ускоряют разработку адаптивных интерфейсов
Следующая статьяПишем асинхронный неблокирующий Rest API на Java