GO: Accès à un "struct" depuis un autre package

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je suis plutôt habitué au langage type "class-oriented", et j’ai un peu de peine avec Go.

avec une structure comme celle-ci pour la création d’une API:

- main.go
  app/
    - app.go
  endpoints/
    - users.go

Le code:

Donc mon main.go:

package main

import "winxaito/app"

func main(){
    a := app.App{}
    a.InitializeDatabase("**", "**", "**")
    a.InitializeRouter()

    a.Run(":8080")
}

app.go

package app

import(
  /* *** */
)

type App struct{
    Router  *mux.Router
    DB  *sql.DB
}

func (a *App) InitializeDatabase(user, password, dbname string) {
    /* ... */
}

func (a *App) InitializeRouter() {
    a.Router = mux.NewRouter().StrictSlash(true)
    a.Router.HandleFunc("/", apiHome).Methods("GET")
}

func (a *App) Run(addr string) {
    log.Fatal(http.ListenAndServe(addr, a.Router))
}

func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}){
    response, _ := json.Marshal(payload)

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(response)
}

users.go

package endpoints

import(
  /* *** */
)

func apiGetUser(w http.ResponseWriter, r *http.Request) {
  //Accès à la base de données dans App
}

func InitUsersRoute() {
  //Initialisation des routes, appelé par app.go ?
}

Comment est-ce que je peux appelé InitUsersRoute() - dans users.go depuis InitializeRouter() - dans app.go et pouvoir accédé à App.DB depuis apiGetUser() - dans users.go ?

Premier problème, avec les imports j’ai des boucles cyclique, et comment accédé à l’instance de App contenant ma DB ?

Je vous remercie.

Salut,

Quand tu as une dépendance cyclique, en général c’est un signe que les responsabilités de chaque package ne sont pas claires : deux choses ne peuvent être mutuellement dépendantes que si elles font partie du même package.

Ici, c’est le cas avec ta structure App : elle ne sert à rien.

Typiquement, tu vas définir une structure quand tu vas vouloir promener des données entre plusieurs endroits du code, mais les données de ton application pourraient simplement exister dans ta fonction main() et ses méthodes :

  • soit se trouver directement dans le package main,
  • soit exister comme des fonctions utilitaires à côté de tes handlers (je pense à celle qui écrit du JSON).

Bref, ici ton problème principal, c’est vraiment que le package app est complètement rendondant avec le package main.

PS :

  • pourquoi garder une référence sur le muxer ? Une fois que tu l’as passé en paramètre de ton serveur HTTP, tu n’y toucheras même plus…
  • si tu as besoin que tes handlers aient une référence sur la base de données, pourquoi ne pas créer une struct qui garde cette référence, et dont les méthodes sont les handlers que tu associes dans le muxer ?
+0 -0

Merci pour ta réponse, je n’ai pas totalement compris même si j’en vois l’idée.

Ma problématique, c’est que j’aimerais pouvoir organiser tout ça. (Certes de tout ce que j’ai, si je met tout dans un seul fichier ou package ça fonctionne, mais je n’aime pas avoir des fichiers trop long et trop de fichier dans le même dossier..)

J’ai trouvé une solution a ma problématique mais je ne sais pas si elle est bonne ?

app.go

func (a *App) InitializeRouter() {
    a.Router = mux.NewRouter().StrictSlash(true)
    a.Router.HandleFunc("/", apiHome).Methods("GET")
    endpoints.InitUsers(a.DB, a.Router)
}

users.go

var database *sql.DB

func InitUsers(db *sql.DB, router *mux.Router){
    database = db
    router.HandleFunc("/user/{uuid}", ApiGetUser).Methods("GET")
}

//Je peux ensuite dans ma fonction ApiGetUser accéder à *database*

Quand tu as une dépendance cyclique, en général c’est un signe que les responsabilités de chaque package ne sont pas claires : deux choses ne peuvent être mutuellement dépendantes que si elles font partie du même package.

Ici, c’est le cas avec ta structure App : elle ne sert à rien.


Bref, ici ton problème principal, c’est vraiment que le package app est complètement rendondant avec le package main.

Donc je peux mettre App dans le package main, mais mon problème reste le même non ? (Mise à part qu’il est impossible d’inclure le package main depuis un autre package.)

pourquoi garder une référence sur le muxer ? Une fois que tu l’as passé en paramètre de ton serveur HTTP, tu n’y toucheras même plus…

Pour définir les routes de chaques endpoints (les routes de users dans users.go, etc.), devrais-je créer toutes mes routes dans le même fichier ?

soit exister comme des fonctions utilitaires à côté de tes handlers (je pense à celle qui écrit du JSON).

J’ai créer un fichier json.go dans le package endpoints avec ces fonctions:

package endpoints

import (
    "encoding/json"
    "net/http"
)

func RespondWithError(w http.ResponseWriter, code int, message string) {
    RespondWithJSON(w, code, map[string]string{"error": message})
}

func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}){
    response, _ := json.Marshal(payload)

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(response)
}

Est-ce bien cela que tu entendais ?

si tu as besoin que tes handlers aient une référence sur la base de données, pourquoi ne pas créer une struct qui garde cette référence, et dont les méthodes sont les handlers que tu associes dans le muxer ?

Comment est-ce qu’on fait ça ?

Merci :)

Pour définir les routes de chaques endpoints (les routes de users dans users.go, etc.), devrais-je créer toutes mes routes dans le même fichier ?

Si tu regardes ce que font les frameworks comme buffalo, c’est ce qui semble être la norme, oui.

Ou bien tu peux définir des fonctions qui prennent ton muxer en argument et lui ajoutent des routes, ou encore des fonctions qui retournent un http.Handler pour chaque sous-module… mais s’il y a un truc que Go m’a appris, c’est qu’il faut rester KISS et ne pas chercher à tout encapsuler dans des packages tant que ça reste gérable : si tu peux tout avoir dans le même fichier, ne t’en prive pas, au moins tu sais où chercher tes routes. :)

L’idée c’est pas forcément de favoriser le code bordélique, mais plutôt de remettre ces considérations à plus tard, jusqu’au moment où tu auras vraiment besoin de redécouper ton code, si ce moment arrive un jour.

J’ai créer un fichier json.go dans le package endpoints avec ces fonctions:

Est-ce bien cela que tu entendais ?

Oui.

Maintenant, un petit coup de golint va te donner le choix entre deux alternatives :

  • Soit tu considères que ces fonctions sont publiques et doivent pouvoir être importées d’un autre package, auquel cas il va te forcer à écrire un commentaire de doc (// NomDeLaFonction blablablabla.
  • Soit ce sont des fonctions utilitaires, auquel cas tu ferais mieux de les garder privées (démarrer leur nom par une minuscule) et de décider que tous les handlers qui devront les utiliser feront partie de ce package (quitte à les mettre dans des fichiers séparés)

Personnellement j’opterais pour la seconde option. C’est aussi ce que ferait buffalo (toutes les actions dans des fichiers séparés dans un package actions).

Comment est-ce qu’on fait ça ?

Je te trouve ça dans un code que j’ai dû faire il n’y a pas longtemps :

// Considère que `jobs.JobQueue` est "la base de données"
func setupHTTPHandler(q jobs.JobQueue) http.Handler {
    r := mux.NewRouter()

        // Je crée un requestHandler qui garde une référence sur cette queue
    h := &requestHandler{q}

    // J'associe ses méthodes dans le routeur
    r.HandleFunc("/jobs", h.listJobs).Methods("GET")
    r.HandleFunc("/jobs", h.addJob).Methods("POST")
    r.HandleFunc("/jobs/{job_id}", h.showJob).Methods("GET")
    r.HandleFunc("/jobs/{job_id}", h.updateJob).Methods("PUT")
    r.HandleFunc("/jobs/{job_id}", h.deleteJob).Methods("DELETE")
    r.PathPrefix("/doc/").Handler(httpSwagger.WrapHandler)

    return r
}

type requestHandler struct {
    queue jobs.JobQueue
}

func (h *requestHandler) listJobs(w http.ResponseWriter, r *http.Request) {
    res, err := h.queue.All()
    if err != nil {
        writeError(w, err, http.StatusInternalServerError)
    }
    writeJSON(w, res, 200)
}

func (h *requestHandler) addJob(w http.ResponseWriter, r *http.Request) { /* ... */ }

// ...
+0 -0

Merci pour ta réponse,

encore juste une question, en comparaison de ce que j’ai essayé et ton code, tu utilise un type .. struct, qu’elle avantage par rapport à une variable global comme j’ai fais ?

var database *sql.DB

func InitUsers(db *sql.DB, router *mux.Router){
    database = db
    router.HandleFunc("/user/{uuid}", ApiGetUser).Methods("GET")
}

Là comme ça, je ne vois pas spécialement d’avantage. La variable globale privée fonctionne très bien et c’est même encore plus simple.

Peut-être y aurait-il un avantage si j’avais envie de rajouter d’autres méthodes à cette struct (qui ne soient pas des handlers), mais vu que c’est amené à être un singleton, c’est vrai qu’ici on s’en fout un peu.

Dans l’idée je n’aime pas trop accéder à une variable globale si celle-ci est initialisée dynamiquement (par une fonction InitQuelqueChose) : là, dans mes méthodes, je suis peut-être un peu plus sûr de moi sur le fait que h.queue n’est pas une référence nulle.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte