Switching From Java to Go
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)
}