I have a structure with integer fields, the representation of which makes sense to humans in hexadecimal form. For example, let this be the Vendor ID field.
I want to save this data to a YAML file for manual editing and then load it from the file. As I understand it, there are no problems with hexadecimal representation of numbers in YAML itself, but go-yaml
(I use v3
) encodes integers in decimal form and I have not found a normal way to make it save them in hexadecimal form.
Let's take the following code as a starting point:
import (
//...
"gopkg.in/yaml.v3"
)
type DeviceInfo struct {
VendorId uint32 `yaml:"vendorid"`
}
func main() {
deviceInfo := DeviceInfo{VendorId: 0xdeadbeef}
yml, err := yaml.Marshal(deviceInfo)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(yml))
}
This code generates YAML with a decimal value:
vendorid: 3735928559
Next, go-yaml
allows you to create a custom marshaler for your own types. I did this (I deliberately omitted the 0x
prefix in the fmt.Sprintf()
format string):
type Uint32Hex uint32
func (U Uint32Hex) MarshalYAML() (interface{}, error) {
return fmt.Sprintf("%x", U), nil
}
type DeviceInfo struct {
VendorId Uint32Hex `yaml:"vendorid"`
}
func main() {
// the same code
}
This code generates the value in hexadecimal, but without the 0x
prefix (logical for now):
vendorid: deadbeef
But if I add the 0x
prefix in the custom marshaler:
func (U Uint32Hex) MarshalYAML() (interface{}, error) {
return fmt.Sprintf("0x%x", U), nil
}
The value is generated correctly, but not as a number, but as a string:
vendorid: "0xdeadbeef"
And in order to then unmarshal this line into a number, I will also have to write a custom unmarshaler. I don't like this solution.
In the end, I have the following questions:
Is there any simple way to generate hexadecimal numbers using go-yaml
that I haven't found?
Is it possible to make a custom encoder in terms of go-yaml
as an extension of the package without changing the package itself? For me, a more convenient way would be to pass a format tag in the structure description, for example, like this:
type DeviceInfo struct {
VendorId uint32 `yaml:"vendorid,hex"`
}
If this requires changing the package code, what is the Go practice for such cases? Just copy the package files into my project, modify as needed and import it locally?
The problem here is that quoting strings is optional in yaml, but go-yaml
uses the same the same internal architecture as the JSON encoder. This means that custom marshaling is processed first and then, entirely separately, quoting logic is applied. The reason deadbeef
isn't quoted, but 0xdeadbeef
is, is because the latter is a number. It gets quoted so that it's not accidentally unmarshalled as a number when it knows it should be a string, because your custom marshaler returned a string. Because deadbeef
can't be read as a valid number, it doesn't need to be quoted. There are two things you can do:
func (U *Uint32Hex) UnmarshalYAML(value *yaml.Node) error {
parsed, err := strconv.ParseUint(value.Value, 0, 32)
*U = Uint32Hex(parsed)
return err
}
go-yaml
and modify it. If you do this, you shouldn't change the import in your source files. Instead, you should add a replace
directive to your go.mod
, like this:require gopkg.in/yaml.v3 v3.0.1
replace gopkg.in/yaml.v3 v3.0.1 => ../yaml.v3 // Your local path to the fork
I prefer solution 1, because it allows you serialize these values in your preferred way, without diverging from the yaml that others produce, and doesn't require you to maintain a fork.