We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Engineers who find a new job through Golang Works average a 15% increase in salary 🚀

Blog hero image

My journey from Node to Go

OmisNomis 17 April, 2018 (10 min read)

My journey from Node to Go

It is no secret that I have a passion for Node, it’s the language that got me into development, and the one I most enjoy.

As a Software Developer it’s always important to expand your horizons; whether that be learning new skills or languages.

What this Article is

I’ve been meaning to embark on the journey of learning Go, A programming language created at Google in 2009, for a while. This articles intention is to document my learning curve.

How I’ll do it

Node is the language I know the best, so I decided to learn Go by doing what I know. Here’s the plan:

  • Make a simple CRUD API, in Node, to manage a list of To-Do’s.
  • Make the same API, in Go.

Some assumptions

As this isn’t a tutorial on Node, my expectation is you have some knowledge of writing Javascript and how the Express web framework works.

The Application

Before we start writing code it’s always important to plan.

What we’ll make:

  • Ability to Create a To-Do item
  • Ability to Retrieve multiple To-Do’s or just one specific To-Do
  • Ability to Update a To-Do, or multiple To-Do’s
  • Ability to Delete a To-Do, or multiple To-Do’s

What we won’t make/handle:

  • Authentication
  • Testing (I have written previous posts on this, if you want to try it)
  • Frontend (this is an exercise to learn Go after-all)

The Node App

The first step is to create a new project folder(mkdir expressApplication), initialise it (npm init -y) and then install our required packages (npm i express body-parser mongoose morgan).

Then we make the Node app. Seeing as this is not a tutorial on how to write an API using node, I am just going to provide the code below.

'use strict';

const mongoose = require('mongoose');
const morgan = require('morgan');
const bodyParser = require('body-parser');

const app = require('express')();
const port = 8000;

/** Mongoose Schema setup */
const Schema = mongoose.Schema;

const ToDo = mongoose.model(
  'ToDo',
  new Schema({
    title: { type: String },
    completed: { type: Boolean },
  })
);

/** Mongo Connection */
mongoose
  .connect(mlabUri)
  .then(() => {
    console.log('Connected to mongo');
  })
  .catch(error => {
    console.error(`Initial Mongo connection error: ${error}`);
  });

/** App setup */
app.use(morgan('combined'));
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json());

/** Routes */
app.get('/getOne', (req, res, next) => {
  ToDo.findOne(req.query, (err, doc) => {
    if (err) {
      next(err);
    }
    if (!doc || doc.length < 1) {
      console.warn('No document found');
      return res.sendStatus(404);
    }
    res.send(doc).status(200);
  });
});

app.get('/getMultiple', (req, res, next) => {
  ToDo.find(req.query, (err, docs) => {
    if (err) {
      return next(err);
    }
    if (!docs || docs.length < 1) {
      console.warn('No documents found');
      return res.sendStatus(404);
    }
    res.send(docs).status(200);
  });
});

app.post('/addOne', (req, res) => {
  // make sure title is passed to the POST request.
  if (!req.body.title || typeof req.body.title !== 'string') {
    return res.status(400).send('Title not passed or not a string');
  }
  const title = req.body.title.toLowerCase();
  const completed = req.body.completed ? req.body.completed.toLowerCase() : undefined;

  const todo = new ToDo({
    title: title,
    completed: completed && (completed === 'true' || completed === 'false') ? completed : false,
  });

  todo.save((err, todo) => {
    if (err) {
      return next(err);
    }
    res.send(`ToDo created: ${JSON.stringify(todo)}`).status(201);
  });
});

app.put('/updateOne', (req, res, next) => {
  ToDo.findOneAndUpdate(req.query, req.body, (err, doc) => {
    if (err) {
      return next(err);
    }
    if (!doc || doc.length < 1) {
      console.warn('No document found');
      return res.sendStatus(404);
    }
    res.sendStatus(200);
  });
});

app.put('/updateMultiple', (req, res, next) => {
  ToDo.find(req.query, (err, docs) => {
    if (err) return next(err);
    if (!docs || docs.length < 1) {
      console.warn('No document found');
      return res.sendStatus(404);
    }
    ToDo.updateMany(req.query, req.body, (err, doc) => {
      if (err) return next(err);
      res.sendStatus(200);
    });
  });
});

app.delete('/deleteOne', (req, res, next) => {
  ToDo.findOneAndRemove(req.query, (err, doc) => {
    if (err) return next(err);
    if (!doc || doc.length < 1) {
      console.warn('No document found');
      return res.sendStatus(404);
    }
    res.send(doc).status(200);
  });
});

app.delete('/deleteMultiple', (req, res, next) => {
  ToDo.find(req.query, (err, docs) => {
    if (err) return next(err);
    if (!docs || docs.length < 1) {
      console.warn('No document found');
      return res.sendStatus(404);
    }
    ToDo.deleteMany(req.query, (err, doc) => {
      if (err) return next(err);
      res.sendStatus(200);
    });
  });
});

/** Error Handler */
app.use((err, req, res, next) => {
  return res.status(500).send(err.stack || err);
});

/** Start app */
app.listen(port, () => {
  console.info(`App listening on port ${port}`);
});

Note: It is not good practice to have everything in one file, but it’s good enough for this article. You’ll also have to input your Mongo connection string (.connect(mlabUri)) if you plan on running this code.

The Go App

This is the part of the article where I will go into a lot more detail.

At the time of writing this paragraph I have never written a Go app, have no idea about best practices or how the language works properly.

The aim, at the end of this section, is to have a good understanding of the basics, document my learning curve and hopefully help others doing the same.

1_pk4gPFD0OLeKbwKlN1v9YA.png

Installation If you haven’t already, head on over to the Go Downloads page and download/install it.

One thing the installer won’t make clear is workspaces. In fact it won’t even ask you what you think about them.

Workspaces “What the hell is a Go Workspace?” I hear you ask. The short and simple answer is it’s where all of your Go development happens.

We’re not going to change where our Workspace is (if you want to you can) so it defaults to $HOME/go. Any new projects we now start will be done inside $HOME/go/src.

If you have decided to specify a different location for your Go Workspace, you’ll need to make a src directory.

Project Setup Now we have our workspace setup, we can create our first project. Each of our subdirectories inside the src directory will represent a separate package or command, and each will contain at least one .go file.

To get started we’ll make a simple Hello World package:

  • mkdir src/helloWorld (makes a helloWorld directory under src)
  • cd src/helloWorld
  • touch main.go (creates a blank file called main.go)

Now we have our first Go programme (main.go), we should write some code. Open the file up in a text editor and lets add the following code:

package main

import "fmt"

// The entry point for the application is the 'main' function in the 'main' package.
// The main function must take no arguments and return no values.
func main() {
	fmt.Println("Hello World")
}

Nothing fancy. We’re naming our package (main), importing a package (fmt) and then using it to print “Hello World” to the console.

When you have created this file, you can run the following commands:-

  • go run main.go (runs the package)
  • go build (creates a standalone executable that you can run without the Go tool)
  • go install (same as go build, but it places the executable in the workspaces /bin directory so you can run it from anywhere in your filesystem*)
  • For this to work your PATH must include your workspaces /bin directory.

Have a play After making the “Hello World” app, I decided to take 10 minutes to have a play around with the language to see what I could learn.

imports are relative to the src directory

If you have your helloWorld package in src/helloWorld, and you decide to create a sub-package in src/helloWord/anotherPackage, the import statement is relative to the src directory so import “helloWorld/anotherPackage”

Files in the same directory can be used as part of the same package if they have the same name

If, under the helloWorld package, you have a file main.go with the following code

package main

import "fmt"

func main() {
  fmt.Println("Hello World", privateHelperFunc())
}

And another file anotherfile.go with the following code

package main

func privateHelperFunc() int {
  return 25
}

The main.go file will have access to the privateHelperFunc function as they act as though they are part of the same package.

Private functions

Functions that start with a lowercase letter (e.g. privateHelperFunc) are private and cannot be used outside the package.

Public functions

Functions that start with a uppercase letter (e.g. PublicHelperFunc) are public and are exported for use.

Assigning imported packages an alias When importing a package you can alias that package. In the below example, the fmt package is aliased as notfmt:

package main
import (
  notfmt "fmt"
)
func main() {
  notfmt.Println("Hello World")
}

Three folders

In your Go Workspace you will have three folders:

  • src (where your Go projects and source code sits)
  • bin (where your executables go when you install)
  • pkg (where packages you import compile to)

Lets make an app Now it’s time to finally get cracking on making our application!

  • Navigate to your Go workspace (the default is ~/go)
  • Make a new directory under src (mkdir src/goApi)goApi will be our package where we aim to replicate our Node/Express API.

Enter iris We will be building our Go API using iris (which is the Go equivalent of Express). Note: In the middle of writing this article, I became aware of a lot of controversy around Iris. If you are planning on using this package in a live environment I implore you to do some research first.

Iris is a fast, simple yet fully featured and very efficient web framework for Go.

Before we do anything we need to install the iris package. Providing you have installed Go, you can do this using the command go get -u github.com/kataras/iris. Once you have done this it will appear under your pkg directory.

Before we get into the integration with MongoDB or creating all of our CRUD operations, it’s important we have a look at how a basic Iris server looks.


package main

import (
	"github.com/kataras/iris"
	"github.com/kataras/iris/middleware/logger"
	"github.com/kataras/iris/middleware/recover"
)

func main() {
	app := iris.New()
	/**
	* Logging
	 */
	app.Logger().SetLevel("debug")
	// Recover from panics and log the panic message to the application's logger ("Warn" level).
	app.Use(recover.New())
	// logs HTTP requests to the application's logger ("Info" level)
	app.Use(logger.New())

	/**
	* Routes
	 */

	// GET http://localhost:8080/regex/id
	// 'id' must be a string which contacts 10 or more numbers
	app.Get("/regex/{id:string regexp(^[0-9]{10}.*$)}", func(ctx iris.Context) {
		ctx.Writef("Hello %s", ctx.Params().Get("id"))
	})
	// POST http://localhost:8080/panic
	app.Post("/panic", func(ctx iris.Context) {
		// app will handle this panic message thanks to the app.use statement above
		panic("recover will log this panic message to the logger")
	})
	// GET http://localhost:8080/html
	app.Get("/html", func(ctx iris.Context) {
		// Return HTML
		ctx.HTML("<h1>Welcome</h1>")
	})
	// Put http://localhost:8080/ping
	app.Put("/ping", func(ctx iris.Context) {
		// Return a string
		ctx.WriteString("pong")
	})
	// Delete http://localhost:8080/hello
	app.Delete("/hello", func(ctx iris.Context) {
		// Return JSON
		ctx.JSON(iris.Map{"message": "Hello Iris!"})
	})

	// Start the server on port 8080
	// ignore server closed errors ([ERRO] 2018/04/09 12:25 http: Server closed)
	app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}

The above example sets up 5 endpoints and starts the server on port 8080. Here are some important things I learnt while putting it together:

  • A panic typically means something went unexpectedly wrong. This is basically the equivalent of throw new Error() in Node. We use app.Use(recover.New()) so our sever can handle any panics thrown by the endpoints, stopping our application from going down in flames.
  • We can use Regex expressions in route definitions. In the GET request example above we have specified that an id has to be a string that contains 10 or more numbers.
  • Ignore server closed errors. We use iris.WithoutServerError(iris.ErrServerClosed) to prevent errors showing on the console when we close (CTRL + C) our server. If we don’t include this we’ll see [ERRO] 2018/04/09 12:25 http: Server closed every time we close it.

Now let’s make our main package file (touch src/goApi/main.go).

package main

import (
	"github.com/kataras/iris"
	"github.com/kataras/iris/middleware/logger"
	"github.com/kataras/iris/middleware/recover"
	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

// ToDo type
type ToDo struct {
	Title     string `bson:"title"`
	Completed bool   `bson:"completed"`
}

func main() {
	app := iris.New()
	/**
	* Logging
	 */
	app.Logger().SetLevel("debug")
	// Recover from panics and log the panic message to the application's logger ("Warn" level).
	app.Use(recover.New())
	// logs HTTP requests to the application's logger ("Info" level)
	app.Use(logger.New())

	/**
	* Mongo
	 */

	// Connection variables
	const (
		Host       = ""
		Username   = ""
		Password   = ""
		Database   = ""
		Collection = ""
	)

	// Mongo connection
	session, err := mgo.DialWithInfo(&mgo.DialInfo{
		Addrs:    []string{Host},
		Username: Username,
		Password: Password,
		Database: Database,
	})
	// If there is an error connecting to Mongo - panic
	if err != nil {
		panic(err)
	}
	// Close session when surrounding function has returned/ended
	defer session.Close()
	// mongogo is the database name
	db := session.DB(Database)
	// todos is the collection name
	collection := db.C(Collection)

	/**
	* Routes
	 */

	// Create todo using POST Request body
	app.Post("/addOne", func(ctx iris.Context) {
		// Create a new ToDo
		var todo ToDo
		// Pass the pointer of todo so it is updated with the result
		// which is the POST data
		err := ctx.ReadJSON(&todo)
		// If there is an error or no Title in the POST Body
		if err != nil || todo.Title == "" {
			ctx.StatusCode(iris.StatusBadRequest)
			ctx.JSON(iris.Map{"message": "Post body must be a JSON object with at least a Title!"})
			return
		}
		// Insert into the database
		collection.Insert(todo)
	})

	// Get todo by Title
	app.Get("/title/{title:string}", func(ctx iris.Context) {
		title := ctx.Params().Get("title")
		var results []ToDo
		err := collection.Find(bson.M{"title": title}).All(&results)
		if err != nil {
			ctx.StatusCode(iris.StatusBadRequest)
			ctx.JSON(iris.Map{"message": "An error occured", "error": err})
			return
		}
		ctx.JSON(iris.Map{"results": results})
	})
	// Get todo by Completed status
	app.Get("/completed/{completed:boolean}", func(ctx iris.Context) {
		completed, _ := ctx.Params().GetBool("completed")
		var results []ToDo
		err := collection.Find(bson.M{"completed": completed}).All(&results)
		if err != nil {
			ctx.StatusCode(iris.StatusBadRequest)
			ctx.JSON(iris.Map{"message": "An error occured", "error": err})
			return
		}
		ctx.JSON(iris.Map{"results": results})
	})
	// Update one todo by title
	app.Put("/title/{title:string}", func(ctx iris.Context) {
		// Get the title from the URL Parameter
		title := ctx.Params().Get("title")
		// Construct the query object
		query := bson.M{"title": title}
		// Create a new ToDo
		var change ToDo
		// Update change with the PUT body
		ctx.ReadJSON(&change)
		// Only update one record
		collection.Update(query, change)
	})
	// Update todo by completed
	app.Put("/completed/{completed:boolean}", func(ctx iris.Context) {
		// Get the completed status from the URL parameter
		completed, _ := ctx.Params().GetBool("completed")
		query := bson.M{"completed": completed}
		var change ToDo
		ctx.ReadJSON(&change)
		// Update all records
		collection.UpdateAll(query, bson.M{"$set": bson.M{"completed": change.Completed}})
	})
	// Remove by Title
	app.Delete("/title/{title:string}", func(ctx iris.Context) {
		title := ctx.Params().Get("title")
		collection.Remove(bson.M{"title": title})
	})
	// Remove by completed
	app.Delete("/completed/{completed:boolean}", func(ctx iris.Context) {
		completed, _ := ctx.Params().GetBool("completed")
		collection.RemoveAll(bson.M{"completed": completed})
	})

	// Run app on port 8080
	// ignore server closed errors ([ERRO] 2018/04/09 12:25 http: Server closed)
	app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))

Here we’ve made seven endpoints and we’ve connected our application to MongoDB using mgo. As mentioned at the top of this section in the middle of writing this article, I became aware of a lot of controversy around Iris. If you are planning on using this package in a live environment I implore you to do some research first.

One of the most important concepts I learnt while making this API was Pointers.

Pointers allow you to pass a reference to the memory location of a variable, rather than the value itself. This would be, in a Node.js example, like amending a primitive value to a reference value.

This Pointer article has a good explanation and examples.

Closing Thoughts

This exercise taught me a few basics about the Go language, but there is still plenty to learn.

I’ve commented most of my Golang code, rather than write explanations for each line in this article. If you have any questions/comments feel free to ask!

References

https://github.com/alco/gostart

https://golang.org/doc/code.html

Originally published on medium.com