Like many people, I came to Go with a background in Java. Getting started on Go isn’t too hard, but it does require a change in mindset when it comes to a few key areas.

Declaring Interfaces

Like in Java, Go interfaces are a powerful way to make code testable and modular. However, the way they are used can make a large difference in how clear the code is.

Antipattern: Creating an Interface Adjacent to the Implementation

type UserDAO interface {
	getUser(id string) (model.User, error)
	createUser(user model.User) (model.User, error)
	updateUser(user model.User) error
	deleteUser(user model.User) error
}

type UserDAOImpl struct {
	// fields
}

// functions for UserDAOImpl to implement UserDAO

This code looks fine, right? It adds an abstraction layer over data access to make room for alternative implementations and increase testability. However, we can do better.

Solution: Creating Interfaces as Needed

// user_dao.go
type UserDAO struct {
	// fields
}

func (dao *UserDAO) getUser(id string) (model.User, error) {
	// get user from db
}

// create, update, and delete are also defined for *UserDAO

// user_service.go
type UserReader interface {
	getUser(id string) (model.User, error)
}

type UserService struct {
	userReader UserReader
}

In this case, we define the UserDAO as a struct and do not assume what interfaces will be needed. When the UserDAO is needed in the UserService, and it only needs the getUser function, a strict interface UserReader is defined to make it immediately clear that the UserService only cares about getting users.

Using Pointers Correctly

Antipattern: Defaulting to Pointers

func (dao *UserDAO)  CreateUser(user *model.User) error {
	// insert into database
	// mutate user with generated "id" field
}

In Java, everything is a pointer (besides primitives), so it can be tempting to bring that practice into Go. This can work, but it increases the chances of encountering a runtime error due to a nil pointer. Besides, what is the point of switching from Java if you still spend hours debugging the Go equivalent to a NullPointerExceptions.

Solution: Only Use Pointers when Needed

Use pointers when:

  • the changes made inside of a function should persist, like in json.Unmarshal
  • passing a very large struct around. Even in this case, I would recommend to avoid it unless it is confirmed to be a bottleneck
  • a <nil> value is needed to differentiate from an empty value

If there is not a specific reason to go with a pointer, don’t do it.

Using Libraries Instead of Frameworks

In the world of Java, writing a web server without Spring is nearly unheard of. Although the standard library has improved in recent years, Spring is still the first thing that comes to mind. When I started with Go, I spent a lot of time researching the best web frameworks and found that everyone recommended not to use one. I was skeptical at first, but after some time it became clear that the standard library is enough for most use cases. If you don’t believe me, look at how simple a request handler is to write.

func createUser(w http.ResponseWriter, r *http.Request) {
	reqBody, _ := ioutil.ReadAll(r.Body)
	var user model.User 
	json.Unmarshal(reqBody, &user)
	
	// delegate to service layer
	
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(user)
}