# 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](https://amodb.dev/ro/pricing/), 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:

```bash
export AMODB_KEY="YOUR_API_KEY"
```

Programul o citește cu `os.Getenv("AMODB_KEY")`, deci nu ajunge niciodată în codul sursă.

## Pasul 2: creează modulul

Fă un folder și inițializează un modul. Nu e nimic de descărcat.

```bash
mkdir topfilms && cd topfilms
go mod init topfilms
go version
```

`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`.

## Pasul 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](https://amodb.dev/ro/genres/). Acest program complet doar afișează linia de stare.

```go
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)
}
```

Nu folosi niciodată `http.DefaultClient` pentru apeluri API, pentru că nu are timeout.

## 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.

```go
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"`
}
```

Interogarea cere `sort_by=vote_average.desc` ș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](https://amodb.dev/ro/discover/).

## 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ă:

```go
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
}
```

Vezi [erori](https://amodb.dev/ro/errors/) pentru fiecare cod și [limite de cereri](https://amodb.dev/ro/rate-limits/) pentru fereastra glisantă.

## 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`.

```go
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)
	}
}
```

Î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.

## Pașii următori

- Adaugă un flag `-page` și transmite-l ca parametru `page` ca să parcurgi mai adânc lista.
- Rezolvă fiecare ID de film cu [filme](https://amodb.dev/ro/movies/) și adaugă `append_to_response=ratings` ca să citești scorul IMDb din `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](https://amodb.dev/ro/java-movie-api/) sau citește [ghidul de pornire rapidă](https://amodb.dev/ro/quickstart/).

## Întrebări și răspunsuri

### De ce versiune de Go are nevoie codul?

Go 1.22 sau mai nou. Folosește funcția min încorporată din Go 1.21, aliasul any și doar biblioteca standard, deci go.mod nu listează dependențe, iar go run . funcționează imediat după go mod init.

### De ce structuri tipizate în loc de map[string]any?

Structurile îți dau nume de câmp verificate la compilare și tipuri numerice reale, iar encoding/json sare peste câmpurile pe care nu le-ai declarat. Asta se potrivește unui API în care pot apărea câmpuri noi în timp, pentru că vechiul cod continuă să funcționeze.

### Cum așteaptă clientul la un 429?

Citește antetul Retry-After ca secunde întregi, revine la 1 când antetul lipsește, plafonează așteptarea la 30 și face cel mult 3 încercări în total. După aceea întoarce apelantului mesajul de stare.

### Pot trimite cereri din mai multe goroutine?

Da, dar fiecare apel se numără în continuare la cele 20, 40 sau 120 de cereri pe minut ale cheii tale. Distribuie apelurile cu un time.Ticker sau un canal cu buffer și lasă reîncercarea la 429 să prindă depășirea ocazională.

### Unde își ține instrumentul cache-ul?

Într-un folder numit amodb-top-films sub os.UserCacheDir, cu un fișier pe cerere, denumit după un hash SHA-256 al URL-ului. Un fișier este reutilizat 30 de minute, aceeași durată cât ține marginea listele discover.

## Pagini conexe

- [API discover](https://amodb.dev/ro/discover/): API discover cu filtrele TMDB v3. Listează filme și seriale după gen, distribuție, companie, cuvânt-cheie, an, evaluare, durată și furnizor, cu sortare.
- [Genuri și liste de referință](https://amodb.dev/ro/genres/): API listă de genuri cu căi TMDB v3: ID-uri de gen pentru filme și TV cu nume traduse, plus liste de țări, limbi, meserii, clasificări și furnizori de vizionare.
- [Limite de cereri](https://amodb.dev/ro/rate-limits/): amodb limitează fiecare cheie în cereri pe minut, în fereastră glisantă de 60 de secunde: 20, 40 sau 120. Citește antetele și tratează 429 cu Retry-After.
- [Erori](https://amodb.dev/ro/errors/): Fiecare eroare amodb, cu statusul HTTP, status_code TMDB și mesajul, plus forma erorii OMDb. Ce înseamnă fiecare și ce ar trebui să facă clientul tău.
- [Tutorial API filme Java](https://amodb.dev/ro/java-movie-api/): Ghid de episoade Java cu HttpClient din JDK și recorduri Jackson: preia un sezon, respectă Retry-After la 429 și salvează răspunsurile în fișiere. Java 17+.
- [Tutorial API filme PHP](https://amodb.dev/ro/php-movie-api/): Funcție PHP și o pagină de film care apelează API-ul amodb cu cURL, reîncearcă la 429, escapează ieșirea și salvează fiecare răspuns într-un fișier. PHP 8.2+.

Actualizat: 2026-10-18. Source: https://amodb.dev/ro/go-movie-api/
