Проблемы, возникающие при написании кода Go

Go

Когда программисты вступают в «старость», это также влияет на ритм написания кода. Раньше это было долгое и непрерывное написание, но сейчас ритм написания кода стал «волнообразным»: то есть пишешь какое-то время, отдыхаешь какое-то время. Конечно, «перерыв» здесь на самом деле не перерыв, а другие действия, такие как просмотр, сортировка и обобщение.

обычно пишутGoПрограммируйте, время от времени сталкивайтесь с некоторыми проблемами или напишите какой-нибудь код, который меня полностью удовлетворит, все это перечислено здесь как «проблемы». Эти «проблемы» (и их решения) часто относительно «небольшие» и «фрагментарные», и не подходят для написания и распространения в «длинном» стиле, в котором они «хороши», и я не знаю, в каком виде. "тем" для обмена. Хорошо, но такие "проблемы" всегда возникают ежедневно. После рассмотрения более целесообразно использовать серию статей для обмена, то есть время от времени, после накопления некоторых проблем, писать статью для обмена.

Это первый, и я буду выкладывать новый в неопределенное время (примечание: это не еженедельно) до тех пор, пока не останется нечего или не о чем писать.Перейти код^0^.

1. Перейти к управлению пакетами

Принять на себя основную тяжестьПерейти к управлению пакетами.

1. Проблемы, вызванные «заразностью» вендора

Идти отВерсия 1.5начать импортмеханизм поставщикаЧтобы помочь в управлении пакетами Go. вместе сvendorПрименение механизма становится все более и более обширным, и мы обнаружим, что иногда, если вы не используете поставщика (без использования сторонних инструментов управления пакетами), многие проблемы компиляции не могут быть решены илиВендорский механизм заразителен. Например следующий пример:

img{512x368}

Как показано на рисунке выше: пакет app_c напрямую вызывает функции из пакета lib_a и использует типы из пакета lib_b (версия v0.2) и пакета lib_b поставщика пакета lib_a (версия v0.1). В таком случае, когда мы скомпилируем пакет app_c, не будет ли проблем? Давайте посмотрим на этот пример:

在$GOPATH/src路径下面我们查看当前示例的目录结构:

$tree
├── app_c
    ├── c.go
├── lib_a
    ├── a.go
    └── vendor
        └── lib_b
            └── b.go
├── lib_b
    ├── b.go

Пример кода для каждого исходного файла выглядит следующим образом:

//lib_a/a.go
package lib_a

import "lib_b"

func Foo(b lib_b.B) {
    b.Do()
}

//lib_a/vendor/lib_b/b.go

package lib_b

import "fmt"

type B struct {
}

func (*B) Do() {
    fmt.Println("lib_b version:v0.1")
}

// lib_b/b.go
package lib_b

import "fmt"

type B struct {
}

func (*B) Do() {
    fmt.Println("lib_b version:v0.2")
}

// app_c/c.go
package app_c

import (
    "lib_a"
    "lib_b"
)

func main() {
    var b lib_b.B
    lib_a.Foo(b)
}

Войдите в каталог app_c и выполните команду компиляции:

$go build c.go
# command-line-arguments
./c.go:10:11: cannot use b (type "lib_b".B) as type "lib_a/vendor/lib_b".B in argument to lib_a.Foo

Мы видим, что компилятор go думает:Тип переменной b, определенной в функции main пакета app_c (lib_b.B), и тип параметра b пакета lib_a.Foo (lib_a/vendor/lib_b.B) являются разными типами и не могут быть присвоены друг другу..

2. Решите вышеуказанные проблемы вручную поставщиком

Этот пример очень показателен, так как же решить эту проблему?Нам нужно использовать механизм поставщика также в app_c, то есть и lib_a, и lib_b, необходимые для app_c, поставляются в app_c.

按照上述思路解决后的示例的目录结构:

$tree
├── app_c
    ├── c.go
    └── vendor
        ├── lib_a
        │   └── a.go
        └── lib_b
            └── b.go
├── lib_a
    ├── a.go
    └── vendor
        └── lib_b
            └── b.go
├── lib_b
    ├── b.go

Однако следует отметить, что каталог vendor в библиотеке app_c/vendor необходимо удалить, и мы сохраняем только поставщика верхнего уровня. Теперь давайте снова скомпилируем c.go, и он может быть успешно скомпилирован.

3. Используйте деп

Для демонстраций или небольших проектов с малым масштабом и небольшим количеством зависимостей ручная поставка весьма эффективна. Рабочий шаг поставщика вручную:

  • Создать поставщика на верхнем уровне проекта;
  • Просмотр зависимостей проекта "deps" с помощью go list -json ./…;
  • Загрузите каждую зависимость одну за другой и определите используемую версию (тег или ветвь), скопируйте конкретную версию в каталог поставщика верхнего уровня и, по крайней мере, выполните все прямые зависимости поставщика;
  • Вы можете создать файл dependencies.list для поставщика верхнего уровня и вручную записать список пакетов зависимостей поставщика и информацию о версии.

Но для проекта чуть большего размера ручной вендоринг будет длительным и трудозатратным, и иногда можно рассматривать только вендоров, которые «напрямую зависят от пакетов», а «бесчисленные» непрямые/транзитивные зависимости доставят вам массу головной боли . В настоящее время мы подумаем об использовании стороннего инструмента управления пакетами. В этот момент, если ты снова поговоришь со мнойgodep,glideПодожди, тогда ты вышел,depявляется предпочтительным.

существует"Первый взгляд на деп"В этой статье мы провели более подробный анализ механизма работы dep на тот момент, сейчас dep эволюционировал до версии 0.3.2, стабилизирован интерактивный интерфейс командной строки. dep init по умолчанию использует сетевой режим, то есть ищет информацию о версии в восходящем потоке каждого зависимого пакета и загружает ее; dep init также поддерживает режим -gopath, то есть получает и анализирует метаинформацию зависимых пакетов в локальном $ ГОПАТ.

Однако для сусликов в Китае процесс dep init по-прежнему является «препятствием», которое трудно преодолеть. Проблема в основном в том, что сторонним пакетам особенно нравятся те пакеты на golang.org/x, от которых они зависят.Обычные пакеты: net, text, crypto и т. д. golang.org/x/{package_name} простоcanonical import path, настоящий код хранится на сайте go.googlesource.com, и когда мы получим эти пакеты в Китае, то получим следующие ошибки:

$go get -u golang.org/x/net
package golang.org/x/net: unrecognized import path "golang.org/x/net" (https fetch: Get https://golang.org/x/net?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

Это приведет к тому, что команда dep init будет заблокирована на долгое время и принесет крайне неприятный опыт домашнему суслику. Что еще хуже, даже если некоторые методы фан цян используются, иногда go.googlesource.com все еще не может быть подключен. Поэтому моя общая практика заключается в том, чтобы выполнить dep init на внешнем хосте, а затем проверить поставщика в репозиторий кода. Таким образом, после того, как другие люди получат ваш код, они смогут получить воспроизводимую сборку без обязательной проверки (и загрузить пакеты зависимостей).

Некоторые друзья могут заменить golang.org/x/net пакетом net, загруженным с github.com/golang, и использовать режим dep init -v -gopath=true. Но эта замена будет проанализирована dep, потому что dep попытается прочитать метаинформацию кодовой базы, и результат все равно будет неудачным.

2. Локальное управление журналами для неконтейнерных приложений

существуетМикросервисы, сегодня популярна контейнеризация, обработка логов отдельного приложения стала проще, а приложению нужно только выводить информацию для вывода на stdout и stderr.инфраструктура лесозаготовокЖурналы контейнеров будут собираться и обрабатываться для последующего хранения, анализа, фильтрации, поиска и отображения. Однако в неконтейнерной среде и без единой инфраструктуры журналов управление журналами возвращается к самому приложению. Простое управление журналами должно как минимум включать ротацию журналов (ротацию), сжатые архивы и обработку файлов исторических архивов. Здесь мы обсудим несколько решений этой проблемы.

1. Хост для логротации

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

Мы создаем демонстрационное приложение, которое выводит логи:

//testlogrotate.go

package main

import (
    "log"
    "os"
    "time"
)

func main() {
    file, err := os.OpenFile("./app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalln("Failed to open log file:", err)
    }
    defer file.Close()

    logger := log.New(file,
        "APP_LOG_PREFIX: ",
        log.Ldate|log.Ltime|log.Lshortfile)

    for {
        logger.Println("test log")
        time.Sleep(time.Second * 1)
    }
}

Программа записывает строку логов в файл app.log каждую 1 с.

# tail -f app.log
APP_LOG_PREFIX: 2018/01/12 19:14:43 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:44 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:45 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:46 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:47 testlogrotate.go:22: test log
... ..

Далее мы будем использовать logrotate для периодической ротации, сжатия и архивирования файла app.log и очистки исторических архивов.Нам нужно настроить конфигурацию для app.log. Каталог, в котором logrotate читает конфигурацию, — это /etc/logrotate.d, и мы создаем файл applog в каталоге /etc/logrotate.d (конечно, вы также можете создавать файлы конфигурации в любом другом каталоге, но файлы конфигурации в других каталогах не может использоваться программой logrotate. Об этом знает задача cron, но такой конфигурационный файл можно использовать вручную в связке с программой logrotate), содержимое файла следующее:

# cat /etc/logrotate.d/applog

/data/tonybai/test/go/app.log {
  rotate 7
  daily
  size=10M
  compress
  dateext
  missingok
  copytruncate
}

Общий смысл этой конфигурации:
* ротация один раз в день
* Логи хранятся 7 дней (ротация=7, ежедневная ротация)
* Архивные журналы находятся в сжатом виде
* Архивные журналы с отметкой времени
* Когда текущий размер журнала > 10M, будет выполнена ротация
* Самое главное это конфигурация copytruncate, смысл этой конфигурации в том, чтобы обрезать app.log после копирования текущего лога app.log в архивный файл, чтобы открытый файл fd app.log не менялся и не повлияет на переход к исходному приложению для продолжения записи журналов. Конечно, в процессе копирования может быть потерян небольшой объем журнала.

Если вы считаете, что logrotate по-прежнему не может удовлетворить ваши требования с точки зрения детализации времени и точности, вы можете регулярно запускать logrotate в сочетании с crontab (crontab -e для редактирования конфигурации crontab):

# logrotate -f /etc/logrotate.d/applog

Вот что вы видите в tail -f при вращении:

APP_LOG_PREFIX: 2018/01/12 20:25:59 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:00 testlogrotate.go:21: test log
tail: app.log: file truncated
APP_LOG_PREFIX: 2018/01/12 20:26:01 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:02 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:03 testlogrotate.go:21: test log

Вы можете видеть, что хвост может обнаружить событие усечения файла.

2. Используйте пакет журнала с собственной функцией поворота

в стекеМногочисленные пакеты ведения журналасередина,logrusЭто широко используемый пакет, который поддерживает структурированные журналы, совместимые с API журналов стандартной библиотеки, поддерживает настройки уровня ведения журнала, поддерживает безопасную параллельную запись журнала и перехватчики и т. д. Однако сам logrus не имеет функции автоповорота, и для этого его нужно комбинировать с другими инструментами. использовать здесьnate finchизlumberjack, давайте рассмотрим простой пример:

// testlogrusAndlumberjack.go

package main

import (
    "time"

    "github.com/natefinch/lumberjack"
    log "github.com/sirupsen/logrus"
)

func main() {
    logger := log.New()
    logger.SetLevel(log.DebugLevel)
    logger.Formatter = &log.JSONFormatter{}

    logger.Out = &lumberjack.Logger{
        Filename:   "./app.log",
        MaxSize:    1, // megabytes
        MaxBackups: 3,
        MaxAge:     1,    //days
        Compress:   true, // disabled by default
        LocalTime:  true,
    }

    for {
        logger.Debug("this is an app log")
        time.Sleep(2 * time.Millisecond)
    }
}

Из кода видно, что: установив logger.Out как экземпляр lumberjack.Logger, реальная запись передается lumberjack.Logger, а последний реализует функцию ротации журнала, которая чем-то похожа на конфигурацию logrotate, включая настройку максимального размера журнала, количество архивных журналов для хранения, необходимость сжатия и срок хранения журналов не более нескольких дней. Однако условие оценки вращения, реализованное дровосеком, в настоящее время имеет только одно условие: MaxSize, и нет функции синхронизации вращения.

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

$ls -lh
-rw-r--r--  1 tony  staff   3.7K Jan 12 21:03 app-2018-01-12T21-03-42.844.log.gz
-rw-r--r--  1 tony  staff   3.7K Jan 12 21:04 app-2018-01-12T21-04-15.017.log.gz
-rw-r--r--  1 tony  staff   457K Jan 12 21:04 app.log

Каждый раз, когда лесоруб обнаруживает, что файл app.log больше MaxSize, он будет сменяться один раз.Здесь уже есть два заархивированных сжатых файла, и лесоруб присваивает им временные метки и серийные номера для облегчения поиска и просмотра.

3. О поддержке лог-уровня и горячем обновлении лог-уровня

Поддержка уровней ведения журнала является важным справочным элементом для параметров пакета ведения журнала. logrus поддерживает настройку шести уровней ведения журнала:

    PanicLevel
    FatalLevel
    ErrorLevel
    WarnLevel
    InfoLevel
    DebugLevel

А для логов разного уровня логрус поддерживает установку хуков для их отдельной обработки, например: помещение в разные лог-файлы. С помощью метода logrus.Logger.SetLevel уровень журнала экземпляра регистратора может быть обновлен во время выполнения.Эта функция позволяет нам более подробно наблюдать за программой, временно открывая журнал уровня отладки в производственной среде, чтобы обнаруживать проблемы и быстро находить ошибки, что очень практично.

комбинироватьСистемный сигналМеханизм, мы можем настроить уровень журнала программы во время выполнения через сигналы USR1 и USR2.Давайте рассмотрим пример:

img{512x368}

Как видно из приведенного выше рисунка, уровни журнала от высокого к низкому: Panic, Fatal, Error, Warn, Info и Debug. Если мы хотим повысить уровень лога, мы отправляем в программу USR1 для увеличения уровня лога, и наоборот, отправляем USR2 для уменьшения уровня лога:

Вносим некоторые изменения в testlogrusAndlumberjack.go: увеличиваем мониторинг сигнала: USR1 и USR2, и печатаем логи разных уровней в цикле, чтобы потом проверить динамическую настройку уровня лога:

// testloglevelupdate.go

import (
    log "github.com/sirupsen/logrus"
    ... ...
)

func main() {
    logger := log.New()
    logger.SetLevel(log.DebugLevel)
    logger.Formatter = &log.JSONFormatter{}

    logger.Out = &lumberjack.Logger{
        Filename:   "./app.log",
        MaxSize:    1, // megabytes
        MaxBackups: 3,
        MaxAge:     1,    //days
        Compress:   true, // disabled by default
        LocalTime:  true,
    }

    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2)
    go watchAndUpdateLoglevel(c, logger)

    for {
        logger.Debug("it is debug level log")
        logger.Info("it is info level log")
        logger.Warn("it is warning level log")
        logger.Error("it is warning level log")
        time.Sleep(5 * time.Second)
    }
}

Функция watchAndUpdateLoglevel используется для мониторинга системных сигналов, полученных программой, и настройки уровня логирования в соответствии с типом сигнала:

// testloglevelupdate.go
func watchAndUpdateLoglevel(c chan os.Signal, logger *log.Logger) {
    for {
        select {
        case sig := <-c:
            if sig == syscall.SIGUSR1 {
                level := logger.Level
                if level == log.PanicLevel {
                    fmt.Println("Raise log level: It has been already the most top log level: panic level")
                } else {
                    logger.SetLevel(level - 1)
                    fmt.Println("Raise log level: the current level is", logger.Level)
                }

            } else if sig == syscall.SIGUSR2 {
                level := logger.Level
                if level == log.DebugLevel {
                    fmt.Println("Reduce log level: It has been already the lowest log level: debug level")
                } else {
                    logger.SetLevel(level + 1)
                    fmt.Println("Reduce log level: the current level is", logger.Level)
                }

            } else {
                fmt.Println("receive unknown signal:", sig)
            }
        }
    }
}

После запуска программы вы можете отправлять сигналы программе следующими командами:

$ kill -s USR1|USR2 程序的进程号

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

Однако здесь следует упомянуть, что logrus в настоящее время экранирует некоторые символы в двойных кавычках в журнале вывода (например, сами двойные кавычки), то есть добавляет «обратную косую черту» впереди, например:

{"level":"debug","msg":"receive a msg: {\"id\":\"000002\",\"ip\":\"201.108.111.117\"}","time":"2018-01-11T20:42:31+08:00"}

Эта проблема значительно снижает читабельность журнала, но проблема кажется нерешенной.

3. Проблема побега, когда json маршалирует строку json

Я написал такую ​​функцию раньше, чтобы унифицировать ответное сообщение внутреннего компонента связи маршала:

func marshalResponse(code int, msg string, result interface{}) (string, error) {
    m := map[string]interface{}{
        "code":   0,
        "msg":    "ok",
        "result": result,
    }

    b, err := json.Marshal(&m)
    if err != nil {
        return "", err
    }

    return string(b), nil
}

Однако, когда тип результата — строка json, выходные данные этой функции экранированы обратной косой чертой:

//testmarshaljsonstring.go
... ...
func main() {
    s, err := marshalResponse(0, "ok", `{"name": "tony", "city": "shenyang"}`)
    if err != nil {
        fmt.Println("marshal response error:", err)
        return
    }
    fmt.Println(s)
}

Запуск этой программы выводит:

{"code":0,"msg":"ok","result":"{\"name\": \"tony\", \"city\": \"shenyang\"}"}

Как решить эту проблему? JSON предоставляет тип RawMessage, который по сути является []byte. Мы можем решить эту проблему, преобразовав строку json в RawMessage, а затем передав ее в json.Marshal:

//testmarshaljsonstring.go
func marshalResponse1(code int, msg string, result interface{}) (string, error) {
    s, ok := result.(string)
    var m = map[string]interface{}{
        "code": 0,
        "msg":  "ok",
    }

    if ok {
        rawData := json.RawMessage(s)
        m["result"] = rawData
    } else {
        m["result"] = result
    }

    b, err := json.Marshal(&m)
    if err != nil {
        return "", err
    }

    return string(b), nil
}

func main() {
    s, err = marshalResponse1(0, "ok", `{"name": "tony", "city": "shenyang"}`)
    if err != nil {
        fmt.Println("marshal response1 error:", err)
        return
    }
    fmt.Println(s)
}

Результат запуска этой программы снова становится желаемым результатом:

{"code":0,"msg":"ok","result":{"name":"tony","city":"shenyang"}}

4. Как использовать переменную флага командной строки после flag.Parse вне основного пакета

Когда мы используем Go для разработки приложения командной строки с менее сложным интерактивным интерфейсом, мы обычно используем пакет flag в std для разбора флага командной строки, а также проверяем и используем переменную flag после flag.Parse в основном пакете. Обычный распорядок таков:

//testflag1.go
package main

import (
    "flag"
    "fmt"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

func usage() {
    fmt.Println("flagdemo-app is a daemon application which provides xxx service.\n")
    fmt.Println("Usage of flagdemo-app:\n")
    fmt.Println("\t flagdemo-app [options]\n")
    fmt.Println("The options are:\n")

    flag.PrintDefaults()
}

func main() {
    flag.Usage = usage
    flag.Parse()

   // ... ...
   // 这里我们可以使用endpoints、user、password等flag变量了
}

В такой подпрограмме мы можем напрямую использовать переменную флага после flag.Parse в основном пакете. Но иногда нам нужно использовать эти переменные флагов вне основного пакета (например, здесь: конечные точки, пользователь, пароль), как это сделать, есть несколько способов, давайте рассмотрим их один за другим.

1. Метод глобальных переменных

Думаю, первая идея большинства сусликов — использовать глобальные переменные, то есть создать пакет конфига, определить глобальные переменные в пакете и привязать эти глобальные переменные к Parse of flag в main:

$tree globalvars
globalvars
├── config
│   └── config.go
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/globalvars/config/config.go

package config

var (
    Endpoints string
    User      string
    Password  string
)

// flag-demo/globalvars/etcd/etcd.go
package etcd

import (
    "fmt"

    "../config"
)

func EtcdProxy() {
    fmt.Println(config.Endpoints, config.User, config.Password)
    //... ....
}

// flag-demo/globalvars/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./config"
    "./etcd"
)

func init() {
    flag.StringVar(&config.Endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&config.User, "user", "", "etcdv3 client user")
    flag.StringVar(&config.Password, "password", "", "etcdv3 client password")
}

.... ...

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy()

    time.Sleep(5 * time.Second)
}

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

Запускаем эту программу:

./main -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123
192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123

Однако этот методОбратите внимание на порядок значений этих глобальных переменных в процессе инициализации пакета Go, например: если вы используете эти глобальные переменные в функции инициализации пакета etcd, то значение каждой полученной вами переменной будет нулевым, потому что функция инициализации пакета etcd выполняется до main.init и main.main, а привязка и Parse еще не выполнены.

2. Передача параметров

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

$tree parampass
parampass
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/parampass/etcd/etcd.go
package etcd
... ...

func EtcdProxy(endpoints, user, password string) {
    fmt.Println(endpoints, user, password)
}

// flag-demo/parampass/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./etcd"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

... ...

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy(endpoints, user, password)

    time.Sleep(5 * time.Second)
}

Этот метод очень интуитивно понятен и не будет здесь объясняться. Но обратите внимание: после использования этого метода должны быть установлены некоторые зависимости между основным пакетом и другими пакетами, по крайней мере, основной пакет будет импортировать те пакеты, которые используют переменную флага.

3. Метод центра конфигурации

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

$tree configcenter
configcenter
├── config
│   └── config.go
└── main.go

//flag-demo/configcenter/config/config.go
package config

import (
    "log"
    "sync"
)

var (
    m  map[string]interface{}
    mu sync.RWMutex
)

func init() {
    m = make(map[string]interface{}, 10)
}

func SetString(k, v string) {
    mu.Lock()
    m[k] = v
    mu.Unlock()
}

func SetInt(k string, i int) {
    mu.Lock()
    m[k] = i
    mu.Unlock()
}

func GetString(key string) string {
    mu.RLock()
    defer mu.RUnlock()
    v, ok := m[key]
    if !ok {
        return ""
    }
    return v.(string)
}

func GetInt(key string) int {
    mu.RLock()
    defer mu.RUnlock()
    v, ok := m[key]
    if !ok {
        return 0
    }
    return v.(int)
}

func Dump() {
    log.Println(m)
}

// flag-demo/configcenter/main.go

package main

import (
    "flag"
    "fmt"
    "time"

    "./config"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}
... ...
func main() {
    flag.Usage = usage
    flag.Parse()

    // inject flag vars to config center
    config.SetString("endpoints", endpoints)
    config.SetString("user", user)
    config.SetString("password", password)

    time.Sleep(5 * time.Second)
}

Мы используем SetString конфигурации в main, чтобы вставить переменные флага в центр конфигурации. После этого мы можем использовать его в других пакетах: GetString и GetInt для получения значений этих переменных, поэтому здесь я не буду приводить пример.

4. «Черная магия»: flag.Lookup

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

$tree flaglookup
flaglookup
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/flaglookup/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./etcd"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

......

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy()

    time.Sleep(5 * time.Second)
}

// flag-demo/flaglookup/etcd/etcd.go
package etcd

import (
    "flag"
    "fmt"
)

func EtcdProxy() {
    endpoints := flag.Lookup("endpoints").Value.(flag.Getter).Get().(string)
    user := flag.Lookup("user").Value.(flag.Getter).Get().(string)
    password := flag.Lookup("password").Value.(flag.Getter).Get().(string)

    fmt.Println(endpoints, user, password)
}

Запустите программу:

$go run main.go -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123
192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123

Результат соответствует нашим ожиданиям.

5. Сравнение

Мы используем картинку для сравнения вышеперечисленных методов:

img{512x368}

Очевидно, что после простой упаковки поиск флагов по «черной магии» должен стать отличным решением. Основной пакет и другие пакеты должны импортировать только флаг.

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

V. Резюме

Выше приведены некоторые проблемы и решения Go, которые возникли и собраны за это время. Уведомление:Эти решения не обязательно являются лучшими решениями! Если у вас есть лучшее решение, приветствуем критику, исправления и интерактивный обмен мнениями..

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


Вейбо:@tonybai_cn
Публичный аккаунт WeChat: iamtonybai
github.com: https://github.com/bigwhite

WeChat ценит:
img{512x368}

© 2018, bigwhite. все права защищены.

Related posts:

  1. первый взгляд
  2. Понимание поставщиков Go 1.5
  3. godep поддерживает поставщика Go 1.5
  4. Различия между импортом пакетов Go и Java
  5. Сводная схема конфигурации программы Golang