Tuesday, March 5, 2024

Golang - Struct

 

Struct



Unlike 'Slice' and 'Map', 'Struct' allows us to define our own type that holds different types of data. It is similar to a class in other object-oriented programming languages. We can store relevant data about a single concept in a single struct type.


Take the following example: person-related data such as 'name', 'age', and 'isMale' can be compiled into a single 'person' structure.



  type person struct {
    name   string
    age    int
    isMale bool
  }


Struct type is similar to a blueprint and can be used to build struct values.
Struct values resemble instances or objects in other object-oriented programming language.



  p1 := person{
    name:   "Jake",
    age:    10,
    isMale: true,
  }

  p2 := person{
    name:   "Mary",
    age:    20,
    isMale: false,
  }



More details



1. Structs fields are fixed at compile-time.


2. The zero value of 'struct' will be a struct value with all of its fields, each of which will have the zero value of its field type.

Ex:

type person struct {
    name   string
    age    int
    isMale bool
  }

  var p1 person

  fmt.Printf("P1: %#v\n", p1)


Result:
            
P1: main.person{name:"", age:0, isMale:false}


3. We do not need to provide all of the field values when initializing.

Ex:

  type person struct {
    name   string
    age    int
    isMale bool
  }

  p1 := person{
    name:   "Jake",
    isMale: true,
  }


4. We can also omit the field name when initializing, and Go will automatically initialize them by matching the field ordering to the values. However, it is not recommended.

Ex:

type person struct {
    name   string
    age    int
    isMale bool
  }

  p2 := person{
    "Mary",
    20,
    false,
  }


5. Struct, unlike Slice and Map, are bare values (similar to Array). So, when you copy one struct value to another, the changes in one will not effect the other.

Ex:

  type person struct {
    name   string
    age    int
    isMale bool
  }

  p1 := person{
    name:   "Jake",
    age:    10,
    isMale: true,
  }

  // Copy
  p2 := p1

  p2.name = "Mary"

  fmt.Printf("P1: %#v\n", p1)
  fmt.Printf("P2: %#v\n", p2)


Result:
            
  P1: main.person{name:"Jake", age:10, isMale:true}
  P2: main.person{name:"Mary", age:10, isMale:true}


6. If all of the struct fields are comparable, it can be compared using the equal operator.

Ex1:

  type person struct {
    name   string
    age    int
    isMale bool
  }

  p1 := person{
    name:   "Jake",
    age:    10,
    isMale: true,
  }

  // Copy
  p2 := p1

  // p2.name = "Mary"

  if p1 == p2 {
    fmt.Println("p1 and p2 are the same")
  } else {
    fmt.Println("p1 and p2 are not the same")
  }


Ex2:

  type person struct {
    name     string
    age      int
    isMale   bool
    nikename []string
  }

  p1 := person{
    name:   "Jake",
    age:    10,
    isMale: true,
  }

  // Copy
  p2 := p1

  if p1 == p2 {
    fmt.Println("p1 and p2 are the same")
  } else {
    fmt.Println("p1 and p2 are not the same")
  }


Result:
            
invalid operation: p1 == p2 (struct containing []string cannot be compared)



Embedding (Object-Oriented Composition)



Before introducing 'struct embedding', let us compare inheritance and composition of OOP. Refer to this article.

Inheritance is the 'is-a' relationship.


Take the following as an example, MountainBike is-a Bicycle; RoadBike is-a Bicycle. So they are all the same type 'Bicycle'.

Ex (Java):

  public class Bicycle {
    // the Bicycle class has three fields
    public int cadence;
    public int gear;
    public int speed;
  }

  public class MountainBike extends Bicycle {
    // the MountainBike subclass adds one field
    public int seatHeight;
  }

  public class RoadBike extends Bicycle {
    // the RoadBike subclass adds one field
    public string color;
  }


Composition is the 'has-a' relationship. 


Take the following as an example, Car has Engine; Motorcycle has Engine. So they all have their own types.

Ex (Java):

  public class Engine {
    public string type;
  }

  public class Car {
    //composition has-a relationship
    public Engine engine;

    public bool isAWD;
  }

  public class Motocycle {
    //composition has-a relationship
    public Engine engine;

    public string gloveColor;
  }


Go does not use 'Inheritance'. Go use 'Composition' and call it 'Embedding'.

Ex:

  type engine struct {
    engineType string
  }

  type car struct {
    // embedding field
    engine engine

    isAWD bool
  }

  type motorcycle struct {
    // embedding field
    engine engine

    gloveColor string
  }

  myCar := car{
    engine: engine{
        engineType: "V4",
    },
    isAWD: true,
  }

  myMotorcycle := motorcycle{
    engine: engine{
        engineType: "V1",
    },
    gloveColor: "red",
  }

  fmt.Printf("myCar: %#v\n", myCar)
  fmt.Printf("myMotorcycle: %#v\n", myMotorcycle)

  // How to get the field value from the embedding field
  fmt.Printf("myCar.engine: %#v\n", myCar.engine.engineType)


A struct field without a name is called an anonymous field.

When getting one field value of the anonymous composition field, we can omit the anonymous composition field name.

Ex:

  type engine struct {
    engineType string
  }

  type car struct {
    // embedding field
    engine engine

    isAWD bool
  }

  type motorcycle struct {
    // anonymous embedding field
    engine

    gloveColor string
  }

  myCar := car{
    engine: engine{
        engineType: "V4",
    },
    isAWD: true,
  }

  myMotorcycle := motorcycle{
    engine: engine{
        engineType: "V2",
    },
    gloveColor: "red",
  }

  fmt.Printf("myCar: %#v\n", myCar)
  fmt.Printf("myMotorcycle: %#v\n", myMotorcycle)

  // How to get the field value from the embedding field
  fmt.Printf("myCar.engine: %#v\n", myCar.engine.engineType)

  // How to get the field value from the anonymous embedding field
  fmt.Printf("myMotorcycle.engine: %#v\n", myMotorcycle.engine.engineType)
  fmt.Printf("myMotorcycle.engine: %#v\n", myMotorcycle.engineType)


How about the name of struct fields are the same with the one from the anonymous composition field (conflict)? Then we cannot omit the embedding field name.

Ex:

  type engine struct {
    engineType string
    // duplicate field name
    title string
  }

  type car struct {
    // anonymous embedding field
    engine engine

    isAWD bool
    // duplicate field name
    title string
  }

  myCar := car{
    engine: engine{
      engineType: "V4",
      title:      "exp1",
    },
    isAWD: true,
    title: "exp2",
  }

  fmt.Printf("myCar: %#v\n", myCar)

  // How to get the field value from the embedding field
  fmt.Printf("myCar.engine: %#v\n", myCar.engine.title)
  fmt.Printf("myCar.engine: %#v\n", myCar.title)



JSON Encoding and Decoding



What is JSON?

* JSON stands for JavaScript Object Notation
* JSON is a lightweight data-interchange format

The 'encoding/json' package in Go allows us to encode (marshal) or decode (unmarshal) data.

Only struct fields beginning with an uppercase letter can be exported by the json package.

Warning:
            
struct field "xxx" has json tag but is not exported


Field tags are part of a struct's type. Thus, they are compile-time constants.

Field tags associate a static string metatdata to a field.

Use 'Marshal' or 'MarshalIndent' to encode data.

Ex:

type permissions map[string]bool

type user struct {
    Name        string      `json:"username"`
    Password    string      `json:"-"`
    Permissions permissions `json:"perm,omitempty"`
}

func main() {
    users := []user{
      {
        "Frank", "1234", nil,
      },
      {
        "Jake", "4321", permissions{
          "admin": true,
        },
      },
    }

    out, err := json.MarshalIndent(users, "", "\t")
    if err != nil {
      fmt.Println("err:", err)
      return
    }

    fmt.Println(string(out))
}


Result:
            
  [
    {
      "name": "Frank"
    },
    {
      "name": "Jake",
      "perm": {
        "admin": true
    }
    }
  ]


Let us investigate the 'json:"perm,omitempty"' in the case above.

    json: 
        It is the key name that allows the JSON package to read this tag.

    "perm,omitempty"
        Is the value of the json tag, which is encased in double quotes and separated by commas.

Use 'Unmarshal' to decode the json string to struct, slice, or map.

Ex:

type permissions map[string]bool

type user struct {
    Name        string      `json:"username"`
    Permissions permissions `json:"perm"`
}

func main() {
    jsonStr := []byte{}

    scanner := bufio.NewScanner(os.Stdin)

    for scanner.Scan() {
      jsonStr = append(jsonStr, scanner.Bytes()...)
    }

    users := []user{}

    json.Unmarshal(jsonStr, &users)
}


No comments:

Post a Comment