[Golang] struct(構造体)

2021-10-22 hit count image

Golangでstruct(構造体)を定義して使う方法について説明します。

概要

今回のブログポストではGolangでstruct(構造体)を使って変数を宣言して使う方法について説明します。このブログポストで紹介するコードは次のリンクで確認できます。

構造体

Golangで構造体(struct)は色んなタイプのフィールドをまとめて提供するタイプです。

構造体の役割

構造体はコードの組み合わせや依存性を低くして、凝集度を高くする役割をします。

Low coupling, high cohesion

  • 関数は関連コードをブロックにして凝集度を高くして再利用性を増加します。
  • 配列は同じタイプのデータをまとめて凝集度を高くします。
  • 構造体は関連データをまとめ、凝集度をあげ、再利用性を増加します。

この構造体はOOP(Object Oriented Programming)のベースになります。

構造体の定義

Golangでは構造体(struct)は次のように定義します。

type タイプ名 struct {
  フィールド名 タイプ
  ...
  フィールド名 タイプ
}

構造体も関数と同じようにフィールド名を使って外部に公開するフィールド(Public)と非公開フィールド(Private)を設定することができます。フィールド名は大文字で始まると外部で使うことができますが、小文字で始まる場合は外部では使うことができません。

これを確認するためmain.goファイルを生成して次のように修正します。

package main

import "fmt"

type Student struct {
  Name  string
  Class int
  No    int
}

func main() {
  var s Student
  s.Name = "Tom"
  s.Class = 1
  s.No = 1

  fmt.Println(s)
  fmt.Printf("%v\n", s)
  fmt.Printf("Name: %s, Class: %d, No: %d\n", s.Name, s.Class, s.No)
}

このように修正したプログラムを実行すると下記のような結果が表示されます。

# go run main.go
{Tom 1 1}
{Tom 1 1}
Name: Tom, Class: 1, No: 1

初期化

次のように構造体を初期化しなくて、変数を宣言する場合、全てのフィールドがフィールドタイプの基本値で設定されます。

type Student struct {
  Name  string
  Class int
  No    int
}

var s Student;

構造体で変数を生成する時、フィールドの順番で初期値を設定することができます。

var s1 Student = Student{"Tom", 1, 2}
var s2 Student = Student{
  "John",
  1,
  3,
}

または、次のようにフィールド名を指定して初期化することもできます。

var a Student = Student{ Name: "Deku", Class: 1, No: 3 };

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

type Student struct {
  Name  string
  Class int
  No    int
}

func main() {
  var s Student
  fmt.Println(s)

  var s1 Student = Student{"Tom", 1, 2}
  var s2 Student = Student{
    "John",
    1,
    3,
  }
  fmt.Println(s1)
  fmt.Println(s2)

  var s3 Student = Student{Name: "Deku", Class: 1, No: 3}
  fmt.Println(s3)
}

このように修正したコードを実行すると、次のような結果が表示されます。

# go run main.go
{ 0 0}
{Tom 1 2}
{John 1 3}
{Deku 1 3}

構造体を含めた構造体

Golangでは次のように構造体が他の構造体を含めることができます。GoalngではこれをNested structと言います。

type ClassInfo struct {
  Class int
  No int
}

type Student struct {
  Class ClassInfo
  Name string
}

このような構造体を含めた構造体は次のように初期化することができます。

var s Student = Student{
  Class: ClassInfo{Class: 1, No: 1},
  Name:  "John",
}

これを確認するため、main.goファイルを次のように修正します。

package main

import "fmt"

type ClassInfo struct {
  Class int
  No    int
}

type Student struct {
  Class ClassInfo
  Name  string
}

func main() {
  var s Student = Student{
    Class: ClassInfo{Class: 1, No: 1},
    Name:  "John",
  }

  fmt.Println(s.Class.Class)
  fmt.Println(s.Class.No)
  fmt.Println(s.Name)
}

このように修正したコードを実行すると、次のような結果が表示されます。

# go run main.go
1
1
John

Embedded field

Golangでは構造体が他の構造体を含むことができますし、次のようにフィールド名を使わなく構造体を含むこともできます。これをGolangではEmbedded fieldと言います。

type ClassInfo struct {
  Class int
  No int
}

type Student struct {
  ClassInfo
  Name string
}

このように宣言されたEmbedded Fieldは次のように初期化することができます。

var s Student = Student{
  ClassInfo: ClassInfo{Class: 1, No: 1},
  Name:      "John",
}

このようなEmbedded Fieldは次のように直接アクセスすることができます。

fmt.Println(s.Class)
fmt.Println(s.No)
fmt.Println(s.Name)

Embedded Fieldは次のように現在構造体のフィールド名と重複されるフィールドを使うことができます。

type ClassInfo struct {
  Class int
  No int
}

type Student struct {
  ClassInfo
  Name string
  No int
}

このように重複されたフィールド名は次のように初期化することができます。

var s1 DupStudent = DupStudent{
  ClassInfo: ClassInfo{Class: 1, No: 1},
  Name:      "John",
  No:        10,
}

フィールド名が重複されたので、次のように重複されたフィールドを直接アクセスすると、現在の構造体の値がリターンされます。

fmt.Println(s1.No) // 10

そしたら、以前と同じようにEmbedded Fieldのフィールドにアクセスしたい場合は、次のようにアクセスすることができます。

fmt.Println(s1.ClassInfo.No) // 1

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

type ClassInfo struct {
  Class int
  No    int
}

type Student struct {
  ClassInfo
  Name string
}

type DupStudent struct {
  ClassInfo
  Name string
  No   int
}

func main() {
  var s Student = Student{
    ClassInfo: ClassInfo{Class: 1, No: 1},
    Name:      "John",
  }

  fmt.Println(s.Class)
  fmt.Println(s.No)
  fmt.Println(s.Name)

  var s1 DupStudent = DupStudent{
    ClassInfo: ClassInfo{Class: 1, No: 1},
    Name:      "John",
    No:        10,
  }

  fmt.Println(s1.Class)
  fmt.Println(s1.No)
  fmt.Println(s1.Name)
  fmt.Println(s1.ClassInfo.No)
}

このように修正したファイルを実行すると次のような結果が表示されます。

# go run main.go
1
1
John
1
10
John
1

メモリソート

Golangで普通はプログラミングする時、メモリをあまり気にしなくてもいいです。(気にするべきですが)しかし、皆さんがメモリが少ないデバイスまたはメモリを効率的使わなければならないプログラムを作成する場合、構造体のメモリソート(Memory Alignment)について考える必要があります。

s := Student{"John", 1}
var str string = "John"
var i int = 1

fmt.Println(unsafe.Sizeof(s))
fmt.Println(unsafe.Sizeof(str))
fmt.Println(unsafe.Sizeof(i))

GolangはCPUが計算しやすくするため、構造体を8の倍数でソートしてメモリに保存します。

24
16
8

もし、フィールドのタイプが8の倍数より小さい場合、Golangは空きのスペース(Memory Padding)を追加して8の倍数に作って保存します。

type Memory struct {
  A int8 // 1 バイト
  B int // 8 バイト
  C int8 // 1 バイト
  D int // 8 バイト
  E int8 // 1 バイト
  // 19 バイト
}

上のような構造体は変数のタイプだけ考えると、19バイトのメモリを使います。しかし、構造体は8の倍数でソートされるので、A, C, Eには空きのスペースが追加され8バイトで計算されますので、実際のメモリは40バイトになります。

このような空きのスペース(Memory Padding)の追加で無駄なメモリが多くなることを避けるため、次のように小さいメモリをまとめて先に宣言してメモリソートを実行することができます。

type Memory struct {
  A int8 // 1 バイト
  C int8 // 1 バイト
  E int8 // 1 バイト
  B int // 8 バイト
  D int // 8 バイト
  // 19 バイト
}

同じように19バイトが実際使うメモリですが、A, C, Eが一緒に最初宣言されたので、3バイトの実際メモリと5バイトの空欄が追加され8バイトで計算されます。したがって、実際のメモリは24バイトになります。

このようにGolangはCPUの計算を効率的するため8の倍数でソートをしますし、これによる無駄なメモリを最小化するためには上のようにメモリソートを実行した方が良いです。

これを確認するためmain.goファイルを開いて次のように修正します。

package main

import (
  "fmt"
  "unsafe"
)

type Student struct {
  Name  string
  Class int
}

type Memory struct {
  A int8
  B int
  C int8
  D int
  E int8
}

type MemoryAlignment struct {
  A int8
  C int8
  E int8
  B int
  D int
}

func main() {
  s := Student{"John", 1}
  var str string = "John"
  var i int = 1

  fmt.Println(unsafe.Sizeof(s))
  fmt.Println(unsafe.Sizeof(str))
  fmt.Println(unsafe.Sizeof(i))

  m := Memory{1, 2, 3, 4, 5}
  fmt.Println(unsafe.Sizeof(m))

  ma := MemoryAlignment{1, 2, 3, 4, 5}
  fmt.Println(unsafe.Sizeof(ma))
}

このように修正したコードを実行すると次のような結果が表示されます。

# go run main.go
24
16
8
40
24

完了

これでGolangで構造体(struct)を定義して使う方法についてみてみました。また、構造体のメモリについて確認して、メモリソートを使ってもっと効率的メモリを支えることが分かりました。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

アプリ広報

今見てるブログを作成たDekuが開発したアプリを使ってみてください。
Dekuが開発したアプリはFlutterで開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。

Posts