Полное руководство по Go: написание инструмента Go

Go Язык программирования

Арес Блю.IO/2017/09/14/…

Автор: Фатих Арслан

Переводчик: oopsguy.com

Ранее я написал программу под названиемgomodifytagsинструмент, который значительно облегчает мою работу. Он автоматически заполняет поле метки структуры на основе имени поля. Позвольте мне показать, что он делает:

在 vim-go 中使用 gomodifytags 的一个示例

Использование такого инструмента может быть оченьлегко управлятьНесколько полей структуры. Инструмент также может добавлять и удалять теги, управлять параметрами тегов, такими какomitempty), определить правила преобразования (snake_case,camelCaseи Т. Д. Но как работает инструмент? Какой пакет Go используется внутри? Есть много вопросов, на которые нужно ответить.

Это очень длинная запись в блоге, объясняющая, как написать такой инструмент, и каждую деталь сборки. Он содержит множество уникальных деталей, хитростей и неизвестных знаний Go.

Берите чашечку кофе ☕️ и вперед!


Во-первых, позвольте мне перечислить, что должен делать этот инструмент:

  1. Он должен читать исходные файлы, понимать и уметь анализировать файлы Go.
  2. ему нужно найти соответствующую структуру
  3. После нахождения структуры необходимо получить имена полей
  4. Необходимо обновить тег структуры в соответствии с именем поля (в соответствии с правилами преобразования, такими какsnake_case)
  5. Он должен иметь возможность обновлять эти изменения в файле или иметь возможность выводить измененный результат в готовом виде.

Давайте сначала разберемся, что такоеСтруктура (struct) тег (tag), откуда мы можем узнать обо всем и о том, как это собрать, из чего можно создавать такие инструменты.

Значение тега структуры (контент, такой какjson: "foo")не является частью официальной спецификации,ноreflectПакет определяет стандарт формата для неофициальной спецификации, который в равной степениstdlibПакет (напр.encoding/json) использовал. это проходитreflect.StructTagОпределение типа:

Это определение немного длинное и не очень легкое для понимания. Попробуем разобрать:

  • Тег struct — это строковый литерал (поскольку он имеет тип string).
  • Ключевая часть представляет собойНет цитатыстроковый литерал
  • Ценная частьс кавычкамистроковый литерал
  • Ключи и значения разделяются двоеточием (:). Значение, состоящее из ключа и значения, разделенных двоеточием, называетсяпара ключ-значение
  • Теги структуры могутСодержит несколько пар ключ-значение(по желанию). пара ключ-значениеразделенные пробелами.
  • Часть, которая не определена, является настройкой опции. рисунокencoding/jsonТакие пакеты читаются как список, разделенный запятыми. Содержимое после первой запятой является опциональной частью, напримерfoo,omitempty,string. Оно имеетfooИ значение [omitempty, string] вариант
  • Поскольку теги структуры являются строковыми литералами, они должны быть заключены в двойные кавычки или обратные кавычки. Поскольку значения должны быть заключены в кавычки, мы всегда используем обратные кавычки для всего тега.

В целом:

结构体标签定义有许多隐藏的细节

Теперь, когда мы знаем, что такое тег структуры, мы можем легко изменить его по мере необходимости. Теперь вопрос в том, как нам разобрать его, чтобы мы могли легко его модифицировать? К счастью,reflect.StructTagСодержит метод, который позволяет нам анализировать и возвращать значение указанного ключа. Вот пример:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	tag := reflect.StructTag(`species:"gopher" color:"blue"`)
	fmt.Println(tag.Get("color"), tag.Get("species"))
}

результат:

blue gopher

Возвращает пустую строку, если ключ не существует.

Это очень полезно,ноЕсть также некоторые недостатки, которые делают его не подходящим для нас, потому что нам нужно больше гибкости:

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

Чтобы улучшить это, я написал собственный пакет Go, который решает все проблемы, упомянутые выше, и предоставляет API для простого изменения различных аспектов тегов структуры.

Пакет называетсяstructtag, Доступна сGitHub.com/клапан-тело-и/тело-так…Получать. Этот пакет позволяет нам лаконично анализировать и изменять теги. Вот полный пример, который вы можете скопировать/вставить и попробовать самостоятельно:

package main

import (
	"fmt"

	"github.com/fatih/structtag"
)

func main() {
	tag := `json:"foo,omitempty,string" xml:"foo"`

	// parse the tag
	tags, err := structtag.Parse(string(tag))
	if err != nil {
		panic(err)
	}

	// iterate over all tags
	for _, t := range tags.Tags() {
		fmt.Printf("tag: %+v\n", t)
	}

	// get a single tag
	jsonTag, err := tags.Get("json")
	if err != nil {
		panic(err)
	}

	// change existing tag
	jsonTag.Name = "foo_bar"
	jsonTag.Options = nil
	tags.Set(jsonTag)

	// add new tag
	tags.Set(&structtag.Tag{
		Key:     "hcl",
		Name:    "foo",
		Options: []string{"squash"},
	})

	// print the tags
	fmt.Println(tags) // Output: json:"foo_bar" xml:"foo" hcl:"foo,squash"
}

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

ответчерез АСТ. AST (Abstract Syntax Tree, абстрактное синтаксическое дерево) позволяет извлекать каждый идентификатор (узел) из исходного кода. Ниже вы видите структуру типа AST (упрощенный вариант):

一个基本的 Go ast.Node 表示形式的结构体类型

В этом дереве мы можем извлечь и манипулировать каждым идентификатором каждой строкой, каждой из скобок и тому подобное. Это поузел ASTВыражать. Например, мы можем изменить имя поля сFooизменить наBar. Та же логика применима и к структурным тегам.

хотетьполучить Go AST, нам нужно разобрать исходный файл и преобразовать его в AST. На самом деле, оба обрабатываются через один и тот же шаг.

Для этого мы будем использоватьgo/parserупаковкаРазобратьфайл, чтобы получить AST (весь файл), затем используйтеgo/astpackage для обработки всего дерева (мы могли бы сделать это вручную, но это тема другого сообщения в блоге). Вы можете увидеть полный пример ниже:

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package main
        type Example struct {
	Foo string` + " `json:\"foo\"` }"

	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, "demo", src, parser.ParseComments)
	if err != nil {
		panic(err)
	}

	ast.Inspect(file, func(x ast.Node) bool {
		s, ok := x.(*ast.StructType)
		if !ok {
			return true
		}

		for _, field := range s.Fields.List {
			fmt.Printf("Field: %s\n", field.Names[0].Name)
			fmt.Printf("Tag:   %s\n", field.Tag.Value)
		}
		return false
	})
}

Выходной результат:

Field: Foo
Tag:   `json:"foo"`

Код делает следующее:

  • Мы определяем пример пакета Go с одной структурой
  • Мы используемgo/parserпакет для разбора этой строки.parserПакеты также могут читать файлы (или целые пакеты) с диска.
  • После парсинга мы обрабатывали узлы (назначенные файлу переменных) и искалиast.StructTypeОпределенные узлы AST (см. график AST). пройти черезast.Inspect()Функция завершает обработку дерева. Он перебирает все узлы, пока не получит ложное значение. Это очень удобно, потому что не нужно знать каждый узел.
  • Мы напечатали имена полей структуры и метки структуры.

теперь мы можем сделатьдве важные вещи, во-первых, мы знаемКак разобрать исходный файл Goи получить тег структуры (черезgo/parser).其次,我们知道了如何Метка аналитической структуры Goи изменить по мере необходимости (черезGitHub.com/клапан-тело-и/тело-так…).

После этого мы можем приступить к созданию нашего инструмента (названногоgomodifytags). Инструмент должен сделать следующее, чтобы

  • Получите конфигурацию, чтобы сообщить нам, какую структуру следует изменить
  • Найдите и измените структуру в соответствии с конфигурацией
  • выходной результат

так какgomodifytagsВ основном будет применяться к редактору, и мы будем передавать конфигурацию через флаги CLI. Второй шаг состоит из нескольких шагов, таких как синтаксический анализ файла, поиск правильной структуры и последующее изменение структуры (путем изменения AST). Наконец, мы выводим результат, независимо от того, находится ли он в необработанном исходном файле Go или в каком-либо пользовательском протоколе (например, JSON, подробнее об этом позже).

Вот основные особенности упрощенной версии gomodifytags:

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

Давайте начнем с первого шага и посмотрим, какполучить конфигурацию. Ниже представлена ​​наша конфигурация со всей необходимой информацией

type config struct {
	// first section - input & output
	file     string
	modified io.Reader
	output   string
	write    bool

	// second section - struct selection
	offset     int
	structName string
	line       string
	start, end int

	// third section - struct modification
	remove    []string
	add       []string
	override  bool
	transform string
	sort      bool
	clear     bool
	addOpts    []string
	removeOpts []string
	clearOpt   bool
}

он разделен натриГлавная часть:

первая частьСодержит настройки того, как и какой файл читать. Это может быть имя файла в локальной файловой системе или непосредственно из стандартного ввода (в основном используется в редакторах). Он также определяет способ вывода результата (исходный файл или JSON) и следует ли перезаписывать файл вместо вывода на стандартный вывод.

Вторая часть定义了如何选择一个结构体及其字段。 Есть несколько способов сделать это.我们可以通过它的偏移(光标位置)、结构体名称、一行单行(仅选择字段)或一系列行来定义它。最后,我们无论如何都得到开始行/结束行。例如在下面的例子中,您可以看到,我们使用它的名字来选择结构体,然后提取开始行和结束行以选择正确的字段:

Если это для редактора, лучше использоватьбайтовое смещение. Например, ниже вы можете видеть, что наш курсор оказалсяportПосле имени поля мы можем легко получить начальную/конечную строку:

в конфигурациитретья частьна самом деле отображение кstructtagОднозначное сопоставление пакета. Это в основном позволяет нам передать конфигурацию после чтения поляstructtagСумка. Как вы знаете,structtagПакеты позволяют нам анализировать тег структуры и вносить изменения в отдельные части. Но он не перезаписывает и не обновляет поля структуры.

Как мы получим конфигурацию? мы просто используемflagpackage, затем создайте флаг для каждого поля в конфиге и назначьте их. Например:

flagFile := flag.String("file", "", "Filename to be parsed")
cfg := &config{
	file: *flagFile,
}

мыдля каждого поля в конфигурацииПовторяй. Для получения полной информации ознакомьтесь с текущей основной веткой gomodifytag.Определение логотипа

Получив конфигурацию, мы можем выполнить базовую проверку:

func main() {
	cfg := config{ ... }

	err := cfg.validate()
	if err != nil {
		log.Fatalln(err)
	}

	// continue parsing
}

// validate validates whether the config is valid or not
func (c *config) validate() error {
	if c.file == "" {
		return errors.New("no file is passed")
	}

	if c.line == "" && c.offset == 0 && c.structName == "" {
		return errors.New("-line, -offset or -struct is not passed")
	}

	if c.line != "" && c.offset != 0 ||
		c.line != "" && c.structName != "" ||
		c.offset != 0 && c.structName != "" {
		return errors.New("-line, -offset or -struct cannot be used together. pick one")
	}

	if (c.add == nil || len(c.add) == 0) &&
		(c.addOptions == nil || len(c.addOptions) == 0) &&
		!c.clear &&
		!c.clearOption &&
		(c.removeOptions == nil || len(c.removeOptions) == 0) &&
		(c.remove == nil || len(c.remove) == 0) {
		return errors.New("one of " +
			"[-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options]" +
			" should be defined")
	}

	return nil
}

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

Мы уже начали говорить о том, как парсить файлы. Разбор здесьconfigМетод структуры. На самом деле все методыconfigЧасть структуры:

func main() {
	cfg := config{}

	node, err := cfg.parse()
	if err != nil {
		return err
	}

	// continue find struct selection ...
}

func (c *config) parse() (ast.Node, error) {
	c.fset = token.NewFileSet()
	var contents interface{}
	if c.modified != nil {
		archive, err := buildutil.ParseOverlayArchive(c.modified)
		if err != nil {
			return nil, fmt.Errorf("failed to parse -modified archive: %v", err)
		}
		fc, ok := archive[c.file]
		if !ok {
			return nil, fmt.Errorf("couldn't find %s in archive", c.file)
		}
		contents = fc
	}

	return parser.ParseFile(c.fset, c.file, contents, parser.ParseComments)
}

parseФункции делают только одно: анализируют исходный код и возвращаютast.Node. Если мы передаем файл, это довольно просто, в этом случае мы используем функцию parser.ParseFile(). должен быть в курсеtoken.NewFileSet(), который создает*token.FileSetтип. мы храним его вc.fset, а также перешел наparser.ParseFile()функция. Зачем?

так какfilesetдля каждого файланезависимыйХраните информацию о местоположении для каждого узла. Это очень пригодится позже и может быть использовано для полученияast.Nodeточное местоположение (обратите внимание, чтоast.Nodeиспользует сжатое местоположениеtoken.Pos. Для получения дополнительной информации необходимо пройтиtoken.FileSet.Position()Функция, чтобы получить одинtoken.Position, который содержит больше информации)

давай продолжим. Это еще более интересно, если исходный файл передается через стандартный ввод.config.modifiedполе легко проверитьio.Reader, но на самом деле мы передаем stdin. Как мы можем определить, нужно ли нам читать со стандартного ввода?

Мы спрашиваем пользователя, хотят ли они передавать контент через стандартный ввод. В этом случае пользователю инструмента необходимо пройти--modifiedзнак (этологическийподписать). Если пользователь передал его, мы просто назначаем стандартный ввод дляc.modified:

flagModified = flag.Bool("modified", false,
	"read an archive of modified files from standard input")

if *flagModified {
	cfg.modified = os.Stdin
}

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

  • Имя файла, за которым следует новая строка строки
  • Размер документа (десятичный), затем строка новых строк
  • содержимое файла

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

этометодТакже используется несколькими другими инструментами (такими какguru,gogetdocд.), очень полезно для редакторов. Поскольку это позволяет редактору передавать измененное содержимое файла, ане сохраняется в файловой системе. Отсюда и названиеmodified.

Теперь, когда у нас есть собственный узел, давайте перейдем к шагу «поиск структуры»:

В основной функции мы будем использовать парсинг из предыдущего шагаast.NodeперечислитьfindSelection()функция:

func main() {
	// ... parse file and get ast.Node

	start, end, err := cfg.findSelection(node)
	if err != nil {
		return err
	}

	// continue rewriting the node with the start&end position
}

cfg.findSelection()Функция возвращает начальную и конечную позиции структуры на основе конфигурации, чтобы сообщить нам, как выбрать структуру. Он выполняет итерацию по заданному узлу, затем возвращает начальную/конечную позицию (как описано в разделе конфигурации выше):

查找步骤遍历所有节点,直到找到一个 *ast.StructType,并返回该文件的开始位置和结束位置

Но как? Помните, что есть три режима. соответственноРядвыберите,Компенсироватьиимя структуры:

// findSelection returns the start and end position of the fields that are
// suspect to change. It depends on the line, struct or offset selection.
func (c *config) findSelection(node ast.Node) (int, int, error) {
	if c.line != "" {
		return c.lineSelection(node)
	} else if c.offset != 0 {
		return c.offsetSelection(node)
	} else if c.structName != "" {
		return c.structSelection(node)
	} else {
		return 0, 0, errors.New("-line, -offset or -struct is not passed")
	}
}

РядВыбор — самая простая часть. Здесь мы просто возвращаем само значение флага. Итак, если пользователь переходит в--line 3,50флаг, функция вернет(3, 50, nil). Все, что он делает, это разделяет значение флага и преобразует его в целое число (снова выполняя проверку):

func (c *config) lineSelection(file ast.Node) (int, int, error) {
	var err error
	splitted := strings.Split(c.line, ",")

	start, err := strconv.Atoi(splitted[0])
	if err != nil {
		return 0, 0, err
	}

	end := start
	if len(splitted) == 2 {
		end, err = strconv.Atoi(splitted[1])
		if err != nil {
			return 0, 0, err
		}
	}

	if start > end {
		return 0, 0, errors.New("wrong range. start line cannot be larger than end line")
	}

	return start, end, nil
}

Редактор использует этот режим, когда вы выбираете группу строк и выделяете их.

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

// collectStructs collects and maps structType nodes to their positions
func collectStructs(node ast.Node) map[token.Pos]*structType {
	structs := make(map[token.Pos]*structType, 0)
	collectStructs := func(n ast.Node) bool {
		t, ok := n.(*ast.TypeSpec)
		if !ok {
			return true
		}

		if t.Type == nil {
			return true
		}

		structName := t.Name.Name

		x, ok := t.Type.(*ast.StructType)
		if !ok {
			return true
		}

		structs[x.Pos()] = &structType{
			name: structName,
			node: x,
		}
		return true
	}
	ast.Inspect(node, collectStructs)
	return structs
}

Мы используемast.Inspect()Функция проходит через AST и ищет структуры. мы сначала ищем*ast.TypeSpec, чтобы мы могли получить имя структуры. поиск*ast.StructTypeдается сама структура, а не ее имя. Поэтому у нас есть обычайstructTypetype, который содержит имя и сам узел структуры. Это удобно везде. Поскольку позиция каждой структуры уникальна и невозможно иметь две разные структуры в одной и той же позиции, мы используем позицию в качестве ключа для карты.

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

func (c *config) offsetSelection(file ast.Node) (int, int, error) {
	structs := collectStructs(file)

	var encStruct *ast.StructType
	for _, st := range structs {
		structBegin := c.fset.Position(st.node.Pos()).Offset
		structEnd := c.fset.Position(st.node.End()).Offset

		if structBegin <= c.offset && c.offset <= structEnd {
			encStruct = st.node
			break
		}
	}

	if encStruct == nil {
		return 0, 0, errors.New("offset is not inside a struct")
	}

	// offset mode selects all fields
	start := c.fset.Position(encStruct.Pos()).Line
	end := c.fset.Position(encStruct.End()).Line

	return start, end, nil
}

Мы используемcollectStructs()чтобы собрать все структуры, а затем выполнить итерацию здесь. Также помните, что мы храним начальное значение, используемое для разбора файла.token.FileSetКакие?

Теперь это можно использовать для получения информации о смещении для каждого узла структуры (мы декодируем его какtoken.Position, что дает нам.Offsetполе). Все, что мы делаем, — это простая проверка и итерация, пока не найдем структуру (названную здесьencStruct):

for _, st := range structs {
	structBegin := c.fset.Position(st.node.Pos()).Offset
	structEnd := c.fset.Position(st.node.End()).Offset

	if structBegin <= c.offset && c.offset <= structEnd {
		encStruct = st.node
		break
	}
}

С помощью этой информации мы можем извлечь начальное и конечное положение найденных структур:

start := c.fset.Position(encStruct.Pos()).Line
end := c.fset.Position(encStruct.End()).Line

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

func (c *config) structSelection(file ast.Node) (int, int, error) {
	// ...

	for _, st := range structs {
		if st.name == c.structName {
			encStruct = st.node
		}
	}

	// ...
}

Теперь, когда у нас есть начальная и конечная позиции, мы наконец можем перейти к третьему шагу: изменению полей структуры.

существуетmainфункцию, мы будем вызывать с узлом, проанализированным на предыдущем шагеcfg.rewrite()функция:

func main() {
	// ... find start and end position of the struct to be modified


	rewrittenNode, errs := cfg.rewrite(node, start, end)
	if errs != nil {
		if _, ok := errs.(*rewriteErrors); !ok {
			return errs
		}
	}


	// continue outputting the rewritten node
}

Это ядро ​​инструмента. существуетrewriteмы перезапишем все поля структуры между начальной и конечной позициями. Прежде чем углубиться, давайте взглянем на общее содержание функции:

// rewrite rewrites the node for structs between the start and end
// positions and returns the rewritten node
func (c *config) rewrite(node ast.Node, start, end int) (ast.Node, error) {
	errs := &rewriteErrors{errs: make([]error, 0)}

	rewriteFunc := func(n ast.Node) bool {
		// rewrite the node ...
	}

	if len(errs.errs) == 0 {
		return node, nil
	}

	ast.Inspect(node, rewriteFunc)
	return node, errs
}

Как видите, мы снова используемast.Inspect()для перехода по дереву для данного узла. мы переписываемrewriteFuncМетки для каждого поля в функции (подробнее об этом позже).

потому что перешел наast.Inspect()Функция не возвращает ошибку, поэтому мы создадим карту ошибок (используяerrsопределения переменных), а затем собирать ошибки по мере прохождения дерева и обработки каждого отдельного поля. Теперь давай поговоримrewriteFuncВнутренняя работа:

rewriteFunc := func(n ast.Node) bool {
	x, ok := n.(*ast.StructType)
	if !ok {
		return true
	}

	for _, f := range x.Fields.List {
		line := c.fset.Position(f.Pos()).Line

		if !(start <= line && line <= end) {
			continue
		}

		if f.Tag == nil {
			f.Tag = &ast.BasicLit{}
		}

		fieldName := ""
		if len(f.Names) != 0 {
			fieldName = f.Names[0].Name
		}

		// anonymous field
		if f.Names == nil {
			ident, ok := f.Type.(*ast.Ident)
			if !ok {
				continue
			}

			fieldName = ident.Name
		}

		res, err := c.process(fieldName, f.Tag.Value)
		if err != nil {
			errs.Append(fmt.Errorf("%s:%d:%d:%s",
				c.fset.Position(f.Pos()).Filename,
				c.fset.Position(f.Pos()).Line,
				c.fset.Position(f.Pos()).Column,
				err))
			continue
		}

		f.Tag.Value = res
	}

	return true
}

Помните, в дереве ASTкаждый узелЭта функция будет вызвана. Поэтому мы ищем только тип*ast.StructTypeузел. Получив это, мы можем начать перебирать поля структуры.

Здесь мы используемstartиendПеременная. Это определяет, хотим ли мы изменить поле. Если позиция поля находится между start-end, мы продолжаем, в противном случае игнорируем:

if !(start <= line && line <= end) {
	continue // skip processing the field
}

Далее мы проверяем, существует ли метка. Если поле метки пустое (то есть nil), тоинициализацияПоле метки. Это помогает вернутьсяcfg.process()Функция предотвращения паники:

if f.Tag == nil {
	f.Tag = &ast.BasicLit{}
}

Теперь позвольте мне сначала объяснитьинтересныйместо, прежде чем продолжить.gomodifytagsПопробуйте получить имя поля поля и обработать его. Однако как насчет анонимного поля? :

type Bar string

type Foo struct {
	Bar //this is an anonymous field
}

В данном случае, поскольку имени поля нет, пытаемся получить из имени типаИмя поля:

// if there is a field name use it
fieldName := ""
if len(f.Names) != 0 {
	fieldName = f.Names[0].Name
}

// if there is no field name, get it from type's name
if f.Names == nil {
	ident, ok := f.Type.(*ast.Ident)
	if !ok {
		continue
	}

	fieldName = ident.Name
}

Как только мы получим имя поля и значение тега, вы можете начать обработку поля.cfg.process()Функция отвечает за обработку полей с именами полей и значениями меток (если они есть). После возвращает обработанный результат (в нашем случае этоstruct tagformat), который мы используем для перезаписи существующих значений тегов:

res, err := c.process(fieldName, f.Tag.Value)
if err != nil {
	errs.Append(fmt.Errorf("%s:%d:%d:%s",
		c.fset.Position(f.Pos()).Filename,
		c.fset.Position(f.Pos()).Line,
		c.fset.Position(f.Pos()).Column,
		err))
	continue
}

// rewrite the field with the new result,i.e: json:"foo"
f.Tag.Value = res

На самом деле, если вы помнитеstructtag, который возвращает экземпляр тегаString()выражение. Прежде чем мы вернемся к окончательному представлению метки, мы используем по мере необходимостиstructtagРазличные методы пакета изменяют структуру. Ниже приведена простая иллюстрация диаграммы:

用 structtag 包修改每个字段

Например, мы хотим расширитьprocess()серединаremoveTags()функция. Эта функция использует следующую конфигурацию для создания массива меток (имен ключей) для удаления:

flagRemoveTags = flag.String("remove-tags", "", "Remove tags for the comma separated list of keys")

if *flagRemoveTags != "" {
	cfg.remove = strings.Split(*flagRemoveTags, ",")
}

существуетremoveTags(), проверяем, что используем--remove-tags. Если есть, мы будем использовать structtagtags.Delete()метод удаления тегов:

func (c *config) removeTags(tags *structtag.Tags) *structtag.Tags {
	if c.remove == nil || len(c.remove) == 0 {
		return tags
	}

	tags.Delete(c.remove...)
	return tags
}

Та же логика применима кcfg.Process()все функции в .


У нас уже есть переписанный узел, давайте обсудим последнюю тему. Вывести и отформатировать результат:

В основной функции мы будем использовать узел, переписанный с предыдущего шага, для вызоваcfg.format()функция:

func main() {
	// ... rewrite the node

	out, err := cfg.format(rewrittenNode, errs)
	if err != nil {
		return err
	}

	fmt.Println(out)
}

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

Теперь давайте посмотримformat()функция:

func (c *config) format(file ast.Node, rwErrs error) (string, error) {
	switch c.output {
	case "source":
		// return Go source code
	case "json":
		// return a custom JSON output
	default:
		return "", fmt.Errorf("unknown output mode: %s", c.output)
	}
}

У нас естьДва режима вывода.

Первый(source) для печати в формате Goast.Node. Это параметр по умолчанию, и если вы используете его в командной строке или просто хотите увидеть изменения в файлах, это отлично для вас.

секундаопции (json) является более продвинутым и предназначен для других сред (особенно для редакторов). Он кодирует вывод в соответствии со следующей структурой:

type output struct {
	Start  int      `json:"start"`
	End    int      `json:"end"`
	Lines  []string `json:"lines"`
	Errors []string `json:"errors,omitempty"`
}

Ввод в инструмент и вывод конечного результата (без ошибок) примерно такие:

назадformat()функция. Как упоминалось ранее, есть два режима. использование исходного режимаgo/formatПакет форматирует AST в исходный код Go. Этот пакет также используется многими другими официальными инструментами, такими какgofmt)использовать. Ниже приведеныsourceКак реализован шаблон:

var buf bytes.Buffer
err := format.Node(&buf, c.fset, file)
if err != nil {
	return "", err
}

if c.write {
	err = ioutil.WriteFile(c.file, buf.Bytes(), 0)
	if err != nil {
		return "", err
	}
}

return buf.String(), nil

Формат пакета принятio.Writerи отформатировать его. Вот почему мы создаем промежуточный буфер (var buf bytes.Buffer), когда пользователь переходит в-writeфлаг, мы можем использовать его для перезаписи файла. После форматирования мы возвращаем строковое представление буфера, в котором содержится отформатированный исходный код Go.

jsonРежим более интересный. Поскольку мы возвращаем часть исходного кода, нам нужно отобразить его именно так, как нужно, что означает включение комментариев. Проблема в том, что при использованииformat.Node()При печати отдельных структур комментарии Go не могут быть напечатаны, если они с потерями.

Что такое потеря потери (комментарий с потерями)? Взгляните на этот пример:

type example struct {
	foo int 

	// this is a lossy comment

	bar int 
}

Каждое поле*ast.Fieldтип. Эта структура имеет*ast.Field.CommentПоле, содержащее комментарий к полю.

Но в приведенном выше примере кому он принадлежит? принадлежатьfooвсе ещеbar?

так какневозможноХорошо, эти аннотации называются аннотациями с потерями. Если вы сейчас используетеformat.Node()Проблема возникает, когда функция печатает структуру выше. Когда вы распечатаете его, вы можете получить (play.go wave.org/afraid/PE HS swf4J…):

type example struct {
	foo int

	bar int
}

Проблема в том, что аннотации с потерями*ast.Fileизчасть,он отделен от дерева. Он печатается только тогда, когда весь файл напечатан. Таким образом, решение состоит в том, чтобы напечатать весь файл, а затем удалить указанную строку, которую мы хотим вернуть в выводе JSON:

var buf bytes.Buffer
err := format.Node(&buf, c.fset, file)
if err != nil {
	return "", err
}

var lines []string
scanner := bufio.NewScanner(bytes.NewBufferString(buf.String()))
for scanner.Scan() {
	lines = append(lines, scanner.Text())
}

if c.start > len(lines) {
	return "", errors.New("line selection is invalid")
}

out := &output{
	Start: c.start,
	End:   c.end,
	Lines: lines[c.start-1 : c.end], // cut out lines
}

o, err := json.MarshalIndent(out, "", "  ")
if err != nil {
	return "", err
}

return string(o), nil

Это гарантирует, что мы можем распечатать все аннотации.


Вот и все!

Мы успешно завершили работу над нашим инструментом, и вот диаграмма полных шагов, которые мы реализовали на протяжении всего руководства:

gomodifytags的概述

Посмотрите, что мы сделали:

  • Передаем логотип CLIзабратьнастроить
  • мы проходимgo/parserпакет анализирует файл, чтобы получитьast.Node.
  • После разбора файла мыпоискПолучите соответствующую структуру, чтобы получить начальную и конечную позиции, чтобы мы могли знать, какие поля необходимо изменить.
  • Как только у нас есть начальная и конечная позиции, мы повторяем снова.ast.Node, перезаписывая каждое поле между начальной и конечной позициями (используяstructtagСумка)
  • После этого мы отформатируем переписанный узел для вывода исходного кода Go или пользовательского JSON для редактора.

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

gomodifytagsВ течение нескольких месяцев успешно внедряются следующие редакторы и плагины, повышающие производительность тысяч разработчиков:

  • vim-go
  • atom
  • vscode
  • acme

Если вас интересует оригинальный исходный код, его можно найти здесь:

я все еще здесьGophercon 2017выступил с речью, если вы заинтересованы, вы можете щелкнуть следующий адрес YouTube, чтобы посмотреть:

Woohoo.YouTube.com/embed/T4AI В…

18.jpg

Спасибо, что прочитали эту статью. Надеюсь, это руководство вдохновило вас на создание нового инструмента Go с нуля.