Saya membina API dengan Go dan PostgreSQL, menyediakan saluran paip CI/CD dengan Google Cloud Run, Cloud Build, Secret Manager dan Artifact Registry serta menyambungkan instance Cloud Run ke CockroachDB.
API adalah berdasarkan permainan Crisis Core: Final Fantasy VII, untuk mensimulasikan "Materia Fusion". Khalayak sasaran artikel ini adalah untuk pembangun yang hanya ingin mengetahui cara membina dan menggunakan API. Saya mempunyai satu lagi artikel di mana saya bercakap tentang semua yang saya pelajari semasa mengerjakan projek ini, perkara yang tidak berjaya dan memahami serta menterjemah peraturan gabungan materia permainan (pautan akan datang tidak lama lagi).
3 titik akhir — pemeriksaan kesihatan (GET), senarai semua materia (GET) dan simulasi gabungan materia (POST)
Materia (baik tunggal dan jamak) ialah bola kristal yang berfungsi sebagai sumber sihir. Terdapat 144 materia yang berbeza dalam permainan, dan ia secara umum diklasifikasikan kepada 4 kategori: "Sihir", "Perintah", "Sokongan" dan "Bebas". Walau bagaimanapun, untuk tujuan mengetahui peraturan pelakuran materia, lebih mudah untuk mempunyai 32 kategori dalaman berdasarkan tingkah laku gabungan mereka dan 8 gred dalam kategori tersebut (lihat rujukan) .
Materia menjadi 'Mastered' apabila ia digunakan untuk tempoh tertentu. Tempoh tidak penting di sini.
Paling penting, 2 bahan boleh dicantum untuk menghasilkan bahan baru. Peraturan yang mengawal gabungan dipengaruhi oleh:
Dan terdapat BANYAK pengecualian, dengan beberapa peraturan mempunyai 3 tahap logik if-else bersarang. Ini menghapuskan kemungkinan mencipta jadual ringkas dalam DB dan meneruskan 1000 peraturan ke dalamnya, atau menghasilkan Satu Formula Untuk Memerintah Mereka Semua.
Ringkasnya, kita perlukan:
Sebaik-baiknya anda boleh memasang DB dari tapak web itu sendiri. Tetapi alat pgAdmin tidak dapat menyambung ke DB atas sebab tertentu, jadi saya menggunakan Homebrew.
brew install postgresql@17
Ini akan memasang sejumlah besar fail binari CLI untuk membantu menggunakan DB.
Pilihan: tambahkan /opt/homebrew/opt/postgresql@17/bin pada pembolehubah $PATH.
# 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);
Buat helaian Excel dengan pengepala jadual dan data, dan eksportnya sebagai fail CSV. Kemudian jalankan arahan:
COPY materia(name,materia_type,grade,display_materia_type,description) FROM '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
Buat kod boilerplate menggunakan autostrada.dev. Tambahkan pilihan api, postgresql, httprouter , env var config, tinted logging, git, live reload, makefile. Kami akhirnya mendapat struktur fail seperti ini:
? 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
Penjana boilerplate telah mencipta kod untuk mengambil pembolehubah persekitaran dan menambahkannya pada kod, tetapi kami boleh memudahkan untuk menjejak dan mengemas kini nilai.
Buat
HTTP_PORT=4444 DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable API_TIMEOUT_SECONDS=5 API_CALLS_ALLOWED_PER_SECOND=1
Tambah pustaka godotenv:
go get github.com/joho/godotenv
Tambah yang berikut pada main.go:
// 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
Boilerplate sudah mempunyai middleware untuk pulih daripada panik. Kami akan menambah 3 lagi: Pemeriksaan Jenis Kandungan, pengehadan kadar dan perlindungan tamat masa API.
Tambah perpustakaan pondok tol:
go get github.com/didip/tollbooth
Kemas kini
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 } }) }
Perisian tengah perlu ditambahkan pada laluan. Ia boleh ditambah sama ada pada semua laluan, atau pada laluan tertentu. Dalam kes kami, semakan Jenis Kandungan (iaitu, mewajibkan pengepala input untuk memasukkan Jenis Kandungan: application/json) hanya diperlukan untuk permintaan POST. Jadi ubah suai route.go seperti berikut:
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 }
Tambah kaedah berikut pada
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
Pengesah, daripada kod yang dijana, akan digunakan kemudian untuk mengesahkan medan input untuk titik akhir gabungan.
Buat fail
Tambah yang berikut:
# create the DB createdb materiafusiondb # step into the DB to perform SQL commands psql materiafusiondb
senarai penuh 32 Jenis Materia boleh didapati di sini.
Buat fail
Tambah yang berikut:
-- 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;
senarai penuh peraturan boleh didapati di sini.
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);
Kami menggunakan cache dalam pelayan kerana:
Kemas kini main.go:
COPY materia(name,materia_type,grade,display_materia_type,description) FROM '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
Kemas kini api/helpers.go:
? 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
Kod pengendali lengkap boleh didapati di sini.
Tambah perpustakaan Swagger:
go get github.com/joho/godotenv
Dalam routes.go nyahkomen baris Swagger, dan tambahkan import:
// 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
Dalam pengendali, DTO dan fail model, tambahkan ulasan untuk dokumentasi Swagger. Rujuk ini untuk semua pilihan.
Di terminal, jalankan:
go get github.com/didip/tollbooth
Ini mencipta folder api/docs, dengan definisi tersedia untuk Go, JSON dan YAML.
Untuk mengujinya, mulakan pelayan setempat dan buka http://localhost:4444/docs.
Struktur folder akhir:
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 } }) }
Dari contoh DB tempatan anda, jalankan:
brew install postgresql@17
Melog masuk ke contoh jauh:
# create the DB createdb materiafusiondb # step into the DB to perform SQL commands psql materiafusiondb
Nilai:
Bahagian terakhir teka-teki.
-- 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;
Ini akan menjadikan Cloud Build mencipta fail certs/root.crt dalam projek kami sebelum binaan bermula, supaya Dockerfile akan mempunyai akses kepadanya walaupun kami tidak pernah menolaknya ke repositori Github kami.
Dan itu sahaja. Cuba tolak komit dan semak sama ada binaan dicetuskan. Papan pemuka Cloud Run akan menunjukkan URL pelayan Go yang dihoskan anda.
Untuk soalan yang berkaitan dengan "Mengapa anda melakukan X dan bukan Y?" baca ini.
Untuk apa-apa lagi yang anda ingin ketahui atau bincangkan, pergi ke sini atau komen di bawah.
Atas ialah kandungan terperinci Membina API dengan Go, PostgreSQL, Google Cloud dan CockroachDB. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!