初心者がGolangでDiscordBot作って痛い目にあった話

#概要
 - GolangでDiscord用抽選Botを開発した
 - ライブラリの挿入や構造体の定義が初心者には注意が必要と悟った

#経緯
モンハンで使うBotが作りたかった。以上!

##モンハンワールド・アイスボーン専用のBotを作っていた
毎回モンハンアイスボーンで一緒に狩猟をする友人(HR/MR:999)が

> クエストと狩猟対象もルーレットで決めたい

と~~変なことを~~言っていたため、~~HR400の私は恐怖のあまり拒否できず~~ワールドで出現する武器・モンスター・クエストを全て抽選できるBotをPythonで開発しました。名前を「受付ジョー」としています。食欲が伺える最高の名前ですね。
![スクリーンショット 2022-02-05 16.35.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484148/9d97e2ab-4854-9df1-512e-c0be4a18444e.png)
食事をするだけでなく、武器などの抽選を行ってくれます。
![スクリーンショット 2022-01-29 18.22.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484148/b7ad55a7-e92b-4692-7351-aa1309be4416.png)
![スクリーンショット 2022-01-29 18.23.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484148/b87a619b-5204-8905-9196-4e6a09636fac.png)

ちなみにHerokuにコードをぶん投げて、そこで仕事させています。

##新作に向けて改修が必要になっていた
現在は次回作であるモンスターハンターライズが発売されており、今夏にはサンブレイクが発売されるらしいのでbot自体の改修が必要になっていました。加えて、利用しているdiscord用のライブラリがサービス終了しているため別ライブラリに変更するなどの修正が必要でした。

「どうせ改修するなら新しく作りたい、どうせ作り直すなら自分があまり触っていない言語で作ってみたい」
と~~頭がトチ狂っていたため~~考え、今回はGoで作ろうという事になりました。

##余談
ちなみに、なぜGoを選んだかというと

 - 昔、"A tour of Go"をやったことがあるが、それ以降Goでなにか書く経験をしていなかった
 - GoのライブラリにDiscord用のライブラリがあり、比較的難易度を下げることが可能である
 - ~~"ナカゴ"というキャラがおり、ちょうどgoと掛けた名前に出来る(ナカ.go)~~

要するに理由は特にないです。

https://go-tour-jp.appspot.com/list

#開発にあたって要件定義
今回作るBotは抽選をしてくれるBotとなるため、要件として以下のモノが必要でした。

 - 武器(ないしは狩猟対象ないしはクエスト名)内から抽選を行う

以上から、次のような工程を踏んだシステムにしました

 1. 投稿されたメッセージを取得する
 1. もしそれに抽選の旨があれば抽選を開始する
 1. 抽選結果を出力する

ということで、とりあえずは武器だけで作っていきました。

#開発物の完成形
以下のようなファイルで構成されています

```
├── go.mod
├── go.sum
├── jsons
│   └── buki.json
└── main.go
```

```Go:main.go
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"github.com/bwmarrin/discordgo"
)

// Variables used for command line parameters
var (
	Token string
	// comment string
)

type privateParam struct {
	Name  string    `json:"name"`
	Param []float64 `json:"param"`
}

type buki_list struct {
	Bukis  []string       `json:"buki-list"`
	People []privateParam `json:"people-param"`
}

func init() {
	flag.StringVar(&Token, "t", "", "Bot Token")
	flag.Parse()
}

func main() {
	// Create a new Discord session using the provided bot token.
	dg, err := discordgo.New("Bot ~~~~~~~~~~~~~~~~~")
	if err != nil {
		fmt.Println("error creating Discord session,", err)
		return
	}

	// Register the messageCreate func as a callback for MessageCreate events.
	dg.AddHandler(messageCreate)

	// In this example, we only care about receiving message events.
	dg.Identify.Intents = discordgo.IntentsGuildMessages

	// Open a websocket connection to Discord and begin listening.
	err = dg.Open()
	if err != nil {
		fmt.Println("error opening connection,", err)
		return
	}

	// Wait here until CTRL-C or other term signal is received.
	fmt.Println("Bot is now running.  Press CTRL-C to exit.")
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
	<-sc

	// Cleanly close down the Discord session.
	dg.Close()
}

// This function will be called (due to AddHandler above) every time a new
// message is created on any channel that the authenticated bot has access to.
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {

	// Ignore all messages created by the bot itself
	// This isn't required in this specific example but it's a good practice.
	if m.Author.ID == s.State.User.ID {
		return
	}
	if strings.Contains(m.Content, "武器") {
		msg := "今回の狩猟は**" + buki_roulette((m.Content)) + "**で行うとしましょう!"
		s.ChannelMessageSend(m.ChannelID, msg)
	}
}

func random(min, max float64) float64 {
	rand.Seed(time.Now().UnixNano())
	return rand.Float64()*(max-min) + min
}

func allSum(list []float64) float64 {
	fmt.Println(list)
	sum := 0.0
	for i := 0; i < len(list); i++ {
		sum += list[i]
	}
	return sum
}

func nakagoGames() string {
	gameList := [...]string{"モンスターハンターライズ", "モンスターハンターワールド:アイスボーン", "モンスターハンターダブルクロス", "Grand Theft Auto V", "武器製造", "雑用", "ネットサーフィン"}
	return gameList[rand.Intn(len(gameList))]
}

func buki_roulette(comment string) string {
	raw, err := ioutil.ReadFile("./jsons/buki.json")
	if err != nil {
		fmt.Println(err.Error())
		return "err"
	}

	//mk json data
	var bl buki_list
	json.Unmarshal(raw, &bl)

	//select a weapon
	a := 0
	for _, name := range bl.People {
		if strings.Contains((comment), string(name.Name)) || name.Name == "normal" {
			rand := random(0, allSum(name.Param))
			for _, buki_param := range name.Param {
				rand -= buki_param
				if rand <= 0 {
					break
				}
				a += 1
			}
			break
		}
	}
	return bl.Bukis[a]
}
```
~~ライブラリそのまんまだね。~~

ライブラリから取ってきたping-pongをいじりまくった結果ですが、それでもここまでいくのに問題がいくつか発生していたので説明していきます。

#躓いていた点
##ping-pong再現まで編
discordgoの取得はここで終わります。

https://github.com/bwmarrin/discordgo

この中にpingと送信したらpongって帰ってくるというプログラムがあるのでそれを使えばコードは終了です。

そんなふうに考えていた時期が、俺にもありました。

うごきませんでした。
###modとsumという存在

初めて動かしたときにモジュールの問題が発生していることだけは分かりました。ですが、これがどうして発生しているかはわかりませんでした。そのためググって調べた結果、modとsumが存在していないことが問題だったことが分かりました。これらの意味については以下非常にわかりやすく説明されていた方がおられたので引用しておきます。

https://qiita.com/ko-watanabe/items/606cfff04438fffd15d0

要するに、Goではモジュールを使用する際に依存関係やライブラリの場所、バージョンを明らかにする必要があり、これらが不明なモジュールを扱おうとしていたためエラーを出していたことが判明しました。

##抽選機能を付けたいね編
ここでやっと"ping"を受け取ると"pong"と言えるナカゴくんになりました。ですが、本来やりたいことは抽選のため、抽選を行えるように修正を加えました。
そこで書いたプログラムが以下のようなモノです。

```Go:main.go
省略

type privateParam struct {
	name  string    `json:"name"`
	param []float64 `json:"param"`
}

type buki_list struct {
	bukis  []string       `json:"buki-list"`
	people []privateParam `json:"people-param"`
}

func init() {
	flag.StringVar(&Token, "t", "", "Bot Token")
	flag.Parse()
}

以下略
```

ちなみに、ここで引用しているbuki.jsonはこんな感じです

```json:buki.json
{
    "buki-list": [
        "大剣",
        "太刀",
        "片手剣",
        "双剣",
        "ハンマー",
        "狩猟笛",
        "ランス",
        "ガンランス",
        "スラアク",
        "チャアク",
        "虫棒",
        "ライト",
        "ヘビイ",
        "弓"
    ],
    "people-param": [
        {
          ︙
         中略
          ︙
        {
            "name": "normal",
            "param": [
                1,
                1,
                1,
                1,
                1,
                1,
                1,
                1,
                1,
                1,
                1,
                1,
                1,
                1
            ]
        }
    ]
}
```

「よっしゃ!これでいける!」そう思い、ウキウキで"go run main.go"と打ち込みました。

そう思っていた時期が、俺にもありました。(2回目)

うごきませんでした。

###構造体の定義の最初は大文字
調べると構造体の定義は必ず大文字ではなければならないという制約を知りました。つまり

```go:変更前
type privateParam struct {
	name  string    `json:"name"`
	param []float64 `json:"param"`
}

type buki_list struct {
	bukis  []string       `json:"buki-list"`
	people []privateParam `json:"people-param"`
}
```

を

```go:変更後
type privateParam struct {
	Name  string    `json:"name"`
	Param []float64 `json:"param"`
}

type buki_list struct {
	Bukis  []string       `json:"buki-list"`
	People []privateParam `json:"people-param"`
}
```

としなければいけませんでした。ここを変更すると

![スクリーンショット 2022-01-29 18.03.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484148/24245980-48b9-7e99-3bff-1619d36affef.png)

ついに動き出してくれました。

#まとめ

普段使用しているPythonとは異なり、厳密な定義を求められる言語であり、そうであるからこその大変さもあると自分の中では推察しました。その結果が今回のmodやsumといったものに出ていたり、構造体定義に現れているのではないかと考えています。
ただ、書き方がシンプルなため誰が扱っても似た内容になる言語である点は非常に面白く、管理の難易度が下げられるのではないかと考えています。
あとは調べていく中で資料が圧倒的に少ないなと感じました。そのため、簡単なものをつくるにしても初学者にとっては茨の道だったりもする反面、プログラムの勉強としては非常に面白かったと感じます。
今後はHerokuとか使ってクラウドサービス上で運用でもしようかなーと考えていたりします。