Advanced Golang Learning Memo

Nothing is better than learning from real project!

Struct, Method and Interface

Struct

The struct works as complex variable in GO. It’s a combination of different vars, func, and method.

The source struct in pagerduty package for ListIncidentsOptions is like :

type Client struct {
	debugFlag    *uint64
	lastRequest  *atomic.Value
	lastResponse *atomic.Value
	authToken           string
	apiEndpoint         string
	v2EventsAPIEndpoint string
	HTTPClient HTTPClient
}
type ListIncidentsOptions struct {
	Total bool `url:"total,omitempty"`
	Since       string   `url:"since,omitempty"`
	Until       string   `url:"until,omitempty"`
	DateRange   string   `url:"date_range,omitempty"`
	Statuses    []string `url:"statuses,omitempty,brackets"`
	IncidentKey string   `url:"incident_key,omitempty"`
	ServiceIDs  []string `url:"service_ids,omitempty,brackets"`
	TeamIDs     []string `url:"team_ids,omitempty,brackets"`
	UserIDs     []string `url:"user_ids,omitempty,brackets"`
	Urgencies   []string `url:"urgencies,omitempty,brackets"`
	TimeZone    string   `url:"time_zone,omitempty"`
	SortBy      string   `url:"sort_by,omitempty"`
	Includes    []string `url:"include,omitempty,brackets"`
}

It can inherit or sub struct:

type Client struct {
	Client *pagerduty.Client
	pagerduty.ListIncidentsOptions
}

So in this example, my new go package has Client struct defined which will take pagerduty package Client stuct as its element Client input, and inherit every single key in pagerduty.ListIncidentsOptions. And thanks to this feature, it makes method conversion possible and simple:

//package action
func Init() *Client {
	authtoken := "xyz"
	since, until := timerange(days)
	var opts pagerduty.ListIncidentsOptions
	opts.Since = since
	opts.Until = until
	opts.SortBy = "created_at:desc"
	client := Client{
		Client:               pagerduty.NewClient(authtoken),
		ListIncidentsOptions: opts,
	}
	return &client
}

Then in the main.go file just call it by:

client := action.Init()

and then all available method defined in action package will become available to this client endpoint, and can go even further down to underlayer pagerduty package to call its (c *Client) method by using client.Client.

Method

Func in a format like following are called method.

func (client *Client) Find_Unique() TypeOfA{
    A:=client.x.x
	return A
}

The result of defining this func is that the struct *Client will have a new field in called Find_Unique, and type will apparently be whatever returned by this func. By aggregate all funcs into struct, it makes codebase much clearer and easier to read. Noticed here *Client is a pointer, the reason here using a pointer is that we want to keep using the same client session, meaning all elements previouse assigned in func init() should be carried over, and if we modify any value of it, it should reflect on the original client.

Interface

Interface is mostly used for aggregating funcs. Think of a senario that you are trying to create a game, which has monsier and player attacking and defensing each other, apparently they’d all have their own attack/defense points, but the way of how to calculate its value may be different, maybe 1 hit from monsitor would cause 50 points of health, but 1 hit from player only causes 5 points. Repetitively defining both attack and defense funcs for both monsiter and player is apparently a tedious job.

package main

import (
	"fmt"
	"math"
)

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * math.Pow(c.Radius, 2)
}

func (c Circle) String() string {
	return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}

type Square struct {
	Width  float64
	Height float64
}

func (s Square) Area() float64 {
	return s.Width * s.Height
}

func (s Square) String() string {
	return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}

type Sizer interface {
	Area() float64
}

type Shaper interface {
	Sizer
	fmt.Stringer
}

func main() {
	c := Circle{Radius: 10}
	PrintArea(c)

	s := Square{Height: 10, Width: 5}
	PrintArea(s)

	l := Less(c, s)
	fmt.Printf("%v is the smallest\n", l)

}

func Less(s1, s2 Sizer) Sizer {
	if s1.Area() < s2.Area() {
		return s1
	}
	return s2
}

func PrintArea(s Shaper) {
	fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

In this example, both c and s have method Area() but with different defintion inside. Using interface can make reusing code much easier, imagine having a GO api gateway tries to call different exchange to make trade decisions, apparently each exchange would have their own buy/sell api endpoint, but the action to buy/sell are same meaning, it’s time to use interface.

Empty interface

type monster struct {
	damage int
}

func (m *monster) attack() int {
	return m.damage
}

type attacker interface {
	attack() int
}

type defender interface {
	defend() int
}

func attackOrDefend(attackerDefender interface{}) {
	// Inside this function, we don't know what we're getting, but we can check!
	if attacker, ok := attackerDefender.(attacker); ok {
		fmt.Printf("Attacking with damage %d\n", attacker.attack())
	} else if defender, ok := attackerDefender.(defender); ok {
		fmt.Printf("Defending with damage %d\n", defender.defend())
	}
}

func main() {
	var a attacker = &monster{200}
	attackOrDefend(a) // Prints "Attacking with damage 200"
	attackOrDefend("Hello") // This is allowed, but does nothing.

here attacker, ok := attackerDefender.(attacker) is special use case for interface type, just like try to evaluate if a dict has desired key, this will evaluate if an interface has desired func. This one single func attackOrDefend(attackerDefender interface{}) will take both possible attacker/defender’s func. Because attackerDefender interface{} is empty, it’d take any input, which also means you have to design a solution to digest unknown structed data.

JSON Marshal And Unmarshal

Usually there are two ways to manipulate json data:

Using map[string]interface{}
	var data map[string]interface{}
	err := json.Unmarshal([]byte(jsonData), &data)
	if err != nil {
		fmt.Printf("could not unmarshal json: %s\n", err)
		return
	}
	fmt.Printf("json map: %v\n", data)

output

json map: map[boolValue:true dateValue:2022-03-02T09:10:00Z intValue:1234 nullIntValue:<nil> nullStringValue:<nil> objectValue:map[arrayValue:[1 2 3 4]] stringValue:hello!]
Using known struct
type myJSON struct {
	...
	
	NullStringValue *string   `json:"nullStringValue,omitempty"`
	NullIntValue    *int      `json:"nullIntValue"`
	EmptyString     string    `json:"emptyString,omitempty"`
}
...
	var data *myJSON
	err := json.Unmarshal([]byte(jsonData), &data)
	if err != nil {
		fmt.Printf("could not unmarshal json: %s\n", err)
		return
	}

	fmt.Printf("json struct: %#v\n", data)
	fmt.Printf("dateValue: %#v\n", data.DateValue)
	fmt.Printf("objectValue: %#v\n", data.ObjectValue)

output

json struct: &main.myJSON{IntValue:1234, BoolValue:true, StringValue:"hello!", DateValue:time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC), ObjectValue:(*main.myObject)(0x1400011c180), NullStringValue:(*string)(nil), NullIntValue:(*int)(nil), EmptyString:""}
dateValue: time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC)
objectValue: &main.myObject{ArrayValue:[]int{1, 2, 3, 4}}

However there are some time the API doesn’t know exactly what will be in the response, so it provides a flexible way that just say I’ll return empty interface{}, which means any, and I don’t care. To digest this type of data, we need to treat it as regular string, then json marshal it and then can unmarshal it into struct and use it for other task.

Pointer

The pointer in GO is the most confusing part, let’s take a look at *[]MyIncident and []*MyIncident. They look the same but actually totally different. *[]MyIncident is a bunch of MyIncident in a list become []MyIncident then the whole list become a pointer. While []*MyIncident is a bunch of *MyIncident in a list and become []*MyIncident. So if I’d like to append/extend the list, which one should I use? The answer is []*MyIncident, this is because the append func works in a way like a=append(a,b), you’d want to modify the same list, *[] won’t work with append.

Another confusing part is, if we have:

type MyIncident struct {
	Incident  pagerduty.Incident
	Duplicate bool
	Alerts    []AlertDetails
	Counter   int
}

Here Incident is another list of elements. So what should we do when try to call []*MyIncident pointer? Would all elements including Incident list’s elements all become pointers? The answer is NO!. []*MyIncident only means this is a list full of *MyIncident pointer, but inside all this pointer is all regular value, not memory address. So you can safely call *MyIncident.Incident[0].