Ich habe eine API mit Go und PostgreSQL erstellt, eine CI/CD-Pipeline mit Google Cloud Run, Cloud Build, Secret Manager und Artifact Registry eingerichtet und die Cloud Run-Instanz mit CockroachDB verbunden.
Die API basiert auf dem Spiel Crisis Core: Final Fantasy VII, um „Materia Fusion“ zu simulieren. Die Zielgruppe dieses Artikels sind Entwickler, die lediglich wissen möchten, wie die API erstellt und bereitgestellt wird. Ich habe einen weiteren Artikel, in dem ich über alles spreche, was ich bei der Arbeit an diesem Projekt gelernt habe, was nicht funktioniert hat und über das Verständnis und die Übersetzung der Materia-Fusion-Regeln des Spiels (Link folgt in Kürze).
3 Endpunkte – Gesundheitsprüfung (GET), Liste aller Materia (GET) und Simulation der Materia-Fusion (POST)
Materia (sowohl Singular als auch Plural) ist eine Kristallkugel, die als Quelle der Magie dient. Es gibt 144 verschiedene Materia im Spiel, die grob in vier Kategorien eingeteilt werden: „Magie“, „Befehl“, „Unterstützung“ und „Unabhängig“. Um jedoch die Regeln der Materia-Fusion herauszufinden, war es einfacher, 32 interne Kategorien basierend auf ihrem Fusionsverhalten und 8 Grade innerhalb dieser Kategorien zu haben (siehe Referenz). .
Eine Materia wird „gemeistert“, wenn sie für eine bestimmte Dauer verwendet wird. Die Dauer ist hier nicht wichtig.
Am wichtigsten ist, dass zwei Materia zu einer neuen Materia verschmolzen werden können. Die Fusionsregeln werden beeinflusst durch:
Und es gibt VIELE Ausnahmen, wobei einige Regeln drei Ebenen verschachtelter If-Else-Logik haben. Dadurch entfällt die Möglichkeit, eine einfache Tabelle in der Datenbank zu erstellen und darin 1000 Regeln zu speichern oder sich eine Formel auszudenken, um sie alle zu beherrschen.
Kurz gesagt, wir brauchen:
Idealerweise können Sie die Datenbank von der Website selbst installieren. Aber das pgAdmin-Tool konnte aus irgendeinem Grund keine Verbindung zur Datenbank herstellen, also habe ich Homebrew verwendet.
brew install postgresql@17
Dadurch wird eine ganze Reihe von CLI-Binärdateien installiert, um die Verwendung der Datenbank zu erleichtern.
Optional: /opt/homebrew/opt/postgresql@17/bin zur Variablen $PATH hinzufügen.
# create the DB createdb materiafusiondb # step into the DB to perform SQL commands psql materiafusiondb
-- create an SQL user to be used by the Go server CREATE USER go_client WITH PASSWORD 'xxxxxxxx'; -- The Go server doesn't ever need to add data to the DB. -- So let's give it just read permission. CREATE ROLE readonly_role; GRANT USAGE ON SCHEMA public TO readonly_role; -- This command gives SELECT access to all future created tables. ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_role; -- If you want to be more strict and give access only to tables that already exist, use this: -- GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role; GRANT readonly_role TO go_client;
CREATE TYPE display_materia_type AS ENUM ('Magic', 'Command', 'Support', 'Independent'); CREATE TYPE materia_type AS ENUM ('Fire', 'Ice', 'Lightning', 'Restore', 'Full Cure', 'Status Defense', 'Defense', 'Absorb Magic', 'Status Magic', 'Fire & Status', 'Ice & Status', 'Lightning & Status', 'Gravity', 'Ultimate', 'Quick Attack', 'Quick Attack & Status', 'Blade Arts', 'Blade Arts & Status', 'Fire Blade', 'Ice Blade', 'Lightning Blade', 'Absorb Blade', 'Item', 'Punch', 'SP Turbo', 'HP Up', 'AP Up', 'ATK Up', 'VIT Up', 'MAG Up', 'SPR Up', 'Dash', 'Dualcast', 'DMW', 'Libra', 'MP Up', 'Anything'); CREATE TABLE materia ( id integer NOT NULL, name character varying(50) NOT NULL, materia_type materia_type NOT NULL, grade integer NOT NULL, display_materia_type display_materia_type, description text CONSTRAINT materia_pkey PRIMARY KEY (id) ); -- The primary key 'id' should auto-increment by 1 for every row entry. CREATE SEQUENCE materia_id_seq AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; ALTER SEQUENCE materia_id_seq OWNED BY materia.id; ALTER TABLE ONLY materia ALTER COLUMN id SET DEFAULT nextval('materia_id_seq'::REGCLASS);
Erstellen Sie eine Excel-Tabelle mit Tabellenkopf und Daten und exportieren Sie sie als CSV-Datei. Führen Sie dann den Befehl aus:
COPY materia(name,materia_type,grade,display_materia_type,description) FROM '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
Erstellen Sie den Boilerplate-Code mit autostrada.dev. Fügen Sie die Optionen API, Postgresql, httprouter, env var config, tinted logging, git, live reload und makefile hinzu. Am Ende erhalten wir eine Dateistruktur wie diese:
? codebase ├─ cmd │ └─ api │ ├─ errors.go │ ├─ handlers.go │ ├─ helpers.go │ ├─ main.go │ ├─ middleware.go │ └─ server.go ├─ internal │ ├─ database --- db.go │ ├─ env --- env.go │ ├─ request --- json.go │ ├─ response --- json.go │ └─ validator │ ├─ helpers.go │ └─ validators.go ├─ go.mod ├─ LICENSE ├─ Makefile ├─ README.md └─ README.html
Der Boilerplate-Generator hat Code erstellt, um Umgebungsvariablen abzurufen und sie dem Code hinzuzufügen, aber wir können es einfacher machen, die Werte zu verfolgen und zu aktualisieren.
Erstellen Sie die Datei
HTTP_PORT=4444 DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable API_TIMEOUT_SECONDS=5 API_CALLS_ALLOWED_PER_SECOND=1
Fügen Sie die Godotenv-Bibliothek hinzu:
go get github.com/joho/godotenv
Fügen Sie Folgendes zu main.go hinzu:
// At the beginning of main(): err := godotenv.Load(".env") // Loads environment variables from .env file if err != nil { // This will be true in prod, but that's fine. fmt.Println("Error loading .env file") } // Modify config struct: type config struct { baseURL string db struct { dsn string } httpPort int apiTimeout int apiCallsAllowedPerSecond float64 } // Modify run() to use the new values from .env: cfg.httpPort = env.GetInt("HTTP_PORT") cfg.db.dsn = env.GetString("DB_DSN") cfg.apiTimeout = env.GetInt("API_TIMEOUT_SECONDS") cfg.apiCallsAllowedPerSecond = float64(env.GetInt("API_CALLS_ALLOWED_PER_SECOND")) // cfg.baseURL = env.GetString("BASE_URL") - not required
Das Boilerplate verfügt bereits über eine Middleware zur Wiederherstellung nach Paniken. Wir werden drei weitere hinzufügen: Inhaltstypprüfung, Ratenbegrenzung und API-Timeout-Schutz.
Mautstellenbibliothek hinzufügen:
go get github.com/didip/tollbooth
Aktualisieren Sie
func (app *application) contentTypeCheck(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Content-Type") != "application/json" { app.unsupportedMediaType(w, r) return } next.ServeHTTP(w, r) }) } func (app *application) rateLimiter(next http.Handler) http.Handler { limiter := tollbooth.NewLimiter(app.config.apiCallsAllowedPerSecond, nil) limiter.SetIPLookups([]string{"X-Real-IP", "X-Forwarded-For", "RemoteAddr"}) return tollbooth.LimitHandler(limiter, next) } func (app *application) apiTimeout(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { timeoutDuration := time.Duration(app.config.apiTimeout) * time.Second ctx, cancel := context.WithTimeout(r.Context(), timeoutDuration) defer cancel() r = r.WithContext(ctx) done := make(chan struct{}) go func() { next.ServeHTTP(w, r) close(done) }() select { case <-done: return case <-ctx.Done(): app.gatewayTimeout(w, r) return } }) }
Die Middleware muss zu den Routen hinzugefügt werden. Sie können entweder zu allen Routen oder zu bestimmten Routen hinzugefügt werden. In unserem Fall ist die Überprüfung des Inhaltstyps (d. h. die Vorgabe, dass die Eingabeheader den Inhaltstyp: application/json enthalten) nur für POST-Anfragen erforderlich. Ändern Sie also „routes.go“ wie folgt:
func (app *application) routes() http.Handler { mux := httprouter.New() mux.NotFound = http.HandlerFunc(app.notFound) mux.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowed) // Serve the Swagger UI. Uncomment this line later // mux.Handler("GET", "/docs/*any", httpSwagger.WrapHandler) mux.HandlerFunc("GET", "/status", app.status) mux.HandlerFunc("GET", "/materia", app.getAllMateria) // Adding content-type check middleware to only the POST method mux.Handler("POST", "/fusion", app.contentTypeCheck(http.HandlerFunc(app.fuseMateria))) return app.chainMiddlewares(mux) } func (app *application) chainMiddlewares(next http.Handler) http.Handler { middlewares := []func(http.Handler) http.Handler{ app.recoverPanic, app.apiTimeout, app.rateLimiter, } for _, middleware := range middlewares { next = middleware(next) } return next }
Fügen Sie die folgenden Methoden zu
func (app *application) unsupportedMediaType(w http.ResponseWriter, r *http.Request) { message := fmt.Sprintf("The %s Content-Type is not supported", r.Header.Get("Content-Type")) app.errorMessage(w, r, http.StatusUnsupportedMediaType, message, nil) } func (app *application) gatewayTimeout(w http.ResponseWriter, r *http.Request) { message := "Request timed out" app.errorMessage(w, r, http.StatusGatewayTimeout, message, nil) }
package main // MateriaDTO provides Materia details - Name, Description and Type (Magic / Command / Support / Independent) type MateriaDTO struct { Name string `json:"name" example:"Thunder"` Type string `json:"type" example:"Magic"` Description string `json:"description" example:"Shoots lightning forward dealing thunder damage."` } // StatusDTO provides status of the server type StatusDTO struct { Status string `json:"Status" example:"OK"` } // ErrorResponseDTO provides Error message type ErrorResponseDTO struct { Error string `json:"Error" example:"The server encountered a problem and could not process your request"` }
brew install postgresql@17
Validator aus dem generierten Code wird später verwendet, um die Eingabefelder für den Fusionsendpunkt zu validieren.
Erstellen Sie die Datei
Fügen Sie Folgendes hinzu:
# create the DB createdb materiafusiondb # step into the DB to perform SQL commands psql materiafusiondb
Eine vollständige Liste der 32 MateriaTypes finden Sie hier.
Erstellen Sie die Datei
Fügen Sie Folgendes hinzu:
-- create an SQL user to be used by the Go server CREATE USER go_client WITH PASSWORD 'xxxxxxxx'; -- The Go server doesn't ever need to add data to the DB. -- So let's give it just read permission. CREATE ROLE readonly_role; GRANT USAGE ON SCHEMA public TO readonly_role; -- This command gives SELECT access to all future created tables. ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_role; -- If you want to be more strict and give access only to tables that already exist, use this: -- GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role; GRANT readonly_role TO go_client;
Die vollständige Liste der Regeln finden Sie hier.
CREATE TYPE display_materia_type AS ENUM ('Magic', 'Command', 'Support', 'Independent'); CREATE TYPE materia_type AS ENUM ('Fire', 'Ice', 'Lightning', 'Restore', 'Full Cure', 'Status Defense', 'Defense', 'Absorb Magic', 'Status Magic', 'Fire & Status', 'Ice & Status', 'Lightning & Status', 'Gravity', 'Ultimate', 'Quick Attack', 'Quick Attack & Status', 'Blade Arts', 'Blade Arts & Status', 'Fire Blade', 'Ice Blade', 'Lightning Blade', 'Absorb Blade', 'Item', 'Punch', 'SP Turbo', 'HP Up', 'AP Up', 'ATK Up', 'VIT Up', 'MAG Up', 'SPR Up', 'Dash', 'Dualcast', 'DMW', 'Libra', 'MP Up', 'Anything'); CREATE TABLE materia ( id integer NOT NULL, name character varying(50) NOT NULL, materia_type materia_type NOT NULL, grade integer NOT NULL, display_materia_type display_materia_type, description text CONSTRAINT materia_pkey PRIMARY KEY (id) ); -- The primary key 'id' should auto-increment by 1 for every row entry. CREATE SEQUENCE materia_id_seq AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; ALTER SEQUENCE materia_id_seq OWNED BY materia.id; ALTER TABLE ONLY materia ALTER COLUMN id SET DEFAULT nextval('materia_id_seq'::REGCLASS);
Wir verwenden einen serverinternen Cache, weil:
main.go aktualisieren:
COPY materia(name,materia_type,grade,display_materia_type,description) FROM '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
API/helpers.go aktualisieren:
? codebase ├─ cmd │ └─ api │ ├─ errors.go │ ├─ handlers.go │ ├─ helpers.go │ ├─ main.go │ ├─ middleware.go │ └─ server.go ├─ internal │ ├─ database --- db.go │ ├─ env --- env.go │ ├─ request --- json.go │ ├─ response --- json.go │ └─ validator │ ├─ helpers.go │ └─ validators.go ├─ go.mod ├─ LICENSE ├─ Makefile ├─ README.md └─ README.html
HTTP_PORT=4444 DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable API_TIMEOUT_SECONDS=5 API_CALLS_ALLOWED_PER_SECOND=1
Den vollständigen Handler-Code finden Sie hier.
Fügen Sie die Swagger-Bibliothek hinzu:
go get github.com/joho/godotenv
Kommentieren Sie in „routes.go“ die Swagger-Zeile aus und fügen Sie den Import hinzu:
// At the beginning of main(): err := godotenv.Load(".env") // Loads environment variables from .env file if err != nil { // This will be true in prod, but that's fine. fmt.Println("Error loading .env file") } // Modify config struct: type config struct { baseURL string db struct { dsn string } httpPort int apiTimeout int apiCallsAllowedPerSecond float64 } // Modify run() to use the new values from .env: cfg.httpPort = env.GetInt("HTTP_PORT") cfg.db.dsn = env.GetString("DB_DSN") cfg.apiTimeout = env.GetInt("API_TIMEOUT_SECONDS") cfg.apiCallsAllowedPerSecond = float64(env.GetInt("API_CALLS_ALLOWED_PER_SECOND")) // cfg.baseURL = env.GetString("BASE_URL") - not required
Fügen Sie in den Handler-, DTO- und Modelldateien Kommentare für die Swagger-Dokumentation hinzu. Hier finden Sie alle Optionen.
Führen Sie im Terminal Folgendes aus:
go get github.com/didip/tollbooth
Dadurch wird ein Ordner „api/docs“ erstellt, dessen Definition für Go, JSON und YAML verfügbar ist.
Um es zu testen, starten Sie den lokalen Server und öffnen Sie http://localhost:4444/docs.
Endgültige Ordnerstruktur:
func (app *application) contentTypeCheck(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Content-Type") != "application/json" { app.unsupportedMediaType(w, r) return } next.ServeHTTP(w, r) }) } func (app *application) rateLimiter(next http.Handler) http.Handler { limiter := tollbooth.NewLimiter(app.config.apiCallsAllowedPerSecond, nil) limiter.SetIPLookups([]string{"X-Real-IP", "X-Forwarded-For", "RemoteAddr"}) return tollbooth.LimitHandler(limiter, next) } func (app *application) apiTimeout(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { timeoutDuration := time.Duration(app.config.apiTimeout) * time.Second ctx, cancel := context.WithTimeout(r.Context(), timeoutDuration) defer cancel() r = r.WithContext(ctx) done := make(chan struct{}) go func() { next.ServeHTTP(w, r) close(done) }() select { case <-done: return case <-ctx.Done(): app.gatewayTimeout(w, r) return } }) }
Führen Sie von Ihrer lokalen DB-Instanz aus Folgendes aus:
brew install postgresql@17
Anmelden bei der Remote-Instanz:
# create the DB createdb materiafusiondb # step into the DB to perform SQL commands psql materiafusiondb
Werte:
Das letzte Puzzleteil.
-- create an SQL user to be used by the Go server CREATE USER go_client WITH PASSWORD 'xxxxxxxx'; -- The Go server doesn't ever need to add data to the DB. -- So let's give it just read permission. CREATE ROLE readonly_role; GRANT USAGE ON SCHEMA public TO readonly_role; -- This command gives SELECT access to all future created tables. ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_role; -- If you want to be more strict and give access only to tables that already exist, use this: -- GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role; GRANT readonly_role TO go_client;
Dadurch erstellt Cloud Build die Datei certs/root.crt in unserem Projekt, bevor der Build beginnt, sodass die Docker-Datei Zugriff darauf hat, auch wenn wir sie nie in unser Github-Repository verschoben haben.
Und das ist es. Versuchen Sie, einen Commit zu pushen, und prüfen Sie, ob der Build ausgelöst wird. Das Cloud Run-Dashboard zeigt die URL Ihres gehosteten Go-Servers an.
Für Fragen im Zusammenhang mit „Warum haben Sie X und nicht Y gemacht?“ Lesen Sie dies.
Für alles andere, was Sie wissen oder besprechen möchten, klicken Sie hier oder kommentieren Sie unten.
Das obige ist der detaillierte Inhalt vonErstellen einer API mit Go, PostgreSQL, Google Cloud und CockroachDB. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!