Search code examples
mongodbgomux

Mongo client set in main function, functions in other modules receive nil value


I have a restful API utilizing mux and mongo-driver. Following a tutorial, I attempted to setup the server and mongo client like so in the main package:

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "github.com/gorilla/mux"
    c "github.com/moonlightfight/elo-backend/config"
    "github.com/moonlightfight/elo-backend/routes/admin"
    "github.com/moonlightfight/elo-backend/routes/tournament"
    "github.com/spf13/viper"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

var client *mongo.Client

func main() {
    // Set the file name of the configurations file
    viper.SetConfigName("config")

    // Set the path to look for the configurations file
    viper.AddConfigPath(".")

    // Enable VIPER to read Environment Variables
    viper.AutomaticEnv()

    viper.SetConfigType("yml")
    var configuration c.Configurations

    if err := viper.ReadInConfig(); err != nil {
        fmt.Printf("Error reading config file, %s", err)
    }

    err := viper.Unmarshal(&configuration)
    if err != nil {
        fmt.Printf("Unable to decode into struct, %v", err)
    }
    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
    clientOptions := options.Client().ApplyURI(fmt.Sprintf("mongodb+srv://%s:%[email protected]/%s?retryWrites=true&w=majority", configuration.Database.DBUser, configuration.Database.DBPass, configuration.Database.DBName))
    port := fmt.Sprintf(":%d", configuration.Server.Port)
    mongo.Connect(ctx, clientOptions)
    router := mux.NewRouter()
    router.HandleFunc("/api/admin", admin.CreateAdminEndpoint).Methods("POST")
    router.HandleFunc("/api/admin/login", admin.AdminLoginEndpoint).Methods("POST")
    router.HandleFunc("/api/tournament/getfromweb", tournament.GetTournamentData).Methods("GET")
    fmt.Printf("server listening on http://localhost%v", port)
    http.ListenAndServe(port, router)
}

Now, in order to manage my code more concisely, I set up modules (as you can see in the imports on main) to handle the functions that mux will use for the endpoints.

On one specific case (handling the "/api/admin" endpoint:

package admin

import (
    "context"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"

    "github.com/dgrijalva/jwt-go"
    c "github.com/moonlightfight/elo-backend/config"
    m "github.com/moonlightfight/elo-backend/models"
    "github.com/spf13/viper"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "golang.org/x/crypto/bcrypt"
)

var client *mongo.Client

// other code here

func CreateAdminEndpoint(response http.ResponseWriter, request *http.Request) {
    response.Header().Set("content-type", "application/json")
    var admin m.Admin
    err := json.NewDecoder(request.Body).Decode(&admin)
    if err != nil {
        log.Println(err)
    }
    // encrypt user password
    admin.Password = HashPassword(admin.Password)
    fmt.Println(client)
    collection := client.Database("test").Collection("Admin")
    ctx, ctxErr := context.WithTimeout(context.Background(), 5*time.Second)

    if ctxErr != nil {
        log.Println(ctxErr)
    }
    result, resErr := collection.InsertOne(ctx, admin)
    if resErr != nil {
        log.Println(resErr)
    }
    json.NewEncoder(response).Encode(result)
}

And when ran, I receive the following error:

2021/06/05 02:02:39 http: panic serving [::1]:53359: runtime error: invalid memory address or nil pointer dereference

This points to the line where I define collection in the endpoint function, which logged as having a nil value. I am clearly not getting the mongo client properly defined in the module, and not sure of the best practice for maintaining this client connection across multiple modules.


Solution

  • The standard way of doing this while avoiding globals would be to define a struct that represents your server, and its methods would be the handlers. Then the methods share the struct's data, and you place things like your mongo client in there.

    Something like this (in your admin package):

    type Server struct {
      client *mongo.Client
    }
    
    func NewServer(client *mongo.Client) *Server {
      return &Server{client: client}
    }
    
    func (srv *Server) CreateAdminEndpoint(response http.ResponseWriter, request *http.Request) {
      // ...
      // use srv.client here
      //
    }
    

    And now in main, you create the server like this:

     client, err := mongo.Connect(...)
     // handle err!
     srv := admin.NewServer(client)
     
     router := mux.NewRouter()
     router.HandleFunc("/api/admin", srv.CreateAdminEndpoint).Methods("POST")