Падручнік па Go і API фільмаў
Створыце на Go кансольную ўтыліту, якая пералічвае фільмы з найвышэйшым рэйтынгам для жанру і года выхаду. Яна дэкадуе JSON у тыпізаваныя структуры, паўтарае запыт пасля 429 праз затрымку Retry-After і кэшуе адказы на дыску. Выкарыстоўвае толькі стандартную бібліятэку, патрабуе Go 1.22 або новей і займае каля 20 хвілін.
Вы створыце topfilms, кансольную ўтыліту на Go. Запусціце go run . -genre 878 -year 2010 -n 10, і яна надрукуе дзесяць фільмаў навуковай фантастыкі 2010 года з найвышэйшым рэйтынгам з ацэнкай і колькасцю галасоў. Яна выкарыстоўвае тыпізаваныя структуры, толькі стандартную бібліятэку і Go 1.22 або новей. Закладзіце 20 хвілін.
Крок 1: Атрымайце ключ API
Пачніце са старонкі цэны, дзе вы абіраеце перыяд з аплатай наперад і адзін з планаў на 20, 40 або 120 запытаў у хвіліну. Потым стварыце ключ у асабістым кабінеце і экспартуйце яго:
Праграма чытае яго праз os.Getenv("AMODB_KEY"), таму ён ніколі не трапляе ў зыходнікі.
export AMODB_KEY="YOUR_API_KEY"Крок 2: Стварыце модуль
Стварыце тэчку і ініцыялізуйце модуль. Нічога спампоўваць не трэба.
go version павінна надрукаваць 1.22 або вышэй. Увесь код ніжэй змяшчайце ў файл main.go.
mkdir topfilms && cd topfilms
go mod init topfilms
go versionКрок 3: Адпраўце першы запыт
Утыліта выклікае GET /3/discover/movie, які фільтруе і сартуе каталог. Жанр 878 гэта Science Fiction, а поўны спіс id ёсць на старонцы жанры. Гэтая поўная праграма толькі друкуе радок статусу.
Ніколі не выкарыстоўвайце http.DefaultClient для выклікаў API, бо ў яго няма тайм-аўту.
package main
import (
"fmt"
"net/http"
"os"
"time"
)
func main() {
url := "https://api.amodb.dev/3/discover/movie?with_genres=878"
req, _ := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("AMODB_KEY"))
res, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
fmt.Println(res.Status)
}Крок 4: Дэкадуйце ў тыпізаваныя структуры
Абвясціце толькі тыя палі, якія чытаеце. encoding/json ігнаруе астатнія, таму новыя палі ў адказе не могуць зламаць утыліту.
Запыт патрабуе sort_ і vote_count.gte=500. Без ніжняй мяжы галасоў верх спісу заняліся б назвы з адзіным галасам на 10 зорак. Кожны фільтр апісаны на старонцы пошук па фільтрах.
type Movie struct {
ID int `json:"id"`
Title string `json:"title"`
ReleaseDate string `json:"release_date"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
}
type DiscoverPage struct {
TotalResults int `json:"total_results"`
Results []Movie `json:"results"`
}Крок 5: Паўтор пры 429 і паведамленні пра памылкі
Прачытайце цела адзін раз, потым вырашайце па кодзе статусу.
| Статус | Што робіць кліент |
|---|---|
| 200 | Запісвае файл кэшу і дэкадуе цела. |
| 401 | Вяртае памылку з назвай AMODB_KEY. Без паўтору, бо наступны выклік таксама збоіць. |
| 429 | Спіць Retry-After секунд (1, калі загалоўка няма, не болей за 30) і спрабуе зноў, да 3 спроб. |
| Іншыя 4xx і 5xx | Вяртае status_code і status_message з цела памылкі TMDB. |
| Памылка транспарту | Вяртае network error, абгорнутую праз %w. |
Дапаможная функцыя затрымкі невялікая:
Усе коды гл. на старонцы памылкі, а слізгальнае акно на старонцы ліміты запытаў.
func retryDelay(header string) time.Duration {
seconds, err := strconv.Atoi(header)
if err != nil || seconds < 1 {
seconds = 1
}
return time.Duration(min(seconds, 30)) * time.Second
}Крок 6: Кэшуйце адказы на дыску
Edge трымае спісы discover 30 хвілін. Кліент захоўвае кожнае паспяховае цела ў кэшавай тэчцы карыстальніка пад імем SHA-256 ад поўнага URL і аддае яго зноў, пакуль файл маладзейшы за TTL. Паўторная каманда тады не робіць запыту.
Крок 7: Поўная праграма
Замяніце main.go гэтым і запусціце go run . -genre 878 -year 2010 -n 10.
Паспрабуйце іншыя фільтры праз сцяжкі. go run . -genre 18 -year 1999 -n 5 пералічвае пяць драм 1999 года з найвышэйшым рэйтынгам. Запусціце тую ж каманду двойчы, і другі запуск адкажа з файла кэшу, не звяртаючыся да сеткі, пакуль не пройдзе 30 хвілін. Калі для вашай камбінацыі ні ў аднаго фільма няма 500 галасоў, утыліта друкуе No movies matched. і завяршаецца нармальна.
Цыкл паўтораў вяртаецца з сярэдзіны switch у кожным выпадку, акрамя 429, у якога яшчэ ёсць спробы. Таму функцыя заканчваецца простай памылкай: кампілятару патрэбны return пасля цыкла, а паведамленне з’явілася б, толькі калі б лагічны код цыкла змянілі.
package main
import (
"crypto/sha256"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"time"
)
const baseURL = "https://api.amodb.dev/3"
type Movie struct {
ID int `json:"id"`
Title string `json:"title"`
ReleaseDate string `json:"release_date"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
}
type DiscoverPage struct {
TotalResults int `json:"total_results"`
Results []Movie `json:"results"`
}
type apiError struct {
StatusCode int `json:"status_code"`
StatusMessage string `json:"status_message"`
}
type Client struct {
key string
hc *http.Client
cacheDir string
}
func NewClient(key string) (*Client, error) {
base, err := os.UserCacheDir()
if err != nil {
base = os.TempDir()
}
dir := filepath.Join(base, "amodb-top-films")
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
return &Client{key: key, hc: &http.Client{Timeout: 10 * time.Second}, cacheDir: dir}, nil
}
func (c *Client) Get(path string, query url.Values, ttl time.Duration, out any) error {
endpoint := baseURL + path + "?" + query.Encode()
cacheFile := filepath.Join(c.cacheDir, fmt.Sprintf("%x.json", sha256.Sum256([]byte(endpoint))))
if info, err := os.Stat(cacheFile); err == nil && time.Since(info.ModTime()) < ttl {
if body, err := os.ReadFile(cacheFile); err == nil {
return json.Unmarshal(body, out)
}
}
for attempt := 1; attempt <= 3; attempt++ {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.key)
res, err := c.hc.Do(req)
if err != nil {
return fmt.Errorf("network error: %w", err)
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return fmt.Errorf("reading reply: %w", err)
}
switch {
case res.StatusCode == http.StatusOK:
_ = os.WriteFile(cacheFile, body, 0o644)
return json.Unmarshal(body, out)
case res.StatusCode == http.StatusTooManyRequests && attempt < 3:
time.Sleep(retryDelay(res.Header.Get("Retry-After")))
default:
return statusError(res.StatusCode, body)
}
}
return errors.New("retries exhausted")
}
func retryDelay(header string) time.Duration {
seconds, err := strconv.Atoi(header)
if err != nil || seconds < 1 {
seconds = 1
}
return time.Duration(min(seconds, 30)) * time.Second
}
func statusError(status int, body []byte) error {
if status == http.StatusUnauthorized {
return errors.New("401: the key is invalid or suspended, check AMODB_KEY")
}
var e apiError
if json.Unmarshal(body, &e) == nil && e.StatusMessage != "" {
return fmt.Errorf("amodb returned %d (status_code %d): %s", status, e.StatusCode, e.StatusMessage)
}
return fmt.Errorf("amodb returned %d", status)
}
func yearOf(date string) string {
if len(date) >= 4 {
return date[:4]
}
return "n/a"
}
func main() {
genre := flag.Int("genre", 878, "genre id, 878 is Science Fiction")
year := flag.Int("year", 2010, "primary release year")
limit := flag.Int("n", 10, "how many movies to print, 1 to 20")
flag.Parse()
key := os.Getenv("AMODB_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "Set AMODB_KEY first.")
os.Exit(1)
}
client, err := NewClient(key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
query := url.Values{}
query.Set("language", "en-US")
query.Set("with_genres", strconv.Itoa(*genre))
query.Set("primary_release_year", strconv.Itoa(*year))
query.Set("sort_by", "vote_average.desc")
query.Set("vote_count.gte", "500")
var page DiscoverPage
if err := client.Get("/discover/movie", query, 30*time.Minute, &page); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
if len(page.Results) == 0 {
fmt.Println("No movies matched.")
return
}
fmt.Printf("%d movies match, best first:\n", page.TotalResults)
for i, m := range page.Results {
if i >= *limit {
break
}
fmt.Printf("%2d. %s (%s) %.1f from %d votes\n", i+1, m.Title, yearOf(m.ReleaseDate), m.VoteAverage, m.VoteCount)
}
}Наступныя крокі
- Дадайце сцяжок
-pageі перадайце яго як параметрpage, каб ісці далей па спісе. - Вызначце кожны id фільма праз фільмы і дадайце
append_, каб прачытаць ацэнку IMDb зto_ response= ratings ratings.imdb.value. - Запускайце некалькі жанраў адначасова праз goroutine, трымаючыся ніжэй за запыты ў хвіліну вашага плана.
- Перанясіце тую ж ідэю на Java або прачытайце хуткі старт.