Вкратце об интерфейсах
Интерфейсы в Golang похожи на электрические розетки со штепсельными вилками и требуют соответствующего с ними обращения (см. код дальше в статье).
Скучные подробности
Я категорически против такого подхода, когда создание интерфейсов происходит без веской на то причины. Ведь это сопряжено с расходом вычислительных ресурсов и возможным запутыванием кода, а мне очень не нравится скрытый код (особенно когда при вызове случается какой-то сбой и приходится распутывать этот код-спагетти). Создавать интерфейс просто потому, что есть такая возможность, еще не означает, что это нужно делать.
Простая аналогия интерфейса Golang — электрическая розетка. Описываемая далее реализация этого интерфейса — как вилка, которая вставляется в розетку. Этот электрический интерфейс принимает любое устройство с подходящей вилкой, которое соответствует требованиям по напряжению и силе тока. Захотели использовать другое устройство — вынимаем из розетки и подключаем новое. Все просто.
Например, у многих есть кофеварка. Когда это устройство подключается с использованием жесткой проводниковой системы или тем более когда кофеварка становится частью стены, шкафа, электрических и сантехнических систем, замена такого устройства весьма времязатратна. Но стоит только задействовать интерфейсы (в случае со стеной и шкафом это опорные или независимые крепления, применительно к водопроводной системе это быстроразборные фитинговые соединения, а в случае с электрикой — соответствующий штекер), и тогда убрать или заменить кофеварку будет не проблема. Просто используем те же интерфейсы с новой кофеваркой, подключаем все и наслаждаемся кофе.
Интерфейсы в Golang облегчают жизнь программиста благодаря технологии plug-and-play («включил и играй (работай)»). Я считаю вескими только три причины создания интерфейса. Это когда нужно:
- Получить доступ к каким-то методам, не переписывая много кода.
- Сделать в модульных тестах имитацию баз данных или внешних API.
- Быстро подключить в код и отключить из него различные пакеты (например, логгер) без полной перезаписи логики.
Когда причины какие-то другие, интерфейс создавать не нужно.
Но это только мои правила, и еще не факт, что они подойдут в вашем случае. Правила должны соответствовать целям, а интерфейсы не должны использоваться без веских на то оснований. Соль хороша в малых количествах. А слишком много соли уже вредно. То же относится и к интерфейсам.
Код
Вот простой интерфейс средства ведения журнала:
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
Весь код находится здесь.
Хороших вам интерфейсов!🙂
Читайте также:
- Обработка ошибок в Golang с помощью Panic, Defer и Recover
- Конкурентность и параллелизм в Golang. Горутины.
- Нормальное завершение работы в Go
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Brian Kovacs: Golang Interfaces Simplified