Tutorial API filme Go
Construiești un instrument Go în linia de comandă care listează cele mai bine cotate filme după gen și an de lansare. Decodează JSON în structuri tipizate, reîncearcă un 429 după întârzierea din Retry-After și ține răspunsurile în cache pe disc. Folosește doar biblioteca standard, cere Go 1.22 sau mai nou și durează cam 20 de minute.
Vei construi topfilms, un instrument Go în linia de comandă. Rulează go run . -genre 878 -year 2010 -n 10 și afișează cele mai bine cotate zece filme științifico-fantastice lansate în 2010, cu scor și număr de voturi. Folosește structuri tipizate, doar biblioteca standard și Go 1.22 sau mai nou. Socotește 20 de minute.
Pasul 1: obține o cheie API
Începe de la prețuri, unde alegi o perioadă plătită în avans și unul dintre planurile de 20, 40 sau 120 de cereri pe minut. Apoi creează o cheie în panoul contului tău și exportă-o:
Programul o citește cu os.Getenv("AMODB_KEY"), deci nu ajunge niciodată în codul sursă.
export AMODB_KEY="YOUR_API_KEY"Pasul 2: creează modulul
Fă un folder și inițializează un modul. Nu e nimic de descărcat.
go version ar trebui să afișeze 1.22 sau mai mult. Pune tot codul de mai jos într-un fișier numit main.go.
mkdir topfilms && cd topfilms
go mod init topfilms
go versionPasul 3: trimite prima cerere
Instrumentul apelează GET /3/discover/movie, care filtrează și sortează catalogul. Genul 878 este Science Fiction, iar lista completă de ID-uri este la genuri. Acest program complet doar afișează linia de stare.
Nu folosi niciodată http.DefaultClient pentru apeluri API, pentru că nu are timeout.
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)
}Pasul 4: decodează în structuri tipizate
Declară doar câmpurile pe care le citești. encoding/json le ignoră pe restul, așa că noile câmpuri din răspuns nu pot strica instrumentul.
Interogarea cere sort_ și vote_count.gte=500. Fără pragul de voturi, titlurile cu un singur vot de 10 stele ar conduce lista. Fiecare filtru este documentat la descoperire.
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"`
}Pasul 5: reîncearcă la 429 și raportează erorile
Citește corpul o singură dată, apoi decide după codul de stare.
| Stare | Ce face clientul |
|---|---|
| 200 | Scrie fișierul de cache și decodează corpul. |
| 401 | Întoarce o eroare care numește AMODB_KEY. Fără reîncercare, pentru că și următorul apel ar eșua. |
| 429 | Doarme Retry-After secunde (1 dacă lipsește, cel mult 30) și încearcă din nou, până la 3 încercări. |
| Alte 4xx și 5xx | Întoarce status_code și status_message din corpul de eroare TMDB. |
| Eroare de transport | Întoarce network error învelit cu %w. |
Funcția ajutătoare pentru întârziere este mică:
Vezi erori pentru fiecare cod și limite de cereri pentru fereastra glisantă.
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
}Pasul 6: păstrează răspunsurile în cache pe disc
Marginea ține listele discover 30 de minute. Clientul salvează fiecare corp reușit în folderul de cache al utilizatorului, denumit după SHA-256 al URL-ului complet, și îl servește din nou cât timp fișierul este mai tânăr decât TTL. O comandă repetată nu mai face atunci nicio cerere.
Pasul 7: programul complet
Înlocuiește main.go cu acesta și rulează go run . -genre 878 -year 2010 -n 10.
Încearcă alte filtre cu flag-urile. go run . -genre 18 -year 1999 -n 5 listează cele mai bine cotate cinci drame din 1999. Rulează aceeași comandă de două ori, iar a doua rulare răspunde din fișierul de cache fără să atingă rețeaua, până trec 30 de minute. Dacă niciun film nu are 500 de voturi pentru combinația ta, instrumentul afișează No movies matched. și iese normal.
Bucla de reîncercare returnează din interiorul lui switch în fiecare caz, cu excepția unui 429 care mai are încercări. De aceea funcția se încheie cu o eroare simplă: compilatorul cere un return după buclă, iar mesajul ar apărea doar dacă s-ar schimba logica buclei.
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)
}
}Pașii următori
- Adaugă un flag
-pageși transmite-l ca parametrupageca să parcurgi mai adânc lista. - Rezolvă fiecare ID de film cu filme și adaugă
append_ca să citești scorul IMDb dinto_ response= ratings ratings.imdb.value. - Rulează mai multe genuri deodată cu goroutine și ține-le sub cererile pe minut ale planului tău.
- Portează aceeași idee în Java sau citește ghidul de pornire rapidă.