Golang Practice
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]
.