I've been using Golang for a while now. It started off with me just wanting to learn something new, something snappy, something to quickly develop smaller size projects or scripts. I've come to really like this language, more than I thought at the beginning.
I recognize that Go can be a powerful tool, and I want to try and dive even deeper into its nuances and the whole ecosystem built around it.
This is my journey into mastering Go. And I'm going to try to document as much as I can, both for my future self, my colleagues and for other people that can benefit from the lessons I will learn, and the headaches I'll get from it.
Workspaces
I've been aware of Golang's Workspaces for a while, even used them at some point. But now is the time to dive a bit deeper, and setup my project using this feature.
So we start of by creating our workspace:
go work init
This command creates a go.work
file, which for now just contains the version of Go.
// go.work
go 1.21.6
According to the documentation, workspaces can be useful when we have multiple dependent modules on which we are working on at the same time. In this case, we want to avoid having to constantly push our new commits, potentially waiting for pipelines, only to test how our modules interact, and we also want to avoid having to use the replace
directive.
Let's try and setup an example where we have our Golang application and a library, which is used by our main program.
We start by creating our two modules workspace-app
and workspace-lib
.
├── go.work
├── LICENSE
├── README.md
├── workspace-app
│ └── go.mod
└── workspace-lib
└── go.mod
And then we run:
go work use workspace-app/ workspace-lib/
Now our go.work
contains the following:
go 1.21.6
use (
./workspace-app
./workspace-lib
)
Now let's write some code for both the library and the application:
# Tree workspace-lib
.
├── go.mod
└── pkg
└── greeting.go
# Tree workspace-app
.
├── cmd
│ └── greeter
│ └── main.go
├── go.mod
└── go.sum
// pkg/greeting.go
package greeting
type Language int8
const (
English Language = iota
Italian
German
)
func Greet(l Language) string {
g := map[Language]string{
English: "Hello!",
Italian: "Ciao!",
German: "Hallo!",
}
return g[l]
}
// cmd/greeter/main.go
package main
import (
"fmt"
"go.lorenzomilicia.dev/go-master/workspace-lib/pkg/greeting"
)
func main() {
g := greeting.Greet(greeting.Italian)
fmt.Println(g)
}
Having pushed the workspace-lib
to the remote repository, the application is able to get the package from it and then run the code correctly.
What if I now want to do some changes to the library, and at the same time test the dependency on the app, all without having to push to the remote and by 'pretending' like if I was importing the code from it?
Let's say we added Polish
as a new Language
:
const (
English Language = iota
Italian
German
Polish
)
func Greet(l Language) string {
g := map[Language]string{
English: "Hello!",
Italian: "Ciao!",
German: "Hallo!",
Polish: "Cześć!",
}
return g[l]
}
What happens if we try and greet with the newly added language from the workspace-app
?
$ go run cmd/greeter/main.go
Cześć!
Wow! Why does that work? We added the new Language only in our local code, yet our application already speaks the new language! So wait, what happens if we comment the workspace-lib
from our go.work
file?
$ go run cmd/greeter/main.go
# command-line-arguments
cmd/greeter/main.go:10:31: undefined: greeting.Polish
Just like we were expecting.
Conclusion
Golang's workspaces are a neat feature, that comes bundled with all the tools available with the basic installation. It avoids the need to alter the code and the imports during the development process, and therefore reduces mistakes in the code we're pushing to our remote repository and/or deployment system.