Почему появляется ошибка not a struct pointer в Go?

Только начал разбираться с Go и столкнулся с такой проблемой. Делаю YAML парсер, который заполняет структуру и потом подставляет дефолтные значения.

import (
    "os"

    "github.com/creasty/defaults"
    "gopkg.in/yaml.v3"
)

type ConfigSchema struct {
    Metadata MetadataSchema `yaml:"metadata"`
    Fields   []FieldSchema  `yaml:"fields"`
}

type MetadataSchema struct {
    Name string `yaml:"name"`
}

type FieldSchema struct {
    Settings FieldSettingsSchema `yaml:"settings"`
    Value    string              `yaml:"value"`
}

type FieldSettingsSchema struct {
    Description string `yaml:"description"`
    Enabled     bool   `yaml:"enabled" default:"true"`
}

func main() {
    yamlPath := "C:/path/test.yml"

    var configSchema ConfigSchema
    _ = parse(yamlPath, configSchema)
}

func parse(filePath string, out interface{}) error {
    fileContent, err := os.ReadFile(filePath)
    if err != nil {
        return err
    }

    err = yaml.Unmarshal(fileContent, &out)
    if err != nil {
        return err
    }

    err = defaults.Set(out) // <- "not a struct pointer"
    if err != nil {
        panic(err)
    }

    return nil
}

Файл парсится корректно, но при заполнении пустых значений почему-то появляется ошибка not a struct pointer. Можете подсказать как её можно исправить?

Думал, что может нужно также указывать ссылку в defaults.Set(&out), но ошибка остается.

Еще был вариант поместить инициализацию var configSchema ConfigSchema в функцию parse и возвращать (ConfigSchema, err). В таком случае всё работает корректно, но так есть привязка к конкретной структуре, но хочется реализовать более универсальный парсер.


Ответы (1 шт):

Автор решения: Pak Uula

Смотрите, в чём дело.

Как defaults.Set проверяет вид содержимого:

  • сначала проверяется, что содержимое является указателем,
  • затем проверяется, что по указателю лежит объект вида reflect.Struct

Когда вы в своём коде вызываете parse(yamlPath, configSchema), на стеке создаётся копия структуры configSchema, указатель на которую упаковывается в interface{}. При распаковке этого контейнера средствами reflect тип содержимого будет ConfigSchema. Когда вы передаёте такой out в defaults.Set, там проверят вид содержимого контейнера interface{} - окажется Struct, а не Ptr. Отсюда ошибка.

Почему в вашем случае не сработает defaults.Set(&out)? Причина в том, что defaults.Set получит объект типа *interface{}, а не *ConfigSchema: в силу статической типизации Go хранится только та типовая информация, которая приведена в тексте программы. Раз out имеет тип interface{}, то &out имеет тип *interface{}. defaults.Set отвергнет *interface{} на втором этапе: указатель указывает на объект вида Interface, а не Struct.

Вот пример, в котором defaults.Set не ругается.

func main() {
    // ...
    var cfg ConfigSchema
    err = parse(bz, &cfg)
    // ...
}
func parse(bytez []byte, out interface{}) error {

    err := yaml.Unmarshal(bytez, out)
    if err != nil {
        return err
    }

    err = defaults.Set(out) 
    if err != nil {
        panic(err)
    }

    return nil
}

Видите в чём разница? Внутри parse я использую только out, и не беру указатель на этот интерфейс. Указатель должен передаваться непосредственно в функцию.

Я передаю в функцию parse указатель на результат: err = parse(bz, &cfg). В контейнер упаковывается указатель на указатель, и при распаковке interface{} получается объект типа *ConfigSchema. Затем я передаю в defaults.Set сам объект out, который содержит указатель на структуру. Благодаря этому defaults.Set находит именно указатель на структуру и не возвращает ошибку.

→ Ссылка