Search code examples
stringgo

How to implement String() method with optional fields in Go?


I am implementing a String() method on a type I have. This type's fields provide optional configuration options, and a user may choose to add any combination of those fields without necessarily needing all of them. The end result needs to be formatted as a JSON string (dictated by external service I'm consuming, not my choice).

Edit: a bit of additional context. The reason I need a String() method is because when I actually provide a value of this type to the external service, it will be formatted as a URL encoded string.

My approach was something along the lines of:


type AnotherStruct struct {
    FieldA []string
    FieldB []string
}

type Spec struct {
    FieldA []string
    FieldB int
    FieldC AnotherStruct
}

func (s Spec) String() string {
    args := make([]any, 0)
    f := "{"
    if s.FieldA != nil {
        f += `"fieldA": %v, `
        args = append(args, s.FieldA)
    }
    if s.FieldB != 0 {
        f += `"fieldB": %d, `
        args = append(args, s.FieldB)
    }
    if !reflect.DeepEqual(s.FieldC, AnotherStruct{}) {
        f += `"fieldC": %v, `
        args = append(args, s.FieldC)
    }
    f += "}"

    return fmt.Sprintf(f, args...)
}

But this seems clunky. Is there a clean way to implement such a method in Go?


Solution

  • There's no reason to create a String method in your case. Everything* you're aiming to do is already handled by the standard library encoding/json package.

    The idiomatic way to do what you're after is with JSON struct tags:

    type Spec struct {
        FieldA []string       `json:"fieldA,omitempty"`
        FieldB int            `json:"fieldB,omitempty"`
        FieldC *AnotherStruct `json:"fieldC,omitempty"`
    }
    

    Then just call json.Marshal() on your value:

    s := Spec{
        FieldA: []string{"one", "two"},
        // FieldB & FieldC omitted
    }
    output, err := json.Marshal(s)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(output))
    

    Prints:

    {"fieldA":["one","two"]}
    

    *The only change I had to make to conform to the standard library expectations was to make FieldC a pointer to AnotherStruct:

        FieldC *AnotherStruct `json:"fieldC,omitempty"`
    

    instead of

        FieldC AnotherStruct `json:"fieldC,omitempty"`