DEV Community

Josh Ellis
Josh Ellis

Posted on

How to Setup MongoDB with Go

After struggling through this and combining a few different resources, I wrote a reference guide for next time. My hope in sharing it is that it will help someone else.

Disclaimer: I'm still relatively new to using MongoDB in Go, so there's no guarantee any of this is the best way... I just know it works for me. I'm happily open to suggestions!

QuickStart

I set up a repo on GitHub if you want to jump straight into the code: https://github.com/imjoshellis/mongo-go-skeleton

Prerequisites

This guide skips the basics. I'm assuming you have an instance of MongoDB ready to connect to and that you know how to set up a Go project with go mod init.

I'm also hiding imports in the snippets to save space, but you can check the GitHub repo for the files with full import info.

User Entity

We'll start with the model so you can see where this is headed. I've found annotating a struct to be the simplest way to handle MongoDB in Go. Let's imagine you want to create a simple user type that has an id, username, and email. You'd declare the type like so.

The bson tags are important so the mongo driver can understand how to associate the data. The json tags won't be consumed in this project. But since you're likely to use this in the context of an API that sends json, you can see what both json and bson tags would look like together.

// src/entities/user.go
type User struct {
 ID       primitive.ObjectID `bson:"_id, omitempty" json:"id"`
 Username string             `bson:"username" json:"username"`
 Email    string             `bson:"email" json:"email"`
}
Enter fullscreen mode Exit fullscreen mode

We'll add two functions here for writing and reading users later, but for now, the struct is good enough.

Connecting to the Database

In my case, I've chosen to create a separate package for data. We'll use the special init function to get the Mongo client up and running the first time its imported.

Since the focus here is on getting MongoDB up and running asap, we'll be working with the db directly in main.go, but I'd normally have an app.go with routes/controllers.

Let's start building the db file. Full file on GitHub

Note: Some of the conventions are from this fantastic guide

To start with, I've chosen to expose the mongo client and collection to the other parts of the app:

// src/data/users.db.go
var (
 Client     *mongo.Client
 Collection *mongo.Collection
)
Enter fullscreen mode Exit fullscreen mode

We'll be using environment variables for the database connection info, so let's set up some constants:

// src/data/users.db.go
type key string

const (
 hostKey     = key("hostKey")
 usernameKey = key("usernameKey")
 passwordKey = key("passwordKey")
 databaseKey = key("databaseKey")
)
Enter fullscreen mode Exit fullscreen mode

Next, we write an init function. Again, this is a special named function that will be run when the package is first imported, so it never has to be manually called as it will run once automatically:

// src/data/users.db.go
func init() {
 var err error
 ctx := context.Background()
 ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
 defer cancel()
 ctx = context.WithValue(ctx, hostKey, os.Getenv("MONGO_HOST"))
 ctx = context.WithValue(ctx, usernameKey, os.Getenv("MONGO_USERNAME"))
 ctx = context.WithValue(ctx, passwordKey, os.Getenv("MONGO_PASSWORD"))
 ctx = context.WithValue(ctx, databaseKey, os.Getenv("MONGO_DATABASE"))
 db, err := configDB(ctx)
 if err != nil {
  log.Fatalf("Database configuration failed: %v", err)
 }
 Collection = db.Collection("users") // Change me!
 log.Info("Successfully connected to MongoDB")
}
Enter fullscreen mode Exit fullscreen mode

The main purpose of the init function is to grab the environment variables and set them onto the context. Then, it attempts to call configDB, which will actually create the db connection with error checks and return the database if it succeeds. You'll want to customize the database name:

// src/data/users.db.go
func configDB(ctx context.Context) (*mongo.Database, error) {
 uri := fmt.Sprintf(`mongodb://%s:%s@%s/%s`,
  ctx.Value(usernameKey).(string),
  ctx.Value(passwordKey).(string),
  ctx.Value(hostKey).(string),
  ctx.Value(databaseKey).(string),
 )
 Client, err := mongo.NewClient(options.Client().ApplyURI(uri))
 if err != nil {
  return nil, fmt.Errorf("couldn't connect to mongo: %v", err)
 }
 err = Client.Connect(ctx)
 if err != nil {
  return nil, fmt.Errorf("client couldn't connect with context: %v", err)
 }
 db := Client.Database("appName") // Change me!
 return db, nil
}
Enter fullscreen mode Exit fullscreen mode

Back in the init function, I've also added some temporary functions for development. The first wipes the collection every time init is run. For obvious reasons, you don't want this in production:

// src/data/users.db.go
func init() {
// ...
 _, err = Collection.DeleteMany(ctx, bson.M{})
 if err != nil {
  log.Fatalf("Deleting users collection failed %v", err)
 }
 log.Warn("Users collection was reset! You probably don't want this to happen in production...")
// ...
}
Enter fullscreen mode Exit fullscreen mode

The next thing I have is an example of how you would make fields unique:

// src/data/users.db.go
func init() {
// ...
keys := []string{"email", "username"}
 for _, k := range keys {
  _, err = Collection.Indexes().CreateOne(
   ctx,
   mongo.IndexModel{
    Keys:    bson.D{{Key: k, Value: 1}},
    Options: options.Index().SetUnique(true),
   },
  )
  if err != nil {
   log.Fatalf("Failed to create unique index on %v: %v", k, err)
  }
  }
// ...
}
Enter fullscreen mode Exit fullscreen mode

This will make it so both email and username are unique values. I've seen it argued that it's better to do this in the Mongo shell, and that's a valid way of doing it. I find doing it this way is easier to avoid errors, especially during development.

Save and Get

Now that we have the database ready to go, let's go back to the User entity file and create two functions for saving to and reading from MongoDB:

// src/entities/user.go
func (u *User) Save() error {
 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 defer cancel()
 res, err := data.Collection.InsertOne(ctx, bson.M{"username": u.Username, "email": u.Email})
 if err != nil {
  log.Error(err)
  return fmt.Errorf("there was a problem saving to the db")
 }
 u.ID = res.InsertedID.(primitive.ObjectID)
 return nil
}
Enter fullscreen mode Exit fullscreen mode

This part is pretty straightforward. We get context, use the InsertOne() function to insert the user, and set the ID field on the user based on the result of InsertOne().

Here's the Get() function:

// src/entities/user.go
func (u *User) Get() error {
 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 defer cancel()
 filter := bson.D{{Key: "_id", Value: u.ID}}
 err := data.Collection.FindOne(ctx, filter).Decode(&u)
 if err != nil {
  return fmt.Errorf("user %s not found", u.ID.Hex())
 }
 return nil
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we're setting up a filter based on the user's ID, then calling FindOne() to the collection. The Decode() function will fill in the blanks on the user if successful.

Testing it Out in Main

Now, normally you'd set up API routes and controllers and all that jazz, but to quickly test whether this is working, I've set up main() to make a user, save them, and query the database for them. I'm using fiber to make an http server to make sure the connection to MongoDB is kept open long enough.

// src/main.go
func main() {
 var user entities.User
 var err error
 user.Username = "imjoshellis"
 user.Email = "josh@imjoshlis.com"
 if user.ID == primitive.NilObjectID {
  log.Info("User has nil ObjectID. Attempting to save...")
 }
 err = user.Save()
 if err != nil {
  log.Fatal("Error reading user from db: %v", err)
 }
 if user.ID != primitive.NilObjectID {
  log.Info("User has a generated ObjectID. Save was successful.")
 }

 var readUser entities.User
 readUser.ID = user.ID
 if readUser.Email == "" {
  log.Info("New user entity created with matching ID. Attempting to read...")
 }
 err = readUser.Get()
 if err != nil {
  log.Fatal("Error reading user from db: %v", err)
 }
 if readUser.Email == user.Email {
  log.Info("New user has the same email as original. Read was successful.")
 }

 defer func() {
  log.Println("Disconnecting from MongoDB...")
  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  defer cancel()
  if err := data.Client.Disconnect(ctx); err != nil {
   log.Fatal(err)
  }
 }()

 app := fiber.New()
 log.Fatal(app.Listen("localhost:8080"))
}
Enter fullscreen mode Exit fullscreen mode

Running Main with Variables

Finally, you'll want to make sure to include the correct variables when you run go run main.go.

In my case, it looks like this:

MONGO_HOST="localhost:27017" MONGO_USERNAME=myUserAdmin MONGO_PASSWORD=admin go run src/main.go
Enter fullscreen mode Exit fullscreen mode

If everything went well, you should see a bunch of logs, followed by a message from fiber saying the server is up and running:

WARN[0000] Users collection was reset! You probably don't want this to happen in production...
INFO[0000] Successfully connected to MongoDB
INFO[0000] User has nil ObjectID. Attempting to save...
INFO[0000] User has a generated ObjectID. Save was successful.
INFO[0000] New user entity created with matching ID. Attempting to read...
INFO[0000] New user has the same email as original. Read was successful.
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it! You'll of course want to add a lot more functionality, including API endpoints, update/delete methods, more entities, etc... But hopefully this helped you get started with MongoDB and Go!

Top comments (0)