A strange behavior of printing struct with nested struct in GoLang

  sonic0002        2018-10-29 09:59:49       8,058        2    

Normally when trying to print a struct , we would use %v to show all data of the struct. It will print the default format for each field in the struct.
%v	the value in a default format
	when printing structs, the plus flag (%+v) adds field names
But recently we observed a strange behavior when printing a struct with nested struct which has a String() string implemented, the %v format prints an 'unexpected' output per our understanding.  

Let's see the example snippet first.

package main

import (
  "fmt"
)

type Inner struct {
}

type A struct {
    Inner
    FieldA string
}

func (i Inner) String() string {
    return "anything"
}

func main() {
    myA := A{FieldA: "A"}
    fmt.Printf("%v", myA)
}

 We expect the output to be

{anything A}

But the actual result is

anything

This doesn't make sense, right? It seems FieldA is ignored if the String() string is implemented for Inner struct type.

Per our understanding, the struct A type has two fields: Inner and FieldA, when printing the values, it would loop through the fields and print using their default format. So Inner should call its String() while FieldA will print its string value. However, the above output makes us doubt our understanding.

After a closer look at the docs, it has below rules.

If the format (which is implicitly %v for Println etc.) is valid for a string (%s %q %v %x %X), the following two rules apply:

  1. If an operand implements the error interface, the Error method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).

  2. If an operand implements method String() string, that method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).

Since Inner is an operand and it implements the String() string method which is considered as a Stringer interface, so it is called when printing. This explains the output we actually see.  Below is actually the part of source code in Go.

switch verb {
case 'v', 's', 'x', 'X', 'q':
	// Is it an error or Stringer?
	// The duplication in the bodies is necessary:
	// setting handled and deferring catchPanic
	// must happen before calling the method.
	switch v := p.arg.(type) {
	case error:
		handled = true
		defer p.catchPanic(p.arg, verb)
		p.fmtString(v.Error(), verb)
		return

	case Stringer:
		handled = true
		defer p.catchPanic(p.arg, verb)
		p.fmtString(v.String(), verb)
		return
	}
}

 Now what if we have two nested structs in A and both of them have String() string implemented?

package main

import (
  "fmt"
)

type Inner struct {
}

type InnerAgain struct {
}

type A struct {
    Inner
    InnerAgain
    FieldA string
}

func (i Inner) String() string {
    return "anything"
}

func (i InnerAgain) String() string {
    return "nothing"
}

func main() {
    myA := A{FieldA: "A"}
    fmt.Printf("%v", myA)
}

 The output is what we originally expected

{anything nothing A}

The reason for above output is that it is ambiguous which String() to invoke so it will fallback to loop through all fields and get their value in default format.

Please be careful about the above behavior Go provides so that you would not miss out some important data when printing struct in log with %v format.

One final thought about above behavior. Is the rule #5 reasonable in the first place? 

PROGRAMMING  GOLANG 

       

  RELATED


  2 COMMENTS


Johan [Reply]@ 2020-06-08 05:08:19

Hi

Do you know if this bug is reported to Golang team?

Thanks

Ke Pi [Reply]@ 2020-06-13 05:30:16

Sorry not yet as we thought it is expected per the official doc. 



  RANDOM FUN

Product and the code behind it