Goでポインタを使うのか値を使うのかで迷ったこと

2024/02/23に公開

はじめに

タイトルの通りです。
実際の業務の中で、ポインタの使いをなんとなくでやってたなとおもい、まとめてみました。
また、ケースの紹介の箇所はプロジェクトのルールや方針等もあると思うので、
あくまで私の理解と私の経験だと理解してください。

ポインタって?

ポインタはメモリのアドレスを指します。
ポインタは「値がどこにあるのか」を示すもので、値自体ではありません。
Go言語では以下のようにポインタを使用できます。

package main

import "fmt"

type Fruits struct {
	Name  string
	Color string
}

func main() {
	// Fruitsをポインタ型でappleに使用する
	apple := &Fruits{
		Name:  "りんご",
		Color: "red",
	}


	fmt.Printf("apple :%+v\n", apple)
	fmt.Printf("appleに格納されているアドレス :%p\n", apple)
}

実行結果

apple :&{Name:りんご Color:red}
変数appleに格納されているアドレス :0xc00011c000

上記のように構造体のフィールドと値を見ることができました。
ポインタの詳しい解説はここでは割愛します。

ポインタのスライスの特徴

ポインタのスライスの特徴は以下の通りです。

  • メモリ効率が良い
    • 要素自体のコピーではなく、そのアドレスを格納
    • そのため、大きなオブジェクトや構造体を扱う際にメモリを節約できる
  • データの一貫性を保つことができる
    • 複数の場所(変数や関数)からアクセスして操作ができる

ポインタのスライスを使ったコードは下記の通りです。

package main

import "fmt"

type Fruit struct {
	Name  string
	Color string
}

func main() {
	// Fruit構造体のインスタンスへのポインタのスライスを初期化
	fruits := []*Fruit{
		&Fruit{Name: "Apple", Color: "Red"},
		&Fruit{Name: "Banana", Color: "Yellow"},
	}

	// スライスを他の変数に代入してもデータの一貫性が保たれており、どちらも変更になる
	fmt.Printf("代入前のリンゴ%+v\n", *fruits[0])
	apple := fruits[0]
	fmt.Printf("代入されたリンゴ%+v\n", *fruits[0])

	// 値を変更して中身を確認する
	apple.Name = "Green Apple"
	fmt.Printf("appleはGreen Appleになってる?:%+v\n", *apple)
	fmt.Printf("代入元のfruits[0]はどうなっている?:%+v\n", *fruits[0])
}

実行結果

代入前のリンゴ{Name:Apple Color:Red}
代入されたリンゴ{Name:Apple Color:Red}
appleはGreen Appleになってる?:{Name:Green Apple Color:Red}
代入元のfruits[0]はどうなっている?:{Name:Green Apple Color:Red}

値のスライスの特徴

値のスライスの特徴は以下の通りです。
言ってしまえば通常のスライスなんですが、一応まとめます。

  • データがコピーできる
    • ポインタと違いデータをそのまま渡しているので、別のデータとして存在させることができる
    • つまり別扱いにできるので、独立性を担保することができる
  • メモリ使用量は少し高い
    • ポインタのメリット分、こちらではこうなる

値のスライスを使ったコードは下記の通りです。

package main

import "fmt"

type Fruit struct {
	Name  string
	Color string
}

func main() {
	// Fruit構造体のインスタンスのスライスを初期化(値のスライス)
	fruits := []Fruit{
		{Name: "Apple", Color: "Red"},
		{Name: "Banana", Color: "Yellow"},
	}

	// スライスを他の変数に代入しても、それぞれが独立したデータとして存在する
	fmt.Printf("代入前のリンゴ %+v\n", fruits[0])
	apple := fruits[0] // ここでAppleの値をコピー
	fmt.Printf("コピーされたリンゴ %+v\n", apple)

	// コピーした値を変更して中身を確認する
	apple.Name = "Green Apple"
	fmt.Printf("コピーされたappleはGreen Appleになってる?:%+v\n", apple)
	fmt.Printf("代入元のfruits[0]はどうなっている?:%+v\n", fruits[0])
}

実行結果

代入前のリンゴ {Name:Apple Color:Red}
コピーされたリンゴ {Name:Apple Color:Red}
コピーされたappleはGreen Appleになってる?:{Name:Green Apple Color:Red}
代入元のfruits[0]はどうなっている?:{Name:Apple Color:Red}

この挙動は他の言語等でもあることなので、イメージしやすいのではないかと思います。

この時、どっちを使えばいいの?

実際に業務で携わるような処理を書く際に迷ったりしたことがあったので、
自分なりにこの場合はこちらがベター、というのをまとめました。
※厳密にはその状況次第だと思いますが、細かいことはここでは割愛します

前提

ざっくりですが、ファイルの構成は以下のようになっているとします。

  • handler
    • リクエスト時に初めに呼ばれるファイル
    • リクエストのパラメータをusecaseに渡す
  • usecase
    • ビジネスロジックが記載されており、データを取得したり加工したりする
      • 具体的なデータの操作処理はrepositoryに渡す
    • 最終的なレスポンスに当たる部分のデータを成形してhandlerに返却する
  • repository
    • データの保存や更新処理などを担う
    • 基本的にusecaseとやりとりをする
    • 今回はDBなどを使わないので、ダミーの関数を用いて一定の確率でエラーが出る状況にする
      • つまりエラーのハンドリングが必要

本来はファイルなども別で管理すべきですが、今回は説明のため単一のファイルにまとめることとします。
かつ、レスポンスで返却する値はfmtで出力するものとします。

ケース1:少量のデータをレスポンスで返却する

このケースでは以下のjsonデータを返却する要件があったとします。

{
    "LastGetDate": "2009-11-10 23:00:00",
    "Fruits": [
        {
            "Name": "apple",
            "Color": "red"
        },
        {
            "Name": "banana",
            "Color": "yellow"
        },
        {
            "Name": "orange",
            "Color": "orange"
        }
    ]
}

かつデータは以下のようにしか持っていないので加工が必要だとします。

type Fruit struct {
	Name  string
	Color string
}

var apple = Fruit{
	Name:  "Apple",
	Color: "Red",
}
var banana = Fruit{
	Name:  "banana",
	Color: "yellow",
}
var orange = Fruit{
	Name:  "orange",
	Color: "orange",
}

この場合、handler/usecase/repositoryはポインタのスライスを返すべきなのか?
値を返すべきなのか?

結論

以下のようになるかと考えています。

  • handler:
    • usecaseにエラーがあった場合だけ処理を中断。レスポンスを基本的に返せばいい
    • それがなんなのかを意識しなくていい
  • usecase:
    • データ取得処理やusecase内で何かしらのエラーが発生する可能性がある
    • エラーを返す場合はレスポンスの値が見られることはないので、メモリの効率も考えてnilで良いのでポインタでいい
  • repository
    • 今回はデータの更新等もないのでポインタで返す必要はない
    • ポインタでも動くが、「更新とかしたいの?」となんらかの意図があるように見えるので、ポインタでなくていい

以下コードです。

package main

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"time"
)

type Fruit struct {
	Name  string `json:"Name"`
	Color string `json:"Color"`
}

type Response struct {
	LastGetDate string  `json:"LastGetDate"`
	Fruits      []Fruit `json:"Fruits"`
}

// データを値として保持
var apple = Fruit{Name: "Apple", Color: "Red"}
var banana = Fruit{Name: "banana", Color: "yellow"}
var orange = Fruit{Name: "orange", Color: "orange"}

func dummyOccurError() error {
	// 10000万分の1の確率でエラーを発生させる
	rand.Seed(time.Now().UnixNano())
	if rand.Intn(10000000) == 0 {
		return fmt.Errorf("random error occurred")
	}
	return nil
}

func Repository() ([]Fruit, error) {
	if err := dummyOccurError(); err != nil {
		return nil, err
	}
	return []Fruit{apple, banana, orange}, nil
}

func Usecase() (*Response, error) {
	fruits, err := Repository()
	if err != nil {
		return nil, err
	}
	currentTime := time.Now().Format("2006-01-02 15:04:05")
	return &Response{
		LastGetDate: currentTime,
		Fruits:      fruits,
	}, nil
}

// Handlerはエラーチェックを行い、適切にレスポンスを生成
func Handler() {
	response, err := Usecase()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	responseJSON, err := json.MarshalIndent(response, "", "    ")
	if err != nil {
		fmt.Println("Error marshaling response:", err)
		return
	}
	fmt.Println(string(responseJSON))
}

func main() {
	Handler()
}

ケース2:リクエストされた値を使ってデータを更新する

このケースでは以下のリクエストを受けてデータを更新するものとします。
DB処理等はできないのでサンプル的なコードになっているのはご了承ください。
リクエスト

{
    "name": "apple",
    "color": "Green" 
}

レスポンスはなし、HTTPコードで200が返却されることを想定します。
リクエストについては実際には動かせないので、上記パラメータがリクエストされたものと
仮定してコードを作成します。
このとき、各処理(handler/usecase/repository)はどういう定義で責務を分けるべきでしょうか。

結論

以下のようになると考えています。

  • ハンドラ:
    • リクエストの値をユースケースに渡す
    • ユースケースからはエラーだけを受け取り、エラーがあった場合だけエラーとして処理を終了する
  • ユースケース:
    • 引数がないことはないので値型でハンドラからデータを受けとる
    • 今回は成功失敗の2択なのでその状況でエラーを返す
  • リポジトリ:
    • 取得に関して:
      • データがないことはありえないので、値型で引数を受け取り、更新されることも加味してポインタ型でデータを返却する
    • 更新に関して
      • メモリを節約できるため、ポインタで引数を受け取りデータを更新
      • 本来はDB絡みの更新エラーの発生の可能性もあるのでエラーは見つつも問題なければnilとして返却する
      • 更新されたデータはポインタなので、呼び出した元で同じ値を使えるはず

イメージのコードです。

package main

import (
	"fmt"
	"math/rand"
	"time"
)

type Fruit struct {
	Name  string
	Color string
}

var fruits = []Fruit{
	{Name: "Apple", Color: "Red"},
	{Name: "Banana", Color: "Yellow"},
	{Name: "Orange", Color: "Orange"},
}

// リクエストの値をダミーで定義
var requestFruitName = "Apple"
var requestFruitColor = "Green"

// サンプル用にエラーを誘発(10000分の1)
func dummyOccurError() error {
	rand.Seed(time.Now().UnixNano())
	if rand.Intn(10000000) == 0 {
		return fmt.Errorf("random error occurred")
	}
	return nil
}

// データの取得
func FetchFruitByName(name string) (*Fruit, error) {
	if err := dummyOccurError(); err != nil {
		return nil, err
	}

	for i, fruit := range fruits {
		if fruit.Name == name {
			return &fruits[i], nil
		}
	}
	return nil, fmt.Errorf("fruit not found")
}

// データ更新
func UpdateFruitColor(fruit *Fruit, newColor string) error {
	if err := dummyOccurError(); err != nil {
		return err
	}
	fruit.Color = newColor
	return nil
}

func Usecase(fruitName, newColor string) error {
	fruit, err := FetchFruitByName(fruitName)
	if err != nil {
		return err
	}
	err = UpdateFruitColor(fruit, newColor)
	if err != nil {
		return err
	}
	return nil
}

func Handler() {
	err := Usecase(requestFruitName, requestFruitColor)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	fmt.Println("Fruit updated successfully")
}

func main() {
	Handler()
}

実行結果

Fruit updated successfully

ケース3:リクエストされた値を使ってデータを更新する(複数)

このケースでは以下のリクエストを受けてデータを更新するものとします。
DB処理等はできないのでサンプル的なコードになっているのはご了承ください。
リクエスト

{
	"UpdateFruits": [
		{
			"Name": "Apple",
			"IsDomestic": false
		},
		{
			"Name": "Banana",
			"IsDomestic": true
		},
		{
			"Name": "Orange",
			"IsDomestic": false
		}
	]
}

レスポンスはなし、HTTPコードで200が返却されることを想定します。
このとき、各処理(handler/usecase/repository)はどういう定義で責務を分けるべきでしょうか。

結論

以下のようになると考えています。

  • ハンドラ:
    • リクエストの値をユースケースに渡す
    • ユースケースからはエラーだけを受け取り、エラーがあった場合だけエラーとして処理を終了する
  • ユースケース:
    • 引数がないことはないので値型でハンドラからデータを受けとる
    • 今回は成功失敗の2択なのでその状況でエラーを返す
  • リポジトリ:
    • 取得に関して:
      • 今回は更新することを想定しているので、ポインタで取得
      • ポインタでの定義は乱用すると、何か意図があるように見えてしまうが、このシチュエーションであれば問題ないと思う
    • 更新に関して
      • メモリを節約できるため、ポインタで引数を受け取りデータを更新
      • 本来はDB絡みの更新エラーの発生の可能性もあるのでエラーは見つつも問題なければnilとして返却する

下記コードはかなり冗長ですが、上記ケースを実現するとこんな感じもあるかなと思います。

package main

import (
	"fmt"
	"math/rand"
)

type Fruit struct {
	Name       string
	IsDomestic bool
}

// ダミーのリクエストデータ
var requestUpdateFruits = []Fruit{
	{Name: "Apple", IsDomestic: false},
	{Name: "Banana", IsDomestic: true},
	{Name: "Orange", IsDomestic: false},
}

var fruits = []*Fruit{
	{Name: "Apple", IsDomestic: false},
	{Name: "Banana", IsDomestic: false},
	{Name: "Orange", IsDomestic: false},
}

// ダミーのエラー発生関数
func dummyOccurError() error {
	// 100分の1の確率でエラーを発生させる
	if rand.Intn(100) == 0 {
		return fmt.Errorf("a random error occurred")
	}
	return nil
}

// Repository: 全データ取得
func FetchFruits() ([]*Fruit, error) {
	if err := dummyOccurError(); err != nil {
		return nil, err
	}
	return fruits, nil
}

// Repository: フルーツのIsDomesticフラグを更新
func UpdateFruit(fruit *Fruit, isDomestic bool) error {
	if err := dummyOccurError(); err != nil {
		return err
	}
	fruit.IsDomestic = isDomestic
	return nil
}

// Usecase: リクエストされたフルーツのIsDomesticフラグを更新
func Usecase() error {
	fetchedFruits, err := FetchFruits()
	if err != nil {
		return err
	}

	for _, reqFruit := range requestUpdateFruits {
		for _, fruit := range fetchedFruits {
			if fruit.Name == reqFruit.Name {
				err := UpdateFruit(fruit, reqFruit.IsDomestic)
				if err != nil {
					return err
				}
				break
			}
		}
	}
	return nil
}

// Handler: ユーザーのリクエストを処理
func Handler() {
	if err := Usecase(); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	fmt.Println("Fruits updated successfully")
}

func main() {
	Handler()
}

実行結果

Fruits updated successfully

終わりに

ポインタにもメリットやデメリットがあり、ちゃんと使えば非常にパフォーマンス・視認性・保守性が高い実装ができると思います。もっと勉強していきます。
誤ってる箇所などあればご指摘ください。

Discussion