Go와 PostgreSQL로 API를 구축하고 Google Cloud Run, Cloud Build, Secret Manager, Artifact Registry로 CI/CD 파이프라인을 설정하고 Cloud Run 인스턴스를 CockroachDB에 연결했습니다.
API는 Crisis Core: Final Fantasy VII 게임을 기반으로 "Materia Fusion"을 시뮬레이션합니다. 이 문서의 대상 독자는 API를 구축하고 배포하는 방법을 알고 싶은 개발자를 위한 것입니다. 이 프로젝트를 진행하면서 배운 모든 것, 효과가 없었던 것, 게임의 마테리아 융합 규칙을 이해하고 번역하는 것에 대해 이야기하는 또 다른 기사가 있습니다(링크는 곧 제공됩니다).
3개의 엔드포인트 — 상태 확인(GET), 모든 물질 목록(GET), 물질 융합 시뮬레이션(POST)
마테리아(단수형 및 복수형 모두)는 마법의 원천 역할을 하는 수정 구체입니다. 게임에는 144개의 서로 다른 마테리아가 있으며, 크게 "마법", "명령", "지원", "독립"의 4가지 범주로 분류됩니다. 그러나 물질 융합의 규칙을 파악하기 위해 융합 행위에 따라 32개의 내부 범주를 갖고, 해당 범주 내에 8등급을 두는 것이 더 쉬웠습니다(참고 자료 참조). .
마테리아는 일정 시간 동안 사용하면 '마스터링'됩니다. 여기서는 기간이 중요하지 않습니다.
가장 중요한 것은 2개의 마테리아를 융합하여 새로운 마테리아를 생산할 수 있다는 것입니다. 융합을 관리하는 규칙은 다음의 영향을 받습니다.
또한 예외가 많이 있으며 일부 규칙에는 3가지 수준의 중첩된 if-else 논리가 있습니다. 이렇게 하면 DB에 간단한 테이블을 만들고 그 안에 1000개의 규칙을 유지하거나 모든 것을 지배하는 하나의 공식을 만들 가능성이 제거됩니다.
요컨대 다음이 필요합니다.
이상적으로는 웹사이트 자체에서 DB를 설치하는 것이 좋습니다. 그런데 pgAdmin 툴이 무슨 이유인지 DB에 연결할 수 없어서 Homebrew를 사용했습니다.
brew install postgresql@17
이렇게 하면 DB 사용에 도움이 되는 CLI 바이너리 파일 전체가 설치됩니다.
선택 사항: $PATH 변수에 /opt/homebrew/opt/postgresql@17/bin을 추가합니다.
# 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);
표 헤더와 데이터가 포함된 Excel 시트를 만들고 CSV 파일로 내보냅니다. 그런 다음 다음 명령을 실행합니다.
COPY materia(name,materia_type,grade,display_materia_type,description) FROM '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
autostrada.dev를 사용하여 상용구 코드를 만듭니다. api, postgresql, httprouter , env var config, Tintedlogging, git, live reload, makefile 옵션을 추가합니다. 결국 다음과 같은 파일 구조를 갖게 됩니다.
? 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
godotenv 라이브러리 추가:
go get github.com/joho/godotenv
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
보일러플레이트에는 패닉 상태를 복구하기 위한 미들웨어가 이미 포함되어 있습니다. 콘텐츠 유형 확인, 속도 제한, API 시간 초과 보호 등 3가지를 더 추가할 예정입니다.
톨게이트 라이브러리 추가:
go get github.com/didip/tollbooth
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 } }) }
경로에 미들웨어를 추가해야 합니다. 모든 경로에 추가하거나 특정 경로에 추가할 수 있습니다. 우리의 경우 Content-Type 확인(즉, 입력 헤더에 Content-Type: application/json을 포함하도록 요구)은 POST 요청에만 필요합니다. 따라서 Routes.go를 다음과 같이 수정하십시오:
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 }
미들웨어 기능을 돕기 위해
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
생성된 코드의 유효성 검사기는 나중에 융합 엔드포인트에 대한 입력 필드의 유효성을 검사하는 데 사용됩니다.
다음을 추가하세요.
# create the DB createdb materiafusiondb # step into the DB to perform SQL commands psql materiafusiondb
여기에서 32개의 MateriaType의 전체 목록을 확인할 수 있습니다.
다음을 추가하세요.
-- 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);
다음과 같은 이유로 서버 내 캐시를 사용하고 있습니다.
main.go 업데이트:
COPY materia(name,materia_type,grade,display_materia_type,description) FROM '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
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
전체 핸들러 코드는 여기에서 찾을 수 있습니다.
Swagger 라이브러리 추가:
go get github.com/joho/godotenv
routes.go에서 Swagger 줄의 주석 처리를 제거하고 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
핸들러, DTO 및 모델 파일에서 Swagger 문서에 대한 설명을 추가합니다. 모든 옵션에 대해서는 이것을 참조하세요.
터미널에서 다음을 실행하세요.
go get github.com/didip/tollbooth
이렇게 하면 Go, JSON 및 YAML에 사용할 수 있는 정의가 포함된 api/docs 폴더가 생성됩니다.
테스트하려면 로컬 서버를 시작하고 http://localhost:4444/docs를 엽니다.
최종 폴더 구조:
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 } }) }
로컬 DB 인스턴스에서 다음을 실행합니다.
brew install postgresql@17
원격 인스턴스에 로그인:
# 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;
이렇게 하면 Cloud Build가 빌드가 시작되기 전에 프로젝트에 certs/root.crt 파일을 생성하므로 Dockerfile은 Github 저장소에 푸시하지 않은 경우에도 해당 파일에 액세스할 수 있습니다.
그리고 그게 다입니다. 커밋을 푸시하고 빌드가 트리거되는지 확인하세요. Cloud Run 대시보드에는 호스팅된 Go 서버의 URL이 표시됩니다.
“왜 Y를 하지 않고 X를 하였나요?” 관련 질문 읽어보세요.
알고 싶은 내용이나 토론하고 싶은 내용이 있으면 여기로 이동하거나 아래에 댓글을 남겨주세요.
위 내용은 Go, PostgreSQL, Google Cloud 및 CockroachDB를 사용하여 API 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!