Maison > développement back-end > Golang > Créer une API avec Go, PostgreSQL, Google Cloud et CockroachDB

Créer une API avec Go, PostgreSQL, Google Cloud et CockroachDB

Linda Hamilton
Libérer: 2024-10-24 07:02:30
original
869 Les gens l'ont consulté

J'ai construit une API avec Go et PostgreSQL, mis en place un pipeline CI/CD avec Google Cloud Run, Cloud Build, Secret Manager et Artifact Registry, et connecté l'instance Cloud Run à CockroachDB.

L'API est basée sur le jeu Crisis Core : Final Fantasy VII, pour simuler « Materia Fusion ». Le public cible de cet article est destiné aux développeurs qui souhaitent simplement savoir comment créer et déployer l’API. J'ai un autre article dans lequel je parle de tout ce que j'ai appris en travaillant sur ce projet, de ce qui n'a pas fonctionné, ainsi que de la compréhension et de la traduction des règles de fusion de matières du jeu (lien à venir).

Liens pour une référence facile

  • Dépôt GitHub et README
  • Documentation et tests Swagger (OpenAPI)
  • Collection publique du facteur
  • Source du modèle de domaine

Objectif de l'API

3 points de terminaison : bilan de santé (GET), liste de tous les matériaux (GET) et simulation de fusion de matériaux (POST)

Le modèle de domaine

La Materia (au singulier et au pluriel) est un orbe de cristal qui sert de source de magie. Il existe 144 matériaux distincts dans le jeu, et ils sont globalement classés en 4 catégories : « Magie », « Commande », « Support » et « Indépendant ». Cependant, dans le but de comprendre les règles de fusion des matières, il était plus facile d'avoir 32 catégories internes basées sur leur comportement de fusion, et 8 grades au sein de ces catégories (voir référence) .

Une matière devient « Maîtrisée » lorsqu’elle est utilisée pendant une certaine durée. La durée n'est pas importante ici.

Plus important encore, 2 materia peuvent être fusionnées pour produire une nouvelle materia. Les règles régissant la fusion sont influencées par :

  • Que l'une ou les deux matières soient maîtrisées.
  • Quelle matière vient en premier (comme dans X Y n'est pas nécessairement égal à Y X).
  • Catégorie interne Materia.
  • Qualité matérielle.

Building an API with Go, PostgreSQL, Google Cloud and CockroachDB

Et il y a BEAUCOUP d'exceptions, certaines règles ayant 3 niveaux de logique if-else imbriquée. Cela élimine la possibilité de créer une table simple dans la base de données et d'y conserver 1 000 règles, ou de proposer une formule unique pour les gouverner toutes.

En bref, il nous faut :

  1. Un tableau materia avec les colonnes name(string), materia_type(ENUM) (les 32 catégories internes), grade(integer), display_materia_type(ENUM) (les 4 catégories utilisées dans le jeu), description(string) et id( entier) comme clé primaire à incrémentation automatique.
  2. Une structure de données pour encapsuler le format des règles de base MateriaTypeA MateriaTypeB = MateriaTypeC.
  3. Code pour utiliser les règles de base et complexes pour déterminer la matière de sortie en termes de catégorie et de grade internes.

1. Configuration de la base de données PostgreSQL locale

Idéalement, vous pouvez installer la base de données à partir du site Web lui-même. Mais l'outil pgAdmin n'a pas pu se connecter à la base de données pour une raison quelconque, j'ai donc utilisé Homebrew.

Installation

brew install postgresql@17
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cela installera tout un tas de fichiers binaires CLI pour faciliter l'utilisation de la base de données.

Facultatif : ajoutez /opt/homebrew/opt/postgresql@17/bin à la variable $PATH.

# create the DB
createdb materiafusiondb
# step into the DB to perform SQL commands
psql materiafusiondb
Copier après la connexion
Copier après la connexion
Copier après la connexion

Créer l'utilisateur et les autorisations

-- 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;
Copier après la connexion
Copier après la connexion
Copier après la connexion

Créer le tableau

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);
Copier après la connexion
Copier après la connexion

Ajouter les données

Créez une feuille Excel avec l'en-tête et les données du tableau, et exportez-la sous forme de fichier CSV. Exécutez ensuite la commande :

COPY materia(name,materia_type,grade,display_materia_type,description) FROM
 '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
Copier après la connexion
Copier après la connexion

2. Création du serveur Go

Créez le code passe-partout à l'aide d'autostrada.dev. Ajoutez les options api, postgresql, httprouter, env var config, tinted logging, git, live reload, makefile. Nous finissons par obtenir une structure de fichier comme celle-ci :

? 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
Copier après la connexion
Copier après la connexion

fichier .env

Le générateur passe-partout a créé du code pour récupérer les variables d'environnement et les ajouter au code, mais nous pouvons faciliter le suivi et la mise à jour des valeurs.

Créez le fichier /.env. Ajoutez les valeurs suivantes :

HTTP_PORT=4444
DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable
API_TIMEOUT_SECONDS=5
API_CALLS_ALLOWED_PER_SECOND=1
Copier après la connexion
Copier après la connexion

Ajouter la bibliothèque godotenv :

go get github.com/joho/godotenv
Copier après la connexion
Copier après la connexion

Ajoutez ce qui suit à 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
Copier après la connexion
Copier après la connexion

Middleware et routes

Le passe-partout dispose déjà d'un middleware pour se remettre des paniques. Nous en ajouterons 3 autres : vérification du type de contenu, limitation du débit et protection contre le délai d'expiration de l'API.

Ajouter une bibliothèque de péages :

go get github.com/didip/tollbooth
Copier après la connexion
Copier après la connexion

Mise à jour

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
  }
 })
}
Copier après la connexion
Copier après la connexion

Le middleware doit être ajouté aux routes. Ils peuvent être ajoutés soit à tous les itinéraires, soit à des itinéraires spécifiques. Dans notre cas, la vérification Content-Type (c'est-à-dire obliger les en-têtes d'entrée à inclure Content-Type: application/json) n'est nécessaire que pour les requêtes POST. Modifiez donc routes.go comme suit :

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
}

Copier après la connexion

Gestion des erreurs

Ajoutez les méthodes suivantes à /api/errors.go pour aider les fonctions middleware :

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)
}
Copier après la connexion

Fichiers de structure de demande et de réponse

/api/dtos.go :

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"`
}
Copier après la connexion

/api/requests.go :

brew install postgresql@17
Copier après la connexion
Copier après la connexion
Copier après la connexion

Le validateur, à partir du code généré, sera utilisé ultérieurement pour valider les champs de saisie du point final de fusion.

Structure des données pour les règles de combinaison

Créez le fichier /internal/crisis-core-materia-fusion/constants.go

Ajoutez ce qui suit :

# create the DB
createdb materiafusiondb
# step into the DB to perform SQL commands
psql materiafusiondb
Copier après la connexion
Copier après la connexion
Copier après la connexion

La liste complète des 32 MateriaTypes peut être trouvée ici.

Créez le fichier /internal/crisis-core-materia-fusion/models.go

Ajoutez ce qui suit :

-- 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;
Copier après la connexion
Copier après la connexion
Copier après la connexion

La liste complète des règles peut être trouvée ici.

Gestionnaire de matériel dans api/handlers.go

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);
Copier après la connexion
Copier après la connexion

Cache sur le serveur

Nous utilisons un cache intégré au serveur car :

  1. Les données extraites de la base de données ne changent jamais.
  2. Les mêmes données sont utilisées par les points finaux de matière et de fusion.

Mettre à jour main.go :

COPY materia(name,materia_type,grade,display_materia_type,description) FROM
 '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
Copier après la connexion
Copier après la connexion

Mettre à jour 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
Copier après la connexion
Copier après la connexion

Gestionnaire de fusion dans api/handlers.go

HTTP_PORT=4444
DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable
API_TIMEOUT_SECONDS=5
API_CALLS_ALLOWED_PER_SECOND=1
Copier après la connexion
Copier après la connexion

Le code complet du gestionnaire peut être trouvé ici.

Document de définition de l'interface utilisateur Swagger et d'OpenAPI

Ajouter la bibliothèque Swagger :

go get github.com/joho/godotenv
Copier après la connexion
Copier après la connexion

Dans routes.go, décommentez la ligne Swagger et ajoutez l'importation :

// 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
Copier après la connexion
Copier après la connexion

Dans les fichiers de gestionnaire, DTO et modèle, ajoutez des commentaires pour la documentation Swagger. Référez-vous à ceci pour toutes les options.

Dans le terminal, exécutez :

go get github.com/didip/tollbooth
Copier après la connexion
Copier après la connexion

Cela crée un dossier api/docs, avec la définition disponible pour Go, JSON et YAML.

Pour le tester, démarrez le serveur local et ouvrez http://localhost:4444/docs.


Structure finale des dossiers :

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
  }
 })
}
Copier après la connexion
Copier après la connexion

3. Configuration de l'instance PostgreSQL distante dans CockroachDB

  1. Utilisez les étapes à partir d'ici.
  2. Après avoir créé le certificat, créez /certs/root.crt dans le projet et ajoutez-y le certificat. Nous ferons référence à ce fichier ultérieurement dans la configuration de Google Run.
  3. ATTENTION ! Nous ne poussons PAS ce dossier vers le référentiel distant. Ajoutez le dossier certs/ à .gitignore. Nous créons le certificat en local uniquement pour tester la connexion, si vous le souhaitez.
  4. Maintenant, lorsque vous allez dans CockroachDB → Tableau de bord → Menu de gauche → Bases de données, vous devriez pouvoir voir la base de données que vous avez créée.

Migration

Depuis votre instance de base de données locale, exécutez :

brew install postgresql@17
Copier après la connexion
Copier après la connexion
Copier après la connexion
  1. Allez dans CockroachDB → Menu de gauche → Migrations → Ajouter un schéma → Faites glisser le fichier SQL que vous venez de recevoir. Toutes les étapes s'exécuteront, à l'exception de l'insertion des données du tableau. Il vous montrera également une liste des étapes qui ont été exécutées.
  2. Au moment de la rédaction de cet article, l'instance PostgreSQL dans CockroachDB ne prend pas en charge les instructions telles que IMPORT INTO. J'ai donc dû créer une instruction INSERT dans un fichier SQL local pour 270 lignes (que nous pouvons dériver de la sortie pg_dump que nous venons de recevoir).
  3. Connectez-vous à l'instance distante et exécutez le fichier SQL.

Connexion à l'instance distante :

# create the DB
createdb materiafusiondb
# step into the DB to perform SQL commands
psql materiafusiondb
Copier après la connexion
Copier après la connexion
Copier après la connexion

4. Déployer une instance Google Cloud Run

  1. Créez un Dockerfile comme celui-ci.
  2. Accédez à Google Cloud Run et créez un nouveau projet pour l'API.
  3. Créer un service → Déployer en continu à partir d'un dépôtCONFIGURATION AVEC CLOUD BUILDFournisseur de référentiel = Github → Sélectionnez votre dépôt → Type de build = Dockerfile → Enregistrer.
  4. Authentification = Autoriser les invocations non athénitées.
  5. La plupart des valeurs par défaut devraient convenir telles quelles.
  6. Faites défiler jusqu'à Conteneurs → Port de conteneur = 4444.
  7. Sélectionnez l'onglet Variables et secrets et ajoutez les mêmes variables d'environnement que celles que nous avons dans notre fichier .env local.

Valeurs :

  1. HTTP_PORT = 4444
  2. DB_DSN = ?sslmode=verify-full&sslrootcert=/app/certs/root.crt
  3. API_TIMEOUT_SECONDS = 5
  4. API_CALLS_ALLOWED_PER_SECOND = 1

Utilisation de Google Secret Manager pour le certificat

La dernière pièce du puzzle.

  1. Rechercher Secret Manager → Créer un secret → Nom = 'DB_CERT' → Téléchargez le certificat .crt de CockroachDB.
  2. Dans Cloud Run → (votre service) → Cliquez sur Modifier le déploiement continu → Faites défiler jusqu'à Configuration → Ouvrir l'éditeur.
  3. Ajoutez ceci comme première étape :
-- 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;
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cela obligera Cloud Build à créer le fichier certs/root.crt dans notre projet avant le début de la build, afin que le Dockerfile y ait accès même si nous ne l'avons jamais poussé vers notre référentiel Github.


Et c’est tout. Essayez de pousser un commit et vérifiez si la build se déclenche. Le tableau de bord Cloud Run affichera l'URL de votre serveur Go hébergé.


Pour les questions liées à « Pourquoi avez-vous fait X et pas Y ? » lisez ceci.

Pour tout ce que vous souhaitez savoir ou discuter, allez ici ou commentez ci-dessous.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal