20200801のGoに関する記事は6件です。

[Go言語] 初心者必見シリーズ: 構造体(Structs)

構造体

構造体は、フィールド(field)の集まりです。

定義方法

構造体は下記のように typestruct を使用して定義します。

  • 構文
  type 構造体の名前 struct {
      フィールド名 型名
      フィールド名 型名
      フィールド名 型名
      ...
  }

同じ型のフィールド名は、カンマ区切りで書くことができます。

  type 構造体の名前 struct {
      フィールド名, フィールド名, フィールド名 型名
      フィールド名 型名
  }

人のデータをまとめた構造体です。

    type People struct {
            Name string
            Sex string
            Age int
            Height int
            Weight int
    }

初期化

方法1:Key:Valueで初期化します。

people := People{Name: "太郎"}

設定しない要素は ゼロ値 で初期化される。

方法2:要素の定義順に初期化します。

people := People{"太郎", "男", ...}

この方法では、全部のfieldを設定する必要があります。

方法3:定義後にドット(.)を用いてfieldを初期化します。

people := People{}
people.Sex = "男"
people3.Age = 8

構造体へのポインター(Pointer to structs)

方法1:& (アドレス演算子)で構造体へのPointerを取得します。

people := People{Name: "太郎"}
p := &people
(*p).Name = "花子"

(*p)p に省略できます。

p.Name = "花子"

方法2: new(構造体名) で構造体へのポインターを生成します。

people := new(People) // Peopleのポインタ型
(*people).Name = "花子"
people.Name = "花子"  // (*people) は peopleに省略できます。

new(T) はT型の無名変数(unnamed variable)を作成し、それを T のゼロ値へ初期化し、 *T 型の値であるそのアドレスを返します。

new(People)&People{} は等価です。

タグ(Tag)

Go構造体タグは、Go構造体宣言のフィールドの後に表示される注釈です。

実行時にreflectパッケージでその情報を取得し利用したりします。

良くある構造体をJSONに変換する例として、

下記のようにフィールドの後ろに json タグをつけると、

  type People struct {
          Name string       `json:"name"`
          Sex string        `json:"sex"`
          Age int               `json:"age"`
          Height int        `json:"height"`
          Weight int        `json:"weight"`
  }

Go言語の json 標準パッケージで構造体をJSONに変換すると、フィールド名がTagで定義された名前に置換されます。

people := People{Name: "太郎", Sex: "男"}
jsonBytes, _ := json.Marshal(people)
fmt.Println(string(jsonBytes)) 
// JSON: {"name": "太郎", "sex": "男"}

その仕組み

jsonパッケージの内部で下記のようにタグ情報を取得してJSONデータに変換しています。

 t := reflect.TypeOf(people)
 for i := 0; i < t.NumField(); i++ {
     field := t.Field(i)
     tag := field.Tag.Get("json") //  `json`タグ情報を取得
 }

下記のように、複数のタグはスペースで区切って記述できます。

  type People struct {
          Name string       `json:"name" validate:"required"`
          Sex string        `json:"sex" validate:"required"`
          Age int               `json:"age" validate:"required"`
          Height int        `json:"height" validate:"required"`
          Weight int        `json:"weight" validate:"required"`
  }

埋め込みフィールド(Embedded fields)

型だけで宣言されていて、明示的なフィールド名がないフィールドを埋め込みフィールドと呼びます。

image-20200801172005624.png

Go言語は埋め込みで他言語のクラスの継承ぽいことができます。

埋め込み子構造体(Address)のフィールドが昇格し、構造体の外(Person)からダイレクトにアクセスすることが可能です。

person.Adress.city  person.city
person.Adress.state  person.state
// PersonがAdressを継承しているように、Adressのフィールドをダイレクトにアクセスすることが可能となります。

Person に同名のフィールドやメソッドがあるときは、Address にあるそのフィールドは昇格されないです。

image-20200801173432397.png

構造体の比較

reflect.DeepEqual はポインターか否かにかかわらず、

フィールドの値が同じであれば true を返します。

import (
    "reflect"
)

...
people1 := People{Name: "太郎"}
people2 := People{Name: "太郎"}

println(reflect.DeepEqual(people1, people2))     // true
println(reflect.DeepEqual(&people1, &people2))   // true
println(people1 == people2)    // true
println(&people1 == &people2)  // false

動画(YouTube)

https://www.youtube.com/watch?v=91_7hWkByv4

元記事

https://aifan.jp/2020/08/01/Go%E8%A8%80%E8%AA%9E-%E5%88%9D%E5%BF%83%E8%80%85%E5%BF%85%E8%A6%8B%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-%E6%A7%8B%E9%80%A0%E4%BD%93%EF%BC%88Structs%EF%BC%89/

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

VS Code の Remote Container で複数のコンテナを起動して作業を切り替える

はじめに

VSCode の Remote Container って便利ですよね。
ローカルPCの環境を汚さずにいろいろな言語を試すことのできるのが最高。

ただ、サクッと試したいだけなのに、いちいち言語別に設定するのが面倒だったりしますよね。

この投稿では、1回起動するだけで複数のコンテナに接続できる設定を紹介します。

公式ドキュメントにも書かれているので、詳しく知りたい方はこちらを。
https://code.visualstudio.com/docs/remote/containers-advanced#_connecting-to-multiple-containers-at-once

デモコードの概要

ここでは、golang と python のコンテナを扱うこととします。
.
├── golang
│ ├── .devcontainer.json
│ └── Dockerfile
├── python
│ ├── .devcontainer.json
│ └── Dockerfile
└── docker-compose.yml

設定ファイルの詳細

1. docker-compose.yml

remote-containerで管理するコードの親元を docker-compose.yml に記載します。

build.context でディレクトリを指定し、そこから Dockerfile までの相対パスを dockerfile に書いておけば大丈夫です。

ディレクトリ構成が気に入らなければ、自由に変えることもできます。

docker-compose.yml
version: "3"
services:
  golang-container:
    build:
      context: golang 
      dockerfile: Dockerfile
    volumes:
      - .:/workspace:cached
    environment:
      TZ: "Asia/Tokyo"
    command: sleep infinity

  python-container:
    build:
      context: python 
      dockerfile: Dockerfile
    volumes:
      - .:/workspace:cached
    environment:
      TZ: "Asia/Tokyo"
    command: sleep infinity

2. .devcontainer.json

コンテナごとの設定を .devcontainer.json に記載します。

親子関係をつなぐために、 dockerComposeFile と service の名前は docker-compose.yml で定義したものと一致させています。

workspaceFolder で言語ごとに区分けたディレクトリを指定してやることにより、VS Code のウィンドウ上に他言語が表示されないようにできます。(表示していると警告が出てきたりしてうるさい)

extensions は調べたらいろいろあるので、好きにカスタマイズしたらいいです。

.devcontainer.json
{
    "name": "Dev Container Golang Env",
    "dockerComposeFile": "../docker-compose.yml",
    "service": "golang-container",
    "workspaceFolder": "/workspace/golang",
    "extensions": [
      "golang.go",
      "donjayamanne.githistory",
      "eamodio.gitlens",
      "codezombiech.gitignore",
      "mhutchie.git-graph",
      "esbenp.prettier-vscode",
      "coenraads.bracket-pair-colorizer",
      "ionutvmi.path-autocomplete"
    ],
    "terminal.integrated.shellArgs.linux": [
        "-l"
    ],
    "shutdownAction": "stopCompose"
}

3. Dockerfile

コンテナごとに使いたい設定を記載します。

普通のDockerfileと同じようにかけるので、ライブラリのインストールもしておくと便利です。

ここでは、みんな大好き git の補完ツールをインストールさせています。

Dockerfile
FROM golang:buster

WORKDIR /

RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      git \
      openssh-client \
      procps \
      ca-certificates \
      curl \
      unzip \
      gnupg \
      vim \
      wget \
 && rm -rf /var/lib/apt/lists/*

# git
RUN wget https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.bash -O ~/.git-completion.bash
RUN chmod a+x ~/.git-completion.bash
RUN echo "source ~/.git-completion.bash" >> ~/.bashrc
RUN wget https://raw.githubusercontent.com/git/git/master/contrib/completion/git-prompt.sh -O ~/.git-prompt.sh
RUN chmod a+x ~/.git-prompt.sh
RUN echo "source ~/.git-prompt.sh" >> ~/.bashrc

複数コンテナの起動方法

  1. VS Code のコマンドパレットを開いて、Remote-Containers: Open Folder in Container... を選択する
  2. 開きたいディレクトリを選択する(今回は golang か python のディレクトリ)

上記のように普通にRemoteContainerを起動する要領で操作したら、選択したコンテナだけでなく、設定に記載されている他のコンテナも作成してくれます。

切り替えるには、1. 2. の手順と同様に、開きたいディレクトリを選択するだけです。
1回目の起動時にコンテナは立ち上がっているので、ウィンドウが切り替えられてすぐに操作できます。

もし別ウィンドウにしたければ、VS Code で新規ウィンドウを作成して、そこから 1. 2. の手順を実行したらいいです。

まとめ

VS Code の Remote Container で複数のコンテナを起動する方法を紹介しました。
ここでは golang と python の2つのコンテナでしたが、自由に追加できます。

複数言語を git のプライベートレポジトリで管理するときなんかに役立つのではないでしょうか。
言語のバージョン違いの動作検証なんかにも使えそうですね。

ソースコードは github で公開しています。
https://github.com/fumiyakk/demo-multiple-remote-containers

python は poetry を使うために手のこんだ作りになってます。
そのうち poetry と black を組み合わせた python の開発環境について紹介します。

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

Goでソースコードを画像化するCLIを作った

こんにちわ
ゴリラです

普段、Twitterでたまにこういうふうにソースコードの画像を貼り付けることがあります。

image.png

画像を作るのにcarbonというサービスを使っています。
このサービスできれいな画像を生成できますが、インターネットとブラウザなしでは使えないためちょっと不便と感じています。
また、sliconというrust製のツールもありますが、これもcarbonと同様の制限があります。
そこで、ブラウザやネットを使用せずCLIでソースコードを画像化できたら良いなと思いCLIを作りました。

どんな感じ

こんな感じでpngファイルを出力できます。外部ツール依存無しでGoだけで動くのでインターネットもブラウザも必要ないです。

使い方

2通りあります。ソースコードを標準入力で渡すか、ファイルを渡すかです。

code2img - convert source code to image

Version: 1.0.0

Usage:
  $ code2img -t monokai main.go main.png
  $ echo 'fmt.Println("Hello World")' | code2img -ext go -t native -o sample.png

ファイルの場合はオプションなくても使えますが、カラースキーマを指定したい場合は-tを使用します。
使用可能なカラースキーマはこちら、サポートしている言語一覧はこちらを参照ください。

実装

処理の流れは大まかと次になります。

  1. コードをトークナイズして、トークンごとに色を情報をつける
  2. トークンを1文字ずつpngに書き込む

ソースコードをトークナイズしてトークンごとに色(RGBA)をつけますが、ここをスクラッチで多言語に対応するのは骨が折れるので、素直にgithub.com/alecthomas/chromaというライブラリを使用しました。

このライブラリを使えば、2の処理だけを書けば済みます。

2の処理は大まかに次になります。

  1. 文字を描画するfontを読み込み
  2. 座標を計算しつつ、1文字ずつ描画する

fontに関してはマルチバイト対応のCicagithub.com/jessevdk/go-assetsで埋め込んでいます。
そのためCLIのサイズが倍くらい増えましたが、致し方ない…

文字描画の処理は次になっています。このiterator.Tokens()がtoken情報を返してくるので、
tokenの種類からstyle.Get()で色情報を取得しています。その後、1文字ずつpngに書き込んで、座標を計算して…を繰り返します。

ここでポイントですが、マルチバイトの場合は座標は +2 しないと文字が重なって読めなくなります。なので1文字の長さを確認して必要あればx座標を +2 しています。これでマルチバイトでも問題なく描画されます。

for _, t := range iterator.Tokens() {
    c := style.Get(t.Type).Colour
    dr.Src = image.NewUniform(color.RGBA{R: c.Red(), G: c.Green(), B: c.Blue(), A: 255})

    for _, c := range t.String() {
        if c == '\n' {
            x = fixed.Int26_6(padding)
            y++
            continue
        } else if c == '\t' {
            x += fixed.Int26_6(padding)
            continue
        }
        dr.Dot.X = fixed.I(10) * x
        dr.Dot.Y = fixed.I(20) * y
        s := fmt.Sprintf("%c", c)
        dr.DrawString(s)

        // if mutibyte
        if len(s) > 2 {
            x = x + 2
        } else {
            x++
        }
    }
}

エディタと連携

標準入力に対応しているので、Vimなどのエディタと連携してサクッと画像化出来ます。

vim-code2img.gif

さいごに

依存なしでソースコードを画像に変換できるので、ぜひ試してみてください。エディタとも連携できて便利です。

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

GoでMonth型をint型に変換

英語で表されるtime.Month型をintで型キャストしたら普通に変換できたのでメモ。

main.go
package main

import (
    "fmt"
    "time"
)

func main() {
        now := time.Now()
        year := now.Year()
        month := now.Month()
        monthToInt := int(month)
        date := now.Day()
        hour := now.Hour()
        minute := now.Minute()
        second := now.Second()

        fmt.Printf("年を表示:%v\n", year)
        fmt.Printf("月をtime.Month型で表示:%v\n", month)
        fmt.Printf("月をint型に変換して表示:%v\n", monthToInt)
        fmt.Printf("日にちを表示:%v\n", date)
        fmt.Printf("時間を表示:%v\n", hour)
        fmt.Printf("分を表示:%v\n", minute)
        fmt.Printf("秒を表示:%v\n", second)

}

結果
年を表示:2020
月をtime.Month型で表示:August
月をint型に変換して表示:8
日にちを表示:1
時間を表示:10
分を表示:31
秒を表示:17
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golangを唐突に始めたい衝動に駆られた話

金曜日の深夜に急にGoを始めたい衝動に駆られた

思い立った経緯

金曜日の夜にふと人気プログラミング言語について閲覧していた。そこでPythonやJavascript不動の人気に、「はい、そうですか。」気分でみていた。そこでみたGoLangをみて、「あ、せっかくだし触りだけでも今日やろうかな」と思ったので、自分への議事録のため記事を投稿。

やったこと

自分のPCでの動作環境構築と公式ドキュメントの「Packages, variables, and functions.
Learn the basic components of any Go progr」をやってみた

動作環境

OS:macOS Catalina 10.15.6
Editor:Visual Studio Code 1.47.3
pkg管理:Homebrew 2.4.9

環境構築

まずはGoLangのインストールから

hoge@hoge-MacBook-Air ~ % brew install go
...省略...
hoge@hoge-MacBook-Air ~ % go version
go version go1.14.5 darwin/amd64

インストール完了だね。

動作確認

Visual Studio Code(以下VSCode)を起動して、必要な拡張機能をインストールする。
スクリーンショット 2020-08-01 3.21.16.png

だけをインストールしました。
そして、(Cmd+Shift+P)で全てのコマンドを表示し、以下を選択する。
スクリーンショット 2020-08-01 3.26.38.png

  • Go:Install/Update Tools

スクリーンショット 2020-08-01 3.28.00.png

項目を全て選択し、インストールする。
インストール完了したら環境構築は完了となるので、実際にコードを試しに書いていく。

サンプルコード実装と内容の理解

公式のページ(https://golang.org/ )にならって、以下のファイルを任意のフォルダで作成。

hello.go
// You can edit this code!
// Click here and start typing.
package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}

そしてコンパイル実行を行う。

hoge@hoge-MacBook-Air sampleGo % go run hello.go
Hello, 世界

問題なく実行確認が完了しました。
これでとりあえずの環境構築が完了としています。

公式ドキュメントを読んでいく

A Tour of Go(https://golang.org/doc/ )でドキュメントを読んでいく。
 

Package & import

Goのプログラムはパッケージで構成されています。
Packageはmainから開始される。
下記のプログラムでは、"fmt"と"math/rand"をインポートしている。

package main

import (
    "fmt"
    "math/rand"
)

Exported names

最初の文字が大文字で始まる名前は、外部パッケージから参照できるエクポートされた名前となっている。
パッケージをインポートしているとエクスポートしている名前は参照することができる。

exported-names.go
// Piはmathパッケージでエクスポートされている
package main
import (
    "fmt"
    "math"
)
func main() {
    fmt.Println(math.Pi)
}

function

関数は0個以上の引数を受け取れる。

Named return values

Goでの戻り値となる変数に名前をつけることができ、ただし、名前をつけた戻り値の変数(以下のxとy)についてはreturn ステートメントに何も書かずに戻すことができ、このことをnaked returnという。推奨は使わない。

named-results.go
package main

import "fmt"

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

func main() {
    fmt.Println(split(17))
}

variables

基本的には「var XXX = X」の形で宣言を行うが、関数ないだけ「XXX := X」と暗黙的な型宣言ができるが、関数の外ではエラーとなる。
基本型と初期値については以下の通り。

bool
    デフォルト初期値:false
string
    デフォルト初期値:""
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 の別名
rune // int32 の別名
     // Unicode のコードポイントを表す
float32 float64
complex64 complex128
    デフォルト初期値:0

Type conversions

Go言語では明示的な変換が必要で、以下のような形でおこなう。
もし、明示的な型を指定せずに変数を宣言する場合は、変数の右側から型推論がされる。

// 通常
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

// シンプルに
i := 42
f := float64(i)
u := uint(f)

Constants

定数はconstキーワードを使って変数と同じように宣言を行う。
定数は、文字(character)、文字列(string)、boolean、数値(numeric)のみで使えます。
なお、定数は := を使って宣言できません。

最後に

今日はここまでやったので、また衝動に駆られた際に続きをやっていこうかと。
よい気晴らしになりました。新しい言語を学び始めるとどっぷりハマってしまうので、
また忘れた頃に続きを別記事で更新します。

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

GoMockで、構造体オブジェクトのポインタを受け取る関数の引数をテストする

概要

モック化させたインターフェースの関数に、期待した値が入って呼ばれるかどうかをテストしたいシーンのお話です。

引数に構造体のポインタを受け取る関数をテストする時、

  • テストスイート内でアサーションのために宣言したオブジェクト
  • 実際のビジネスロジックの中で生成されたオブジェクト

はポインタが異なるため、オブジェクトのチェックが難しくなります。

gomockを使っている際に、この問題を解決する方法をまとめます。

GoMock

GitHub: https://github.com/golang/mock

今回使用したバージョンはv1.4.3です。

ユースケース

概要だけだとイメージが湧きにくいかと思いますので、ここからは例を交えて説明します。
ユースケースは下記のようにシンプルなものを用意します。

  • タイトルと著者名を入力して、Bookオブジェクトを作成する
  • タイトルに空文字を入れた時には、Bookオブジェクトのタイトルが「no title」になる

テストケース

テストでは、

  • When: ユースケースの関数に空文字のタイトルを渡した時
  • Then: DBに保存されるエンティティのタイトルに「no title」がセットされている

ということをチェックします。

ビジネスロジック実装

Entity

ID、 タイトル、 著者名を持ちます。

book.go
package entity

type Book struct {
    ID     uint64
    Title  string
    Author string
}

func NewBook(title, author string) Book {
    return Book{
        Title:  title,
        Author: author,
    }
}

Repository

entityを永続化させるインターフェースです。

book_repository.go
package repository

import "github.com/.../entity"

type BookRepository interface {
    // Bookオブジェクトのポインタを受け取り、DBに保存する
    Save(book *entity.Book) error
}

RepositoryのSave関数がポインタを受け取っています。
このインターフェースを、GoMockでモック化させます。

Interactor

そしてユースケースに該当する、Interactorの関数を今回はテストします。

book_interactor.go
package service

import (
    "fmt"

    "github.com/.../entity"
    "github.com/.../repository"
)

type BookInteractor struct {
    bookRepository repository.BookRepository
}

func (bi BookInteractor) CreateBook(title, author string) error {
    // titleが空の場合はno titleを入れる
    if title == "" {
        title = "no title"
    }

    // entity作成
    book := entity.NewBook(title, author)

    // entityをDBに保存
    err := bi.bookRepository.Save(&book)
    if err != nil {
        return fmt.Errorf("failed to save book: %w", err)
    }

    return nil
}

テストコード

失敗するケース

book_interactor_test.go
package service

import (
    "testing"

    "github.com/golang/mock/gomock"
    "github.com/.../entity"
    mock_repository "github.com/.../mock/repository"
)

func TestBookInteractor_CreateBook(t *testing.T) {
    controller := gomock.NewController(t)
    defer controller.Finish()

    t.Run("succeed when name is empty", func(t *testing.T) {
        // テストする関数の引数に入れる値
        argTitle := "" // When: タイトルに空を指定する
        argAuthor := "taro"

        // bookRepositoryをモック化
        bookRepository := mock_repository.NewMockBookRepository(controller)

        // bookRepository.Save関数の引数に期待するオブジェクト
        expectedBook := entity.Book{
            ID:     0,
            Title:  "no title", // Then: no titleがセットされている
            Author: "taro",
        }

        // bookRepository.Save関数に期待した引数が入って呼ばれるかチェック
        bookRepository.EXPECT().Save(&expectedBook).Return(nil)

        interactor := BookInteractor{
            bookRepository: bookRepository,
        }

        // action: テスト対象関数呼び出し
        err := interactor.CreateBook(argTitle, argAuthor)
        if err != nil {
            t.Fatal(err)
        }
    })
}

Save関数が呼ばれる時の引数の構造体の中身は、想定する expectedBook と同じはずですが、このケースでは失敗します。

なぜなら、Save関数が呼ばれる時には引数がポインタで渡されています。
そして、この expectedBook と、interactorの中で NewBook で生成されたオブジェクトの中身は同じですが、オブジェクト自体のポインタが異なるためにエラーが吐かれます。

この場合にどうすれば良いかというと、2つやり方があります。

  • 構造体の中身をアサーションするカスタムMatcherを作る
  • mockに用意された Do メソッドを使って、構造体の中身を1つずつアサーションする

前者は少々面倒なので、後者で回避します。

成功するケース

book_interactor_test.go
package interactor

import (
    "testing"

    "github.com/golang/mock/gomock"
    "github.com/.../entity"
    mock_repository "github.com/.../mock/repository"
    "github.com/stretchr/testify/assert"
)

func TestBookInteractor_CreateBook(t *testing.T) {
    controller := gomock.NewController(t)
    defer controller.Finish()

    t.Run("succeed when name is empty", func(t *testing.T) {
        // テストする関数の引数に入れる値
        argTitle := "" // When: タイトルに空を指定する
        argAuthor := "taro"

        // bookRepositoryをモック化
        bookRepository := mock_repository.NewMockBookRepository(controller)

        // bookRepository.Save関数に期待した引数が入って呼ばれるかチェック
        bookRepository.EXPECT().Save(gomock.Any()).Return(nil).Do(func(actualBook *entity.Book) {
            // 変更箇所
            assert.Equal(t, "no title", actualBook.Title) // Then: no titleがセットされている
            assert.Equal(t, "taro", actualBook.Author)
        })

        interactor := BookInteractor{
            bookRepository: bookRepository,
        }

        // action: テスト対象関数呼び出し
        err := interactor.CreateBook(argTitle, argAuthor)
        if err != nil {
            t.Fatal(err)
        }
    })
}

GoMockには、モックオブジェクトの関数の処理後にフックされる Doメソッドがあり、それを使用します。
Doに渡すコールバック関数の中に、Saveメソッドが呼ばれた時の実際の引数が入ってきます。
それを対象にコールバック関数内でアサーションを行います。

ポインタで引数が渡されるなら、渡ってきたオブジェクトの中身を1つずつアサーションしてしまおう という戦法です。

なお、Save関数の引数に、gomock.Any() という「どんな引数でも期待する」というMatcherを入れないと、モック関数の呼び出しでコケます。

最後に

GoMockで、構造体オブジェクトのポインタを受け取る関数の引数をテストする方法をまとめました。
もしもっとスマートにテストできる方法があれば教えていただけますと嬉しいです!

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