Go言語の実装テクニック

# はじめに

私は普段SREとしてAWSやEKS(Kubernetes)などのクラウドインフラの運用に従事しています。クラウド技術を支える代表的な言語としてGo言語が挙げられます。本記事では自身が学んできたGo言語のテクニックについて紹介します。

OSSのソースコードリーディングやコントリビュート、業務委託でGoを読んだり、書いたりしていますが、まともにGoをレビューしていただける環境での開発経験がないので誤りがあるかもしれません。ご了承ください。

## コンストラクタ関数

Goでは`func NewUser(name string, age int) *User`のようなメソッドがコンストラクタのような役割をします。また、レシーバを使うことでgetterである`func (u User) GetName() string`、setterである`func (u *User) SetName(name string)`が提供できます。

```golang:
package main

import "log"

type User struct {
	name string
	age int
}

func NewUser(name string, age int) *User {
	return &User{
		name: name,
		age: age,
	}
}

// 値レシーバ
func (u User) GetName() string {
	return u.name
}

// ポインタレシーバ
func (u *User) SetName(name string) {
	u.name = name
}

func main() {
	user := NewUser("John", 20)
	log.Println(user.GetName()) // John
	user.SetName("Doe")
	log.Println(user.GetName()) // Doe
}
```

## 継承(構造体の埋め込み)

Goでは構造体を埋め込むことで継承のようなものを実現できます。子構造体のフィールドにアクセスすることができます。

```golang:
package main

import (
	"log"
)

// 子構造体
type Computer struct {
	memorysize int
	cpu 	  string
}

// 親構造体
type NotePC struct {
	Computer
	battery int
	Weight int
	
}

func main() {
	notePc := NotePC{
		Computer: Computer{
			memorysize: 8,
			cpu: "intel",
		},
		battery: 5000,
		Weight: 1000,
	}
	log.Println(notePc.memorysize) // 8
}
```

## interfaceの実装漏れを防ぐ

コンストラクタ関数でinterface型を返すようにするとinterfaceの実装漏れに気づくことができます。`var _ UserService = &User{}` としてもメモリ領域を確保せず、interfaceの実装漏れを防ぐことができます。

```golang:
package main

import "log"

type UserService interface {
	Greeting() string
}

type User struct {
	Name string
	Age  int
}

// 戻り値はinterface型
func NewUser(name string, age int) UserService {
	return &User{
		Name: name,
		Age:  age,
	}
}

func (u *User) Greeting() string {
	return "Hello, " + u.Name + "!"
}

func main() {
	user := NewUser("John", 20)
	log.Println(user.Greeting())
}
```

## 依存性注入と逆転

Goでは、以下の2点を実現することで、`UserUsecase構造体 -> UserRepositoryインターフェース <- UserRepository構造体`の依存関係を作ることができ、DIとDIPを実現することができます。

- UserUsecaseコンストラクタにUserRepositoryを渡す
- UserUsecase構造体にUserReposiotryインターフェースを埋め込む

```golang:
package main

import (
	"log"
)

type User struct {
	Id int
	Name string
}

type UserReposiotry interface {
	GetUsers() ([]User, error)
}

type UserRepositoryImpl struct {}

func NewUserRepository() UserReposiotry {
	return &UserRepositoryImpl{}
}

func (ur *UserRepositoryImpl) GetUsers() ([]User, error) {
	return []User{
		{Id: 1, Name: "user1"},
		{Id: 2, Name: "user2"},
		{Id: 3, Name: "user3"},
	}, nil
}

type UserUsecaseImpl struct {
	repository UserReposiotry
}

func NewUserUsecae(repository UserReposiotry) *UserUsecaseImpl {
	return &UserUsecaseImpl{repository: repository}
}

func (uu *UserUsecaseImpl) GetUsers() ([]User, error) {
	return uu.repository.GetUsers()
}

func main() {
	repository := NewUserRepository()
    // UserUsecaseコンストラクタにUserRepositoryを渡す
	usecase := NewUserUsecae(repository)
	users, err := usecase.GetUsers()
	if err != nil {
		log.Println(err)
	}
	for _, user := range users {
		log.Println(user)
	}
}
```

## 三項演算子みたいなやつ

Goには三項演算子がありません。if文を使って以下のような書き方で実装することが多いです。

```golang:
message := "不合格です"
if point > 70 {
	message = "合格です"
}
```

## Enumみたいなやつ

GoにはEnumがありません。iotaを使うことで列挙型を実現できます。

```golang
package main

import "log"

type Color int

const (
    Red Color = iota
    Blue
    Yellow
)

func (c Color) String() string {
    switch c {
    case Red:
        return "Red"
    case Blue:
        return "Blue"
    case Yellow:
        return "Yellow"
    default:
        return "Unknown"
    }
}

func main() {
	var c Color = Red
	log.Println(c.String())  // Red
}
```

## 並行処理を行う

SQSからメッセージをポーリングし、処理するworkerの例です。goroutineを使うことで簡単に平行処理を簡単に書くことができます。

```golang:
package main

import (
	"context"
	"log"
	"sync"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/sqs"
	"github.com/aws/aws-sdk-go-v2/service/sqs/types"
)

func NewSqs() (*sqs.Client, error) {
	cfg, err := config.LoadDefaultConfig(context.TODO(),
		config.WithRegion("ap-northeast-1"),
	)
	if err != nil {
		return &sqs.Client{}, err
	}
	return sqs.NewFromConfig(cfg), nil
}

func main() {
	client, err := NewSqs()
	if err != nil {
		panic(err)
	}
	
	queueURL, err := client.GetQueueUrl(context.TODO(), &sqs.GetQueueUrlInput{
		QueueName: aws.String("sqs-name"),
	})
	if err != nil {
		panic(err)
	}

	for {
		resp, err := client.ReceiveMessage(context.TODO(), &sqs.ReceiveMessageInput{
			QueueUrl: queueURL.QueueUrl,
		})
		if err != nil {
			log.Printf("Error receiving messages: %v\n", err)
            continue
		}
  		
		var wg sync.WaitGroup
		for _, message := range resp.Messages {
			wg.Add(1)
			go func(message types.Message) {
				defer wg.Done()
				_, err := client.DeleteMessage(context.TODO(), &sqs.DeleteMessageInput{
					QueueUrl:      queueURL.QueueUrl,
					ReceiptHandle: message.ReceiptHandle,
				})
				if err != nil {
					log.Printf("Error deleting message: %v\n", err)
				}
			}(message)
		}
		wg.Wait()
	}
}
```

# おわりに

Go言語の実装テクニックについて書きました。誤りや他にもいいテクニックがあれば教えていただきたいです。