【Golang】海外サッカーリーグの試合情報を取得できるAPI「football-data.org」を使ってみた。

# はじめに

私があまり知らないだけかもしれないが、
海外サッカーリーグの試合情報（スケジュールや各種スタッツ等）を取り扱う国産サービスってあまり無い気がします。
（大手だと[Sportsnavi](https://soccer.yahoo.co.jp/ws/)くらいしか知らない。。）

海外で有名なサービスだと
[Sofascore](https://www.sofascore.com/)
[WhoScored](https://www.whoscored.com/)
[transfermarkt](https://www.transfermarkt.com/)　※移籍情報がメイン

など沢山ありますが、それらサービスは独自に試合情報を収集しているか、
または外部の試合情報取得用のAPIから取得していると思われます。

今回は、外部の試合情報取得APIを使って
イングランド・プレミアリーグの試合情報
（APIコール時点の、第○節分の試合スケジュール、順位表、ゴールスコア上位１０名）
を取得してWEB画面に表示させてみようと思います。

（私の勉強がてらに）Golangを使って実装してみます。
※ツッコミどころが多々あるかもしれませんが、ご了承ください。

# 対象読者
・WEBAPIを何となくでも理解している人
・Golang経験者、初学者
・海外サッカーが好きな人

# 主な使用技術
・Golang v1.17
・GJSON v1.14.1

# 使用するAPIについて

[football-data.org](https://www.football-data.org/)
各国サッカーリーグの試合情報をJSON形式で取得できます。

※無償版、有償版があるようです。
　今回は無償版を使用しますが、以下のように機能が制限されます。
　・取得データは、12のコンペティション（リーグ）のみ
　・取得データは、基本データのみ（スケジュール、試合結果、一部スタッツ）
　・APIコールは1分間に10回まで

※APIのリファレンスが以下にあります。ご参考までに。
[football-data.org - API Reference Documentation](https://docs.football-data.org/general/v4/index.html)

### 事前準備
APIを使用するため、アカウントを登録します。
<B>①</B> 右上「Get started」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1184745/8d3f63b7-3bfd-0578-a497-047ce8a2146f.png)

<B>②</B> 名前（適当でOK）とメールアドレスを入力します。
「Choose ur weapon if you like to:」は「Golang」を選択し、「Create account」をクリック。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1184745/a40a85fc-fdc3-601d-47f5-7b033f028c63.png">

<B>③</B>　②で入力したメールアドレス宛にAPIトークンが送られたことを確認する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1184745/c1204320-5fff-d4b6-5dfe-2edac6939951.png)

これで事前準備はOK。

# 実装

### 構成
構成は以下の通りとしました。
```
football
├── assets
│   ├── css
│   │   └── styles.css
│   ├── index.html 　　　　　　　　　　　　　　　　　　　　　　　　　　　　　 // 試合情報の表示画面
│   └── js
│       └── jquery-3.6.0.min.js
├── cmd
│   ├── main.go　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　 　// サーバー＆画面レンダリング機能
│   └── match
│       ├── getData.go　　　　　　　　　　　　　　　　　　　　　　　 // 試合情報取得
│       ├── schedule.go　　　　　　　　　　　　　　　　　　　　　　　// 試合スケジュール取得　（getData.goから呼ばれる）
│       ├── score.go　　　　　　　　　　　　　　　　　　　　　　　　　　　　　// ゴールスコア取得　（getData.goから呼ばれる）
│       └── standings.go　　　　　　　　　　　　　　　　　　　　　// 順位表取得　（getData.goから呼ばれる）
├── go.mod
└── go.sum
```

### フロントエンドの実装

css、jsについては、以下のサイトから入手したものを使用しています。
[Start Bootstrap - Scrolling Nav v5.0.4](https://startbootstrap.com/template/scrolling-nav)

htmlも、各自適当にご用意ください。サンプルを置いておきます↓
<details>
<summary><b>index.htmlを表示</b></summary>

```
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <link href="assets/css/styles.css" rel="stylesheet"/>
        <title>{{ .Title}}</title>
    </head>
    <div id="page-top">
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top" id="mainNav">
            <div class="container px-4">
                <a class="navbar-brand" href="/">{{ .Title}}</a>
            </div>
        </nav>
    </div>
    <body>
        <!-- schedule section-->
        <section id="schedule">
            <div class="container px-4">
                <div class="row gx-4 justify-content-center">
                    <div class="col-lg-8">
                        <h2>イングランド・プレミアリーグ{{(index .MatchList 0).StartDate}}-{{(index .MatchList 0).EndDate}}シーズン 第{{(index .MatchList 0).CurrentMatchday}}節 試合日程</h2>
                        <table >
                            <thead>
                                <tr align="center">
                                    <th width="40%">ホーム</th>
                                    <th width="20%">試合開始日時(日本時間)</th>
                                    <th width="40%">アウェイ</th>
                                </tr>
                            </thead>
                            {{ range .MatchList }}
                            {{ if eq .CurrentMatchday .Matchday}}
                            <tbody>
                                <tr align="center">
                                    <td width="40%"><img src="https://crests.football-data.org/{{.HomeTeamId}}.svg" height="50"></br>{{.HomeTeamName}}</td>
                                    <td width="20%"><div class="text-center">{{.Month}}/{{.Day}}({{.WDays}})</div><div class="text-center">{{.StartTime}}</div><p class="fw-bolder fs-1">{{.HomeScore}} - {{.AwayScore}}</p></td>
                                   <td width="40%"><img src="https://crests.football-data.org/{{.AwayTeamId}}.svg" height="50"></br>{{.AwayTeamName}}</td>
                                </tr>
                            </tbody>
                            {{ end }}
                            {{ end }}
                        </table>
                    </div>
                </div>
            </div>
        </section>
        <!-- standings section-->
        <section id="standings">
            <div class="container px-4">
                <div class="row gx-4 justify-content-center">
                    <div class="col-lg-8">
                        <h2>イングランド・プレミアリーグ{{(index .MatchList 0).StartDate}}-{{(index .MatchList 0).EndDate}}シーズン 順位表</h2>
                        <table>
                            <thead>
                                <tr align="left">
                                    <th width="10%">順位</th>
                                    <th width="40%">チーム名</th>
                                    <th width="10%">試合数</th>
                                    <th width="10%">勝</th>
                                    <th width="10%">引</th>
                                    <th width="10%">負</th>
                                    <th width="10%">Pts</th>
                                </tr>
                            </thead>
                            {{ range .StandingsList }}
                            <tbody>
                                <tr align="left">
                                    <th width="10%">{{ .Position}}</th>
                                    <th width="40%"><img src="https://crests.football-data.org/{{.TeamId}}.svg" height="20">　{{ .TeamName}}</th>
                                    <th width="10%">{{ .PlayedGames}}</th>
                                    <th width="10%">{{ .Won}}</th>
                                    <th width="10%">{{ .Draw}}</th>
                                    <th width="10%">{{ .Lost}}</th>
                                    <th width="10%">{{ .Points}}</th>
                                </tr>
                            </tbody>
                            {{ end }}
                        </table>
                    </div>
                </div>
            </div>
        </section>
        <!-- scorers section-->
        <section id="scorers">
            <div class="container px-4">
                <div class="row gx-4 justify-content-center">
                    <div class="col-lg-8">
                        <h2>イングランド・プレミアリーグ{{(index .MatchList 0).StartDate}}-{{(index .MatchList 0).EndDate}}シーズン ゴールスコア</h2>
                        <table>
                            <thead>
                                <tr align="left">
                                    <th width="10%">順位</th>
                                    <th width="25%">選手名</th>
                                    <th width="25%">国籍</th>
                                    <th width="30%">所属チーム名</th>
                                    <th width="10%">ゴール数</th>
                                </tr>
                            </thead>
                            {{ range .ScoreList }}
                            <tbody>
                                <tr align="left">
                                    <th width="10%">{{.Standings}}</th>
                                    <th width="25%">{{.PlayerName}}</th>
                                    <th width="25%">{{.Nationality}}</th>
                                    <th width="30%"><img src="https://crests.football-data.org/{{.TeamId}}.svg" height="20">　{{ .TeamName}}</th>
                                    <th width="10%">{{.Goals}}</th>
                                </tr>
                            </tbody>
                            {{ end }}
                        </table>
                    </div>
                </div>
            </div>
        </section>
        <div class="container mb-4 text-center">
            <a class="btn btn-sm btn-success" href="#page-top">ページトップに戻る</a>
        </div>
        <!-- Bootstrap core JS-->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        <!-- Core theme JS-->
        <script src="assets/js/scripts.js"></script>
        <!-- Font Awesome -->
        <script src="https://kit.fontawesome.com/e6a049339c.js" crossorigin="anonymous"></script>
    </body>
</html>

```
</details>

### バックエンドの実装
main.go
```
package main

import (
	"football/cmd/match"
	"log"
	"net/http"
	"os"
	"strings"
	"text/template"
)

func main() {

	// ルーティングパス設定
	dir, _ := os.Getwd()
	dir2 := strings.ReplaceAll(dir, "/cmd", "")

	// ルーティング
	http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir(dir2+"/assets"))))
	http.HandleFunc("/", index)

	// ListenAndServe
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

// メイン画面表示
func index(w http.ResponseWriter, r *http.Request) {

	//　試合情報取得
	p := match.GetMatchData(r, false)

	// 画面レンダリング
	renderTemplate(w, "index", p)
}

// 画面レンダリング
func renderTemplate(w http.ResponseWriter, tmpl string, p match.Page) {
	t := template.Must(template.ParseFiles(
		"../assets/" + tmpl + ".html",
	))
	t.ExecuteTemplate(w, tmpl+".html", p)
}
```
getData.go
```
package match

import (
	"encoding/json"
	"io/ioutil"
	"log"
	"net/http"
)

type Page struct {
	Title         string
	SubTitle      string
	MatchList     MatchLists
	ScoreList     ScoreLists
	StandingsList StandingsLists
}

const (
	SITE_TITLE string = "LIVESCORE"
	BLUNK      string = ""
)

func (p Page) MarshalBinary() ([]byte, error) {
	return json.Marshal(p)
}

// 試合データ取得
func GetMatchData(r *http.Request, fstLoginFlg bool) Page {

	c1 := make(chan MatchLists)
	c2 := make(chan ScoreLists)
	c3 := make(chan StandingsLists)

	// スケジュール取得
	go Schedule(c1)
	// ゴールスコア取得
	go Score(c2)
	// 順位表取得
	go Standings(c3)

	//　取得結果を一つにまとめる
	p := Page{SITE_TITLE, BLUNK, <-c1, <-c2, <-c3}

	return p
}

func ApiCall(param string) []byte {

	url := "https://api.football-data.org/v4/competitions/PL/" + param
	authHeaderName := "X-Auth-Token"
	authHeaderValue := "APIトークンをここに入力"

	req, _ := http.NewRequest(http.MethodGet, url, nil)
	req.Header.Set(authHeaderName, authHeaderValue)

	client := new(http.Client)
	resp, err := client.Do(req)
	if err != nil {
		log.Println("Error Request:", err.Error())
	}

	if resp.StatusCode != 200 {
		log.Println("Error Response:", resp.Status)
	}

	defer resp.Body.Close()

	byteArray, _ := ioutil.ReadAll(resp.Body)
	jsonBytes := ([]byte)(byteArray)

	return jsonBytes
}
```
※変数「authHeaderValue」に、事前準備で入手したAPIトークンを設定ください。


schedule.go
```
package match

import (
	"fmt"
	"strconv"
	"time"

	"github.com/tidwall/gjson"
)

type MatchList struct {
	Month           int
	Day             int
	WDays           string
	StartTime       string
	HomeTeamId      int
	AwayTeamId      int
	HomeTeamName    string
	AwayTeamName    string
	CurrentMatchday int
	StartDate       string
	EndDate         string
	Matchday        int
	HomeScore       int
	AwayScore       int
}

type GetSecretValueOutput struct {
	SecretString string
}

type MatchLists []MatchList

// スケジュール取得
func Schedule(ch chan MatchLists) {

	var rtnLists MatchLists

	jsonBytes := ApiCall("matches")
	games_num := int(gjson.GetBytes(jsonBytes, "resultSet.count").Int())
	wdays := []string{"日", "月", "火", "水", "木", "金", "土"}
	jst, _ := time.LoadLocation("Asia/Tokyo")

	for i := 1; i < games_num; i++ {

		arg := "matches." + strconv.Itoa(i)
		jdate := gjson.GetBytes(jsonBytes, arg+".utcDate").Time().In(jst)
		hour := fmt.Sprintf("%02d", jdate.Hour())
		minute := fmt.Sprintf("%02d", jdate.Minute())
		startTime := hour + ":" + minute

		homeTeamId := int(gjson.GetBytes(jsonBytes, arg+".homeTeam.id").Int())
		awayTeamId := int(gjson.GetBytes(jsonBytes, arg+".awayTeam.id").Int())
		homeTeamName := gjson.GetBytes(jsonBytes, arg+".homeTeam.name").String()
		awayTeamName := gjson.GetBytes(jsonBytes, arg+".awayTeam.name").String()
		currentMatchday := int(gjson.GetBytes(jsonBytes, arg+".season.currentMatchday").Int())
		startDate := gjson.GetBytes(jsonBytes, arg+".season.startDate").String()
		endDate := gjson.GetBytes(jsonBytes, arg+".season.endDate").String()
		matchday := int(gjson.GetBytes(jsonBytes, arg+".matchday").Int())
		homeScore := int(gjson.GetBytes(jsonBytes, arg+".score.fullTime.home").Int())
		awayScore := int(gjson.GetBytes(jsonBytes, arg+".score.fullTime.away").Int())

		rtnList := MatchList{
			int(jdate.Month()),
			int(jdate.Day()),
			wdays[jdate.Weekday()],
			startTime,
			homeTeamId,
			awayTeamId,
			homeTeamName,
			awayTeamName,
			currentMatchday,
			startDate[2:4],
			endDate[2:4],
			matchday,
			homeScore,
			awayScore}

		rtnLists = append(rtnLists, rtnList)
	}
	ch <- rtnLists
}
```
※APIから受け取ったJSONデータから試合スケジュール（APIコール時点の第◯節分）を抜き出しています。
※抜き出す際は「[GJSON](https://github.com/tidwall/gjson)」と呼ばれるGoライブラリでJSONデータをパースしています。

score.go

```
package match

import (
	"strconv"

	"github.com/tidwall/gjson"
)

type ScoreList struct {
	Standings   int
	PlayerName  string
	Nationality string
	TeamId      int
	TeamName    string
	Goals       string
}

type ScoreLists []*ScoreList

// ゴールスコア取得
func Score(ch chan ScoreLists) {

	var rtnLists ScoreLists

	jsonBytes := ApiCall("scorers")
	games_num := int(gjson.GetBytes(jsonBytes, "count").Int())

	for i := 0; i < games_num; i++ {

		arg := "scorers." + strconv.Itoa(i)

		playerName := gjson.GetBytes(jsonBytes, arg+".player.name").String()
		nationality := gjson.GetBytes(jsonBytes, arg+".player.nationality").String()
		teamId := int(gjson.GetBytes(jsonBytes, arg+".team.id").Int())
		teamName := gjson.GetBytes(jsonBytes, arg+".team.name").String()
		goals := gjson.GetBytes(jsonBytes, arg+".goals").String()

		rtnList := ScoreList{
			i + 1,
			playerName,
			nationality,
			teamId,
			teamName,
			goals}

		rtnLists = append(rtnLists, &rtnList)
	}
	ch <- rtnLists
}
```
※APIから受け取ったJSONデータからゴールスコア（APIコール時点のゴールスコア上位１０名分）を抜き出しています。
※こちらも同様に「[GJSON](https://github.com/tidwall/gjson)」と呼ばれるGoライブラリでJSONデータをパースしています。

standings.go
```
package match

import (
	"strconv"

	"github.com/tidwall/gjson"
)

type StandingsList struct {
	Position    int
	TeamId      int
	TeamName    string
	PlayedGames int
	Won         int
	Draw        int
	Lost        int
	Points      int
}

type StandingsLists []StandingsList

// 順位表取得
func Standings(ch chan StandingsLists) {

	var rtnLists StandingsLists
	jsonBytes := ApiCall("standings")

	for i := 0; i < 20; i++ {

		arg := "standings.0.table." + strconv.Itoa(i)

		position := int(gjson.GetBytes(jsonBytes, arg+".position").Int())
		teamId := int(gjson.GetBytes(jsonBytes, arg+".team.id").Int())
		teamName := gjson.GetBytes(jsonBytes, arg+".team.name").String()
		playedGames := int(gjson.GetBytes(jsonBytes, arg+".playedGames").Int())
		won := int(gjson.GetBytes(jsonBytes, arg+".won").Int())
		draw := int(gjson.GetBytes(jsonBytes, arg+".draw").Int())
		lost := int(gjson.GetBytes(jsonBytes, arg+".lost").Int())
		points := int(gjson.GetBytes(jsonBytes, arg+".points").Int())

		rtnList := StandingsList{
			position,
			teamId,
			teamName,
			playedGames,
			won,
			draw,
			lost,
			points}

		rtnLists = append(rtnLists, rtnList)
	}
	ch <- rtnLists
}
```
※APIから受け取ったJSONデータからゴールスコア（APIコール時点の第○節までの順位）を抜き出しています。
※こちらも同様に「[GJSON](https://github.com/tidwall/gjson)」と呼ばれるGoライブラリでJSONデータをパースしています。

# 稼働確認
ターミナルからmain.goを実行する。
```
go run main.go
```
ブラウザからローカルホストにアクセスする。
```
http://localhost:8080/
```

以下、稼働結果
試合スケジュール
![スクリーンショット 2023-02-20 7.58.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1184745/5b0107cf-b43a-7665-d92b-59f59ee69a93.png)
順位表
![スクリーンショット 2023-02-20 7.59.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1184745/a71fa84c-f507-67e5-1bca-1955ae1fb2d1.png)
ゴールスコア
![スクリーンショット 2023-02-20 8.00.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1184745/120e383f-be39-48a7-dc76-9428fb4650b6.png)

# 最後に
リアルタイムに情報を取得してくれますし、個人で使う分には良いかもしれません。
ただ、無償版はAPIコール回数にキツイ制限があるため、そこのところを気をつけましょう。
（処理を工夫or有償版or他API検討などして）