20210117のGoに関する記事は9件です。

Goでテストを書くために、まずinterfaceを設計するということ

Goでテストコードを書くことになった場合に

あなたが、勤務先で上長から「テストコードを書いてください」と指示されたとき、
テストが書ける設計になっているでしょうか。

この記事では、S3からファイルを取得して読み取るというユースケースに対して
テストを書くことについて考えていきます。

テストが書きづらいケース

main.go
var awssess = newSession()
var s3Client = newS3Client()

const defaultRegion = "ap-northeast-1"

func newSession() *session.Session {
    return session.Must(session.NewSessionWithOptions(session.Options{
        SharedConfigState: session.SharedConfigEnable,
    }))
}

func newS3Client() *s3.S3 {
    return s3.New(awssess, &aws.Config{
        Region: aws.String(defaultRegion),
    })
}

func readObject(bucket, key string) ([]byte, error) {
    obj, err := s3Client.GetObject(&s3.GetObjectInput{
        Bucket: aws.String(bucket),
        Key:    aws.String(key),
    })
    if err != nil {
        return nil, err
    }

    defer obj.Body.Close()
    res, err := ioutil.ReadAll(obj.Body)
    if err != nil {
        return nil, err
    }

    return res, nil
}

func main() {
    res, err := readObject("{Your Bucket}", "{Your Key}")
    if err != nil {
        log.Println(err)
    }

    log.Println(string(res))

    return
}

S3バケットに存在するファイルをlog.Println()により表示しています。
readObject()に対してテストコードを書きます。
しかしreadObject()はS3に直接アクセスする処理が含まれており、外部処理に完全に依存しています。
このままではテストを実行するごとにS3へのアクセスを実行しなくてはならず、現実的ではありません。

ここでまず考えるべきことは、モックを作るということです。

関数を分ける

まず、現状のreadObject()の問題点は以下の2点が同じ関数に存在することにあります。

  • S3からファイルの取得
  • ファイルの読み取り

そこで、この2つの処理を別関数で分離します。

func getObject(bucket, key string) (*s3.GetObjectOutput, error) {
    obj, err := s3Client.GetObject(&s3.GetObjectInput{
        Bucket: aws.String(bucket),
        Key:    aws.String(key),
    })
    if err != nil {
        return nil, err
    }

    return obj, nil
}

func readObject(bucket, key string) ([]byte, error) {
    obj, err := getObject(bucket, key)
    if err != nil {
        return nil, err
    }

    defer obj.Body.Close()
    res, err := ioutil.ReadAll(obj.Body)
    if err != nil {
        return nil, err
    }

    return res, nil
}

これで分離ができました。
しかし、このままでは前述のS3アクセスへの依存は解決できていません。

ここで、interfaceの出番です。

interfaceの実装

結論から述べると、モック化したい関数はinterfaceの関数として実装するとよいです。
当記事では、getObject()interfaceの関数とします。

そうすることで、getObject()の中の処理を外から渡すことができます。
正常時はS3へのアクセスを渡し、テスト時はモックを渡すことができるわけです。

以下がinterfaceを実装した全体コードです。

main.go
package main

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
    "io/ioutil"
    "log"
)

var awssess = newSession()
var s3Client = newS3Client()

const defaultRegion = "ap-northeast-1"

func newSession() *session.Session {
    return session.Must(session.NewSessionWithOptions(session.Options{
        SharedConfigState: session.SharedConfigEnable,
    }))
}

func newS3Client() *s3.S3 {
    return s3.New(awssess, &aws.Config{
        Region: aws.String(defaultRegion),
    })
}

type objectGetterInterface interface {
    getObject() (*s3.GetObjectOutput, error)
}

type objectGetter struct {
    Bucket string
    Key    string
}

func newObjectGetter(bucket, key string) *objectGetter {
    return &objectGetter{
        Bucket: bucket,
        Key:    key,
    }
}

func (getter *objectGetter) getObject() (*s3.GetObjectOutput, error) {
    obj, err := s3Client.GetObject(&s3.GetObjectInput{
        Bucket: aws.String(getter.Bucket),
        Key:    aws.String(getter.Key),
    })
    if err != nil {
        return nil, err
    }

    return obj, nil
}

func readObject(t objectGetterInterface) ([]byte, error) {
    obj, err := t.getObject()
    if err != nil {
        return nil, err
    }

    defer obj.Body.Close()
    res, err := ioutil.ReadAll(obj.Body)
    if err != nil {
        return nil, err
    }

    return res, nil
}

func main() {
    t := newObjectGetter("{Your Bucket}", "{Your Key}")
    res, err := readObject(t)
    if err != nil {
        log.Println(err)
    }

    log.Println(string(res))

    return
}

readObject()の引数に注目してください。
objectGetterInterfaceを引数に渡しています。

また、getObject()objectGetter構造体をレシーバとし、メソッド化しています。

objectGetterInterfaceは、以下のメソッドを満たしていることを条件としています。

getObject() (*s3.GetObjectOutput, error)

つまり、objectGetter構造体はobjectGetterInterfaceの条件を満たしているということになります。

では、モックのメソッドについても考えていきます。
同じように、getObject() (*s3.GetObjectOutput, error)を満たすように実装してみましょう。

type objectGetterMock struct{}

func (m objectGetterMock) getObject() (*s3.GetObjectOutput, error) {
    b := ioutil.NopCloser(strings.NewReader("hoge"))
    return &s3.GetObjectOutput{
        Body: b,
    }, nil
}

hogeという文字列が記録されているファイルが格納されているという想定です。

これで、objectGetterMock構造体も同様にobjectGetterInterfaceの条件を満たすことができました。

では実際のテストコードをみてみましょう。

テストコード

main_test.go
package main

import (
    "github.com/aws/aws-sdk-go/service/s3"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/suite"
    "io/ioutil"
    "strings"
    "testing"
)

type testSuite struct {
    suite.Suite
    service *objectGetter
}

func (s *testSuite) SetUpTest() {
    s.service.Bucket = "dummy"
    s.service.Key = "dummy"
}

func TestExecution(t *testing.T) {
    suite.Run(t, new(testSuite))
}

type objectGetterMock struct{}

func (m objectGetterMock) getObject() (*s3.GetObjectOutput, error) {
    b := ioutil.NopCloser(strings.NewReader("hoge"))
    return &s3.GetObjectOutput{
        Body: b,
    }, nil
}

func (s *testSuite) Test() {
    mock := objectGetterMock{}
    res, _ := readObject(mock)
    assert.Equal(s.T(), "hoge", string(res))
}

以下に注目してください。

func (s *testSuite) Test() {
    mock := objectGetterMock{}
    res, _ := readObject(mock)
    assert.Equal(s.T(), "hoge", string(res))
}

objectGetterMockreadObject()に渡しています。
そのため、テストではS3からファイルの取得について気にする必要はなく、ファイルの読み取りにフォーカスしたテストを書くことができました。

終わりに

interfaceを設計することでテストコードが非常に書きやすくなります。
ぜひ、テストコードを書くことで品質の高いプロダクトを目指してください。

こちらにサンプルコードを公開しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】go-oidcを使ってOpen ID Connectを利用する

Open ID Connect

  • outh2の進化版でトークン発行の処理がある
  • アプリケーション自体にユーザーパスワードなどの認証情報を持たせずに、別のプロバイダー(ここではGoogle)に委任する
  • プロバイダーはいろいろある
    • Google
    • AzureAD
    • AWS Cognito
    • outh0
  • stateという概念があり、クロスサイトスクリプティングなどのなりすましを防止

公式パッケージ

https://github.com/coreos/go-oidc

インストール

git cloen https://github.com/coreos/go-oidc

exampleのREADMEの通りにセットアップする

cd go-oidc/example
yuta:~/go-oidc/example (v3=) $ cat README.md
# Examples

These are example uses of the oidc package. Each requires a Google account and the client ID and secret of a registered OAuth2 application. To create one:

1. Visit your [Google Developer Console][google-developer-console].
2. Click "Credentials" on the left column.
3. Click the "Create credentials" button followed by "OAuth client ID".
4. Select "Web application" and add "http://127.0.0.1:5556/auth/google/callback" as an authorized redirect URI.
5. Click create and add the printed client ID and secret to your environment using the following variables:

Google Developer Consoleへアクセス

https://console.developers.google.com/

認証情報(アプリケーション)を作る

image.png

設定する

image.png

環境変数の設定

  • 作成すると、クライアントIDとクライアントシークレットが作られる
export GOOGLE_OAUTH2_CLIENT_ID=739786550065-g36863uform2efr5nrvvmjpj30pu9nuf.apps.googleusercontent.com
export GOOGLE_OAUTH2_CLIENT_SECRET=4sLRLGBdYu_un8C58c8Yg2yr

osパッケージで実行サーバのOS環境変数が読み込まれる

var (
  clientID     = os.Getenv("GOOGLE_OAUTH2_CLIENT_ID")
  clientSecret = os.Getenv("GOOGLE_OAUTH2_CLIENT_SECRET")
)

動作確認

起動

yuta:~/go-oidc/example (v3=) $ go run idtoken/app.go
2021/01/17 16:13:04 listening on http://127.0.0.1:5556/

http://127.0.0.1:5556/へアクセス

image.png

認証成功時

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang】サポートする GOOS と GOARCH を一覧で確認するコマンド(ビルドの対応プラットフォーム確認)

go build する際に指定可能な $GOOS$GOARCH の一覧をコマンドで確認したい。

なんか、Go 言語のバージョンだけでなく環境によってもビルドできる(サポートされた)OS やアーキテクチャが異なるようだったので、シェル・スクリプトで事前にチェックしたいのです。

「golang GOOS 一覧 確認 コマンド」でググってもピンポイントな記事が出てこなかったので、自分のググラビリティとして。

TL; DR

go tool dist list

TS; DR

$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
...(以下省略)

$ go version
go version go1.15.6 linux/amd64

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang】サポートする GOOS と GOARCH を一覧で確認するコマンド(GOARM のリンクもあるでよ)

go build する際に指定可能な $GOOS$GOARCH の一覧をコマンドで確認したい。

なんか、Go 言語のバージョンだけでなく環境によってもビルドできる(サポートされた)OS やアーキテクチャが異なるようだったので、シェル・スクリプトで事前にチェックしたいのです。

「golang GOOS 一覧 確認 コマンド」でググってもピンポイントな記事が出てこなかったので、自分のググラビリティとして。

TL; DR

go tool dist list

TS; DR

$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
...(以下省略)

$ go version
go version go1.15.6 linux/amd64

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Macでgqlgenのserver.goを起動するとファイアウォール警告が出る

GoのGraphQLサーバライブラリであるgqlgenの開発サーバ(GraphQL Playground)をMac上で起動すると、ファイアウォールの警告が出てしまってめんどくさい。
20210108160313.png

通常はMacの環境設定のセキュリティとプライバシーのファイアウォール設定で例外を設定するのですが、うまくいかなかったので別の方法で解決します。

server.goの最後で次のようになっています。

log.Fatal(http.ListenAndServe(":"+port, nil))

これだと「0.0.0.0」でlistenするようです。0.0.0.0はすべてのネットワークインタフェースを指し、過剰です。

以下のように「localhost」まはは「127.0.0.1」を明示するように変更します。

log.Fatal(http.ListenAndServe("localhost:"+port, nil))

これで警告は出なくなりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINE Bot からDropboxに写真をアップロードする

タイトルの通り、LINE Bot からDropboxに写真をアップロードするシステムを作ってみました。現状は課題だらけですが、主要機能が実装できたので投稿します。

LINE Botに画像を送ると、
image.png

Dropboxにアップロードします。
image.png

ソースコード

main.go
package main

import (
    "io"
    "log"
    "net/http"
    "os"

    "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox"
    "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/files"
    "github.com/line/line-bot-sdk-go/linebot"
    "gopkg.in/ini.v1"
)

type Config struct {
    channelSecrt string
    channelToken string
    dropboxToken string
}

var conf Config

func init() {
    c, _ := ini.Load("config.ini")
    conf = Config{
        channelSecrt: c.Section("lineBotApi").Key("secret").String(),
        channelToken: c.Section("lineBotApi").Key("token").String(),
        dropboxToken: c.Section("DropboxApi").Key("token").String(),
    }
}

func main() {
    http.HandleFunc("/callback", lineHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func lineHandler(w http.ResponseWriter, r *http.Request) {
    bot, err := linebot.New(
        conf.channelSecrt,
        conf.channelToken,
    )
    if err != nil {
        log.Fatal(err)
    }
    events, err := bot.ParseRequest(r)
    if err != nil {
        if err == linebot.ErrInvalidSignature {
            w.WriteHeader(400)
        } else {
            w.WriteHeader(500)
        }
        return
    }
    for _, event := range events {
        if event.Type == linebot.EventTypeMessage {
            switch message := event.Message.(type) {
            case *linebot.ImageMessage:
                content, err := bot.GetMessageContent(message.ID).Do()
                if err != nil {
                    log.Println(err)
                }
                filename := message.ID + ".png"
                file, err := os.Create(filename)
                if err != nil {
                    log.Println(err)
                }

                if _, err = io.Copy(file, content.Content); err != nil {
                    log.Println(err)
                }

                config := dropbox.Config{
                    Token: conf.dropboxToken,
                }
                cli := files.New(config)

                req := files.NewCommitInfo("/" + filename)
                f, _ := os.Open(filename)
                _, err = cli.Upload(req, f)
                if err != nil {
                    log.Print(err)
                    return
                }

                if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage("アップロードが完了しました")).Do(); err != nil {
                    log.Print(err)
                }
            default:
                if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage("画像を送信してください")).Do(); err != nil {
                    log.Print(err)
                }
            }
        }
    }
    if err := http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil {
        log.Fatal(err)
    }
}

要点まとめ

Botに送信した画像を受け取ってローカルサーバに保存する

保存するファイルが一意な名前になるようにmessageIDをファイル名にしています。

content, err := bot.GetMessageContent(message.ID).Do()
                if err != nil {
                    log.Println(err)
                }
                filename := message.ID + ".png"
                file, err := os.Create(filename)
                if err != nil {
                    log.Println(err)
                }

                if _, err = io.Copy(file, content.Content); err != nil {
                    log.Println(err)
                }

Dropboxにファイルをアップロードする

                config := dropbox.Config{
                    Token: conf.dropboxToken,
                }
                cli := files.New(config)

                req := files.NewCommitInfo("/" + filename)
                f, _ := os.Open(filename)
                _, err = cli.Upload(req, f)
                if err != nil {
                    log.Print(err)
                    return
                }

改善点

・ローカルに保管したファイルを削除する
・複数の画像を受け取れるようにする(現状は1枚ずつしか送信できない)

感想

思い描いてた機能を実装できるって楽しいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go moduleを使う

環境変数で有効化する

go env -w GO111MODULE=on
go env | grep MOD

初期化処理

go mod init test

go.modが作成されている

go.mod
module test

go 1.13

起動

自動的にgo getされていないパッケージがインポートされる

go run main.go
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go.modの使い方

GOPATHを使っているとimport出来ないライブラリとか出てきて変なエラーが出てくる事があると思います。
GoModulesのgo.modを使えば解決するかもしれないので記載しておきます。

【環境】

windows10
VScode



ターミナルでgo envと打って設定を確認します


VScodeからターミナル→新しいターミナルで開いてgo envと打ち込みます。
ずらーっと色々設定が並んでくると思いますがその中の
GO111MODULE=
これがデフォルトだとonになっていないので設定をonにします。

GO111MODULE=をonにする

ターミナルで
go env -w GO111MODULE=on
と打ち込みましょう。
もう一度go envと打って確認すると
set GO111MODULE=onとなっているはずです。
※筆者はこの時、なぜかonにならず原因が分からなかったので
一度Go言語を再インストールしたところうまくいきました。

go.modを配置する

プロジェクトのルートディレクトリにcdで移動して、
go mod init example.com/m
と打ちます。
すると、ルートディレクトリにgo.modが作成されます。
go.modが作成されてから、go build と打ち込んで
ビルドするとimportに書かれているライブラリが自動でダウンロードされます。
ダウンロードされたライブラリがgo.modに書き込まれていきます。
こういう感じで↓

[go.mod]
module example.com

go 1.15

require github.com/labstack/echo/v4 v4.1.17

go.modはプロジェクト毎に作成して使います。
go.sumというファイルも自動生成されますが気にしなくて大丈夫です!

moduleの部分についての説明はこちら

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「世界一美しい数式」をプログラムで表現する

はじめに

$e^{iπ} = -1$
は、「世界一美しい数式」といわれているオイラーの等式です。
確かに美しいというか無駄がないという感じですが、これをプログラムで出力してみたいと思いました。
ただ、それだけです。

とはいえ、一見簡単そうな数式ですが、プログラムで表現するのは少しだけ手間です。

  • $e$や$π$といった無理数を扱う必要がある。
  • 複素数を扱う必要がある。
  • さらに、複素数のべき乗を扱う必要がある。

実行環境

オンラインでコンパイル可能な以下のいずれかで実行確認しました。
Paiza.io
TIO

D言語の場合

  • $e$はE、$π$は、PIという定数定義がstd.mathで用意されている。
  • 複素数は、complexテンプレートが用意されている。
  • べき乗は^^演算子を利用できる。
import std;

void main()
{
    auto x = complex(cast(real)0.0, PI);
    auto e = complex(E, cast(real)0.0);
    auto ans = e ^^ x;
    writeln(ans);
    writefln("%.10f %.10f", ans.re, ans.im);
}
出力結果
-1-5.01656e-20i
-1.0000000000 -0.0000000000

writeln(ans)ように、演算結果をそのまま出力すると、-1-5.01656e-20iと誤差が発生します。
他のプログラミング言語でもそうですが、無理数を浮動小数で扱う以上、このようになります。
ちなみに、D言語の場合、倍精度(double)よりも精度が高いreal型を利用できるので、虚数部の誤差が5.01656e-20と、より少なくなっています。
double型を利用すると、他のプログラミング言語と同様-1+1.22465e-16iとなります。

誤差を丸めて表示するためにwritefln("%.10f %.10f", ans.re, ans.im)と小数点以下10桁表示にしました。
その場合、実数部が-1.0000000000、虚数部が-0.0000000000と表示されました。

以下、他のプログラミング言語の実装例も紹介します。

C言語の場合

  • $e$や$π$は、C言語の標準仕様では定数定義されていない。
    コンパイラによりM_EM_PIとして定義されている場合がある。

  • 複素数は、double complexが用意されている。

  • 複素数のべき乗はcpow関数を利用できる。

#include <stdio.h>
#include <math.h>
#include <complex.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#ifndef M_E
#define M_E 2.71828182845904523536
#endif

int main(void){
    double complex x1, z;
    x1 = M_PI * I;
    z  = cpow(M_E, x1);
    printf("%.20f %.20f\n", creal(z), cimag(z));
    printf("%.10f %.10f\n", creal(z), cimag(z));
}
出力結果
-1.00000000000000000000 0.00000000000000012246
-1.0000000000 0.0000000000

double complex型を複素数形式でそのまま表示できないようです。

C++の場合

  • $e$や$π$は、<cmath>M_EM_PIとして定義。
  • 複素数は、complexテンプレートが用意されている。
  • べき乗はpow関数を利用できる。
#include <iostream>
#include <cmath>
#include <complex>
using std::complex;

int main(void){
    auto x1 = complex<double>(0.0, M_PI);
    auto e  = complex<double>(M_E, 0.0);
    auto z  = pow(M_E, x1);
    std::cout << z << std::endl;
    printf("%.10f %.10f\n", real(z), imag(z));
}
出力結果
(-1,1.22465e-16)
-1.0000000000 0.0000000000

C#の場合

  • $e$や$π$は、Math.EMath.PIとして定義。
  • 複素数は、Complexクラスが用意されている。
  • 複素数のべき乗はComplex.Pow関数を利用できる。
using System;
using System.Numerics;

public class Example
{
  public static void Main()
  {
    Complex x = new Complex(0.0, Math.PI);
    Complex ans = Complex.Pow(Math.E, x);
    Console.WriteLine(ans);
    Console.WriteLine("{0:f10} {1:f10}", ans.Real, ans.Imaginary);
  }
}
出力結果
(-1, 1.22464679914735E-16)
-1.0000000000 0.0000000000

Goの場合

  • $e$や$π$は、math.Emath.Piとして定義。
  • 複素数は、complex型が用意されている。
  • 複素数のべき乗はcmplx.Pow関数を利用できる。
package main

import (
  "fmt"
  "math"
  "math/cmplx"
)

func main() {
     x1  := complex(0, math.Pi)
     ans := cmplx.Pow(math.E, x1)
     fmt.Println(ans)
     fmt.Printf("%.10f %.10f", real(ans), imag(ans))
}
出力結果
(-1+1.2246467991473515e-16i)
-1.0000000000 0.0000000000

Perlの場合

  • $e$や$π$は、定数定義されていない。
  • 複素数は、1.0 + 1.0 * iといった形で利用できる。
  • べき乗は**演算子を利用できる。
use Math::Complex;
my $pi  = 3.14159265358979323846;
my $e   = exp(1.0);
my $x   = 0.0 + $pi * i;
my $ans = $e ** $x;
print $ans, "\n";
printf "%.10f %.10f\n", Re($ans), Im($ans);
出力結果
-1+1.22464679914735e-16i
-1.0000000000 0.0000000000

Pythonの場合

  • $e$や$π$は、math.emath.piとして定義。
  • 複素数は、complex型が用意されている。
  • べき乗はpow関数を利用できる。
import math
x   = complex(0, math.pi)
ans = pow(math.e, x)
print(ans)
print("%.10f %.10f" % (ans.real, ans.imag))
出力結果
(-1+1.2246467991473532e-16j)
-1.0000000000 0.0000000000

Rubyの場合

  • $e$や$π$は、Math::EMath::PIとして定義。
  • 複素数は、Complex型が用意されている。
  • べき乗は**演算子を利用できる。
x   = Complex(0.0, Math::PI)
ans = Math::E ** x
puts(ans)
printf("%.10f %.10f\n", ans.real, ans.imag)
出力結果
-1.0+0.0i
-1.0000000000 0.0000000000

Rubyでは、演算結果をそのまま出力しても誤差が表示されませんでした。

Haskellの場合

  • $e$は、定数定義されていない。$π$は、piとして定義。
  • 複素数は、1.0 :+ 1.0といった形で利用できる。
  • べき乗は**演算子を利用できる。
import Data.Complex
import Text.Printf

main = do
  let x   = 0.0 :+ pi
  let e   = exp 1.0
  let ans = e ** x
  let re  = realPart(ans)
  let im  = imagPart(ans)
  print ans
  printf "%.10f %.10f\n" (re::Double) (im::Double)
出力結果
(-1.0) :+ 1.2246467991473532e-16
-1.0000000000 0.0000000000

Juliaの場合

  • $e$や$π$は、πとして定義。
  • 複素数は、Complex型が用意されている。
    • 虚数単位 im も定数として用意されている。
  • べき乗は^演算子を利用できる。
using Printf
x = π * im  # == complex(0.0, π)
ans =  ^ x
println(ans)
@printf("%.10f %.10f\n", real(ans), imag(ans))
出力結果
-1.0 + 1.2246467991473532e-16im
-1.0000000000 0.0000000000
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む