個人メモ〜Go言語入門(少し目的のあるコードを書く編)


別記事で基礎的な文法を調べ、Hello Worldまで行った。
[個人メモ〜Go言語入門(文法と環境構築編)](https://qiita.com/PG-practice/items/42f17aa2ad43e473b033)

今回の記事ではHello Worldの次のステップとして少しコードを書いてみた。
以下を順に見ていく。
- 同一パッケージ内で複数のファイルを作成する
- 別のパッケージを作成する
- HTTPリクエスト
- AWS SDK for Go V2をインストールして、Lambda関数を呼び出す

この記事でやることを理解するために調べた内容が上記の記事にメモってあるので、この記事の内容は基本的に上記記事も見ればわかるはず。

## 前提
環境構築してhello worldまでできている。
`go mod init hello` している。
```:go.mod
module hello

go 1.22.5
```


## 同一パッケージ内で複数のファイルを作成する
以下の構成で、`hello.go`から`hello2.go`のメソッドを呼び出す。

構成
```
.
├── go.mod
├── hello.go
├── hello2.go
```

```go:hello.go
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Printf("Hello, World! %s\n", os.Args[1])
    // 同一パッケージなのでimportなしで参照できる
	Hello2()
}
```

```go:hello2.go
package main

import (
	"fmt"
)

func Hello2() {
	fmt.Printf("Hello, World! 2\n")
}
```

`go run`はどちらのファイルも指定しないといけないようだ
```
go run hello.go hello2.go tarou
```

出力
```
Hello, World! tarou
Hello, World! 2
```

以下のあたりがポイントか。
- 同一パッケージなのでimportの必要はない
- `hello2.go`の方ではメソッドをexportするため、`Hello2()`の1文字目は大文字にする必要がある
- 実行時に全てのファイルを指定する(ビルドさえすれば大丈夫なのか、実用的にはどうしているんだろう)

## 別のパッケージを作成する
構成

```
.
├── go.mod
├── hello.go
└── mymodule
    └── hello3.go
```

```go:hello.go
package main

import (
	"fmt"
	"hello/mymodule"
	"os"
)

func main() {
	fmt.Printf("Hello, World! %s\n", os.Args[1])
	mymodule.Hello3()

}
```

```go:hello3.go
package mymodule

import (
	"fmt"
)

func Hello3() {
	fmt.Printf("Hello, World! 3\n")
}

```

- ファイル構造の問題もあるかもだが、importのパスが`<呼び出し側のモジュール名(hello)>/<呼び出される側のモジュール名(mymodule)>`だったのがやや見つけにくかった。

## HTTPリクエスト
標準のモジュールを使う。

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"net/http/httputil"
	"time"
)

type Client struct {
	url string
}

const url = "https://httpbin.org/anything"
const data = "data to send"

func (c *Client) httpreq() (*http.Response, error) {
	req, err := http.NewRequest("POST", c.url, bytes.NewBufferString(data))
	if err != nil {
		panic(err)

	}

	req.Header.Set("testkey", "testvalue")

	client := &http.Client{
		Timeout: 2 * time.Second,
	}

	// リクエスト内容を確認できる
	dump, _ := httputil.DumpRequest(req, true)
	fmt.Println("==httputil.DumpRequestの出力==\n", string(dump))
	fmt.Printf("==clientの出力==\n%+v\n", client)

	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}

	defer resp.Body.Close()
	fmt.Printf("Status: %s\n", resp.Status)
	fmt.Printf("Status Code: %d\n", resp.StatusCode)
	b, err := io.ReadAll(resp.Body)
	fmt.Printf(string(b))
	return resp, err
}

func main() {
	fmt.Printf("Post Start!\n")

	c := Client{url: url}
	res, err := c.httpreq()

	if err != nil {
		fmt.Println(err)
	}
	if res != nil {
		fmt.Println("main end. response is: ", res)
	}
}
```

httpbinに送らせてもらっている。出力はこう。オブジェクト部分がhttpbinからのレスポンスで、それをそのまま表示している。
```
Post Start!
POST /anything HTTP/1.1
==httputil.DumpRequestの出力==
 POST /anything HTTP/1.1
Host: httpbin.org
Testkey: testvalue

data to send
==clientの出力==
&{Transport:<nil> CheckRedirect:<nil> Jar:<nil> Timeout:2s}
Status: 200 OK
Status Code: 200
{
  "args": {}, 
  "data": "data to send", 
...
  "headers": {
...
    "Host": "httpbin.org", 
    "Testkey": "testvalue", 
  }, 
  "json": null, 
  "method": "POST", 
  "url": "https://httpbin.org/anything"
}
main end. response is:  &{200 OK 200 HTTP/2.0 2 0 map[Access-Control-Allow-Credentials:[true] Access-Control-Allow-Origin:[*] Content-Length:[431] Content-Type:[application/json] Date:[Sun, 14 Jul 2024 11:51:43 GMT] Server:[gunicorn/19.9.0]] 0xc000058340 431 [] false false map[] 0xc00008a5a0 0xc000084420}
```


## AWS SDK for Go V2をインストールして、Lambda関数を呼び出す
やはり外部ライブラリのインストールまでは試したいところ。
[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/code-library/latest/ug/go_2_lambda_code_examples.html)にラッパーが書いてあったのだが、不慣れだったため意外にこれを呼び出すのに苦戦した。

ドキュメントに従いインストール
https://aws.github.io/aws-sdk-go-v2/docs/getting-started/

```bash
go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/config
# あとは使用するサービスごとにもインストールが必要
go get github.com/aws/aws-sdk-go-v2/service/lambda
```
`go.mod`にrequireのセクションが追加される
```
module hello

go 1.22.5

require (
        github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
        ...
```

コード

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

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

// Lambda呼び出し時のペイロード
var payload = map[string]any{"key1": "val1", "key2": "val2"}

func main() {
	//fmt.Printf("Hello, World!\n")

	sdkConfig, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("ap-northeast-1"))
	if err != nil {
		fmt.Println("Couldn't load default configuration. Have you set up your AWS account?")
		fmt.Println(err)
		return
	}

	lambdaClient := lambda.NewFromConfig(sdkConfig)
	wrappedLambdaClient := FunctionWrapper{LambdaClient: lambdaClient}

	output := wrappedLambdaClient.Invoke("<Lambda関数名>", payload, false)
	fmt.Println(string((*output).Payload))
	//コンパイラがよしなにやってくれて同じ結果になる
	// fmt.Println(string(output.Payload))

	// 呼び出し後のresponseをオブジェクトとして扱うために、JSONオブジェクトの代わりのmapを定義
	var lambdaBody map[string]interface{}

	if err := json.Unmarshal(output.Payload, &lambdaBody); err != nil {
		fmt.Println(err)
	}
	// Lambdaの返り値は {"status":200,"body":"hello!"}のような形式としておく
	fmt.Println(lambdaBody["body"])

}

//以下は公式のドキュメントにあるラッパーそのまま
//https://docs.aws.amazon.com/ja_jp/code-library/latest/ug/go_2_lambda_code_examples.html

// FunctionWrapper encapsulates function actions used in the examples.
// It contains an AWS Lambda service client that is used to perform user actions.
type FunctionWrapper struct {
	LambdaClient *lambda.Client
}

// Invoke invokes the Lambda function specified by functionName, passing the parameters
// as a JSON payload. When getLog is true, types.LogTypeTail is specified, which tells
// Lambda to include the last few log lines in the returned result.
func (wrapper FunctionWrapper) Invoke(functionName string, parameters any, getLog bool) *lambda.InvokeOutput {
	logType := types.LogTypeNone
	if getLog {
		logType = types.LogTypeTail
	}
	payload, err := json.Marshal(parameters)
	if err != nil {
		log.Panicf("Couldn't marshal parameters to JSON. Here's why %v\n", err)
	}
	invokeOutput, err := wrapper.LambdaClient.Invoke(context.TODO(), &lambda.InvokeInput{
		FunctionName: aws.String(functionName),
		LogType:      logType,
		Payload:      payload,
	})
	if err != nil {
		log.Panicf("Couldn't invoke function %v. Here's why: %v\n", functionName, err)
	}
	return invokeOutput
}
```

Lambda関数の戻り値は以下の形式にしている。
```
{
    "statusCode": 200,
    "body": "Hello From Lambda",
    "headers": {
        "header1": "value1"
    }
}
```

Goプログラム実行の出力
```
{"statusCode":200,"body":"Hello From Lambda","headers":{"header1":"value1"}}
Hello From Lambda
```

ポイント
- Lambda関数呼び出し時のペイロードを構造体を定義せずにオブジェクトっぽく渡している
- Lambda関数の出力もオブジェクトっぽく扱うために`map[string]interface{}`を使っている
- `fmt.Println(string((*output).Payload))`の部分は、`output`はポインタだが`*`をつけなくてもコンパイラが勝手にいい感じにしてくれるので同じ出力になる