SoFunction
Updated on 2025-03-03

Detailed explanation of Golang's custom types and methods collection

Golang picking up the golang is mainly used to record some forgotten golang related knowledge that you have never noticed.

It hasn't been updated for a long time, so let's start with a puzzle to practice:

package main
 
import (
    "encoding/json"
    "fmt"
    "time"
)
 
type MyTime 
 
func main() {
    myTime := MyTime(()) // Assume that the time obtained is 20:30:00 on July 20, 2022, time zone UTC+8    res, err := (myTime)
    if err != nil {
        panic(err)
    }
    (string(res))
}

What will the above code output:

1. Compilation error

2. Runtime panic

3.{}

4."2022-07-20T20:30:00.135693011+08:00"

Many people will definitely choose 4, but the answer is 3:

$ go run
 
{}

Isn't it very surprising that MyTime is, theoretically, and it should have been implemented, why is the output empty?

In fact, this is a problem encountered by a group member recently. At first glance, it looks like a golang bug, but in fact, it still does not master the basic rules of the language.

Before we go deeper, we first ask ourselves two questions:

  • Is MyTime really the Time type?
  • Has MyTime really been implemented?

For question 1, just quote the description in the spec:

A named type is always different from any other type.

/ref/spec#Type_identity

It means that as long as the type defined is different (except type alias), even if their underlying type is the same, there are two different types.

Then you will know the answer to question 1, obviously MyTime is not.

Since MyTime is not Time, can it use Time-type method? After all, the base type of MyTime is Time. Let's write a code to verify:

package main
 
import (
    "fmt"
    "time"
)
 
type MyTime 
 
func main() {
    myTime := MyTime(()) // Assume that the time obtained is 20:30:00 on July 20, 2022, time zone UTC+8    res, err := ()
    if err != nil {
            panic(err)
    }
    (string(res))
}

Running results:

# command-line-arguments
./:12:24: undefined (type MyTime has no field or method MarsharlJSON)

Now there is an answer to question 2: MyTime is not implemented.

So how does json serialize a type that is not implemented? I won't stop here. It's written in the document. For types that do not implement Marshaler, the default process uses reflection to obtain all non-export fields, and then serialize them in sequence. Let's take a look at the structure of time:

type Time struct {
        // wall and ext encode the wall time seconds, wall time nanoseconds,
        // and optional monotonic clock reading in nanoseconds.
        //
        // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
        // a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
        // The nanoseconds field is in the range [0, 999999999].
        // If the hasMonotonic bit is 0, then the 33-bit field must be zero
        // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
        // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
        // unsigned wall seconds since Jan 1 year 1885, and ext holds a
        // signed 64-bit monotonic clock reading, nanoseconds since process start.
        wall uint64
        ext  int64
 
        // loc specifies the Location that should be used to
        // determine the minute, hour, month, day, and year
        // that correspond to this Time.
        // The nil location means UTC.
        // All UTC times are represented with loc==nil, never loc==&utcLoc.
        loc *Location
}

There are non-public fields inside, so the whole result after direct serialization is {}. Of course, the Time type is reimplemented by itself, so it can be serialized normally to the value we expect.

Our MyTime does not implement the entire interface, so we have gone through the default serialization process.

So we can draw an important conclusion: Type B, B, derived from a certain type A, cannot obtain any of the method sets of A.

It is not impossible to have all the methods of B, but you have to say goodbye to a form like type B A.

Method one is to use type alias:

- type MyTime 
+ type MyTime = 
 
func main() {
-   myTime := MyTime(()) // Assume that the time obtained is 20:30:00 on July 20, 2022, time zone UTC+8+   var myTime MyTime = () // Assume that the time obtained is 20:30:00 on July 20, 2022, time zone UTC+8    res, err := (myTime)
    if err != nil {
        panic(err)
    }
    (string(res))
}

A type alias is free to name, which means that an alias of type A is created without defining any new type (note those two lines of change). Now MyTime is Time, so you can also directly use Time's MarshalJSON.

Method 2: Use the embedded type:

- type MyTime 
+ type MyTime struct {
+     
+ }
 
func main() {
-   myTime := MyTime(()) // Assume that the time obtained is 20:30:00 on July 20, 2022, time zone UTC+8+   myTime := MyTime{}
    res, err := ()
    if err != nil {
            panic(err)
    }
    (string(res))
}

By embedding Time into MyTime, MyTime can also obtain a method set of Time type. For more specific things, you can see another article I wrote before: golang: Embed type

What if I really need to derive a new type, usually when we write a general module, we need to hide the implementation details, so if we want to wrap the original type, what should we do at this time?

In fact, we can make MyTime re-implementation:

type MyTime 
 
func (m MyTime) MarshalJSON() ([]byte, error) {
    // I can reuse Time directly for convenience    return (m).MarshalJSON()
}
 
func main() {
    myTime := MyTime(()) // Assume that the time obtained is 20:30:00 on July 20, 2022, time zone UTC+8    res, err := ()
    if err != nil {
            panic(err)
    }
    (string(res))
}

This seems to violate the DRY principle, but it may not be necessary. This is just a bad example. In real scenarios, the derived custom types are often customized, so there will be some additional operations in the serialization function, so there will be no conflict with DRY.

No matter which solution, it can solve the problem and make a choice based on your actual needs.

Summarize

To sum up, a custom type B derived from A has only two sources of methods in its method set:

  • Those methods directly defined on B
  • Methods of other types included in B as embedded type

The method of A does not exist in B.

If it is a custom type B struct {a, b int} derived from an anonymous type, then there is only one source of the methods in the B method set:

Those methods directly defined on B

And the most important thing is that if the two types have different names, even if their structure is exactly the same, they are two different types.

These knowledge about edges and corners is easy to be forgotten, but there is still a chance to encounter it at work. Remembering it can save a lot of trouble.

This is the article about the detailed explanation of Golang's custom types and method sets. For more related Golang custom types, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!