⚠️ Migrate to Go Modules
Go provides a new feature called Modules which provides flexibility to manage your project and dependencies with ease. If you are working on Go packages, then you should consider relocating your project to Go Modules. Read more about Go Modules.
If you are familiar to languages like Java or JavaScript on Node, then you might be quite familiar with packages. A package is nothing but a directory with some code files, which exposes different variables (features) from a single point of reference. Let me explain, what that means.
Imagine you have more than a thousand functions that you need constantly while working on any project. Some of these functions have common behavior. For example, toUpperCase
and toLowerCase
function transforms case of a string
, so you write them in a single file (probably case.go
). There are other functions which do some other operations on string
data type, so you write them in a separate file.
Since you have many files which do something with string
data type, so you created a directory named string
and put all string
related files into it. Finally, you put all of these directories in one parent directory which will be your package. The whole package structure looks like below.
package-name
├── string
| ├── case.go
| ├── trim.go
| └── misc.go
└── number
├── arithmetics.go
└── primes.go
I will explain thoroughly, how we can import functions and variables from a package and how everything blends together to form a package, but for now, imagine your package as a directory containing .go
files.
Every Go program must be a part of some package. As discussed in Getting started with Go lesson, a standalone executable Go program must have package main
declaration. If a program is part of the main package, then go install
will create a binary file; which upon execution calls main
function of the program. If a program is part of a package other than main
, then a package archive file is created with go install
command. Don’t worry, I will explain all this in upcoming topics.
Let’s create an executable package. As we know, to create a binary executable file, we need our program to be a part of main
package and it must have main
function which is the entry point of execution.
A package name is the name of the directory contained in src
directory. In the above case, app
is the package since app
is the child directory of src
directory. Hence, go install app
command looked for app
sub-directory inside src
directory of GOPATH
(since GOROOT
doesn’t have it).
Then it compiled the package and created app
binary executable file (same as the package name) inside bin
directory (set by GOBIN
) which should be executable from the terminal since bin
directory in the PATH
.
💡 Package declaration which should be first line of code like
package main
in above example, can be different than package name. Hence, you might find some packages where package name (name of the directory) is different than package declaration. When you import a package, package declaration is used to create package reference variable, explained later in the article.
go install <package>
command looks for any file with main
package declaration inside given package
directory. If it finds a file, then Go knows this is an executable program and it needs to create a binary file. A package can have many files but only one file with main
function, since that file will be the entry point of the execution.
If a package does not contain a file with main
package declaration, then Go creates a package archive (.a
) file inside pkg
directory.
Since, app
is not an executable package, it created app.a
file inside pkg
directory. We can not execute this file as it’s not a binary executable file.
Package naming convention
Go community recommends to use plain and simple names for packages. For example, strutils
for string utility functions or http
for HTTP requests related functions. A package names with under_scores
, hy-phens
or mixedCaps
should be avoided.
Creating a package
As we discussed, there are two types of packages. An executable package and a utility package. An executable package is your main application since you will be running it. A utility package is not self-executable, instead, it enhances the functionality of an executable package by providing utility functions and other important assets.
As we know, a package is nothing but a directory. Let’s create greet
directory inside src
and put some files in it. This time, we will write package greet
a declaration on the top of each file to state that this is a utility package.
Export members
A utility package is supposed to provide some variables to a package who imports it. Like export
syntax in JavaScript
, Go exports a variable if a variable name starts with Uppercase. All other variables not starting with an uppercase letter is private to the package.
⚠️ I am going to use variable word from now on in this article, to describe an export member but export members can be of any type like
constant
,map
,function
,struct
,array
,slice
etc.
Let’s export a greeting variable from day.go
file.
In the above program, Morning
variable will be exported from the package but not the morning
variable since it starts with a lowercase letter.
Importing a package
Now, we need an executable package which will consume our greet
package. Let’s create an app
directory inside src
and create entry.go
file with main
package declaration and main
function. Note here, Go packages do not have an entry file naming system like index.js
in Node. For an executable package, a file with main
function is the entry file for execution.
To import a package, we use import
syntax followed by the package name.
💡 Unlike other programming languages, a package name can also be a subpath like
some-dir/greet
and Go will automatically resolve the path to thegreet
package for us as you will see in the nested package topic ahead.
Go first searches for package directory inside GOROOT/src
directory and if it doesn’t find the package, then it looks for GOPATH/src
. Since, fmt
package is part of Go’s standard library which is located in GOROOT/src
, it is imported from there. But since Go cannot find greet
package inside GOROOT
, it will lookup inside GOPATH/src
and we have it there.
Above program throws compilation error, as morning
variable is not visible from the package greet
. As you can see, we use .
(dot) notation to access exported members from a package. When you import a package, Go creates a global variable using the package declaration of the package. In the above case, greet
is the global variable created by Go because we used package greet
declaration in programs contained in greet
package.
We can group fmt
and greet
package imports together using grouping syntax (parentheses). This time, our program will compile just fine, because Morning
variable is exported from the greet
package.
💡 BTW, you can run an executable Go package from anywhere using
go run <package-name>
command. Go will try to resolve the package fromGOROOT
orGOPATH
directories and execute themain
function. Hence, in the above example, you could have just executed thego run app
command.
Nested package
We can nest a package inside a package. Since for Go, a package is just a directory, it’s like creating a subdirectory inside an already existing package. All we have to do is write an appropriate package declaration and provide a relative path of the nested package when we import it.
As you can see from the above example, Go has created the de
variable from the greet/de
package import which contains all the exports from the de
sub-package. The de
variable name comes from the package de
declaration.
Package compilation
As discussed in the previous lesson, go run
command compiles and executes a program. We know, go install
command compiles a package and creates a binary executable file or package archive file.
The package archive file is created to avoid compilation of the package every single time it is imported in a program. The go install
command pre-compiles a package and Go refers to .a
files.
💡 Generally, when you install a 3rd party package, Go compiles the package and create package archive file. If you have created a package locally, then your IDE might create package archive as soon as you save the file in the package or when package gets modified. VSCode compiles the package when you save it if you have Go plugin installed.
However, the nested packages won’t be compiled into archive files when you use go install <package-name>
command. To create an archive file for the nested package, you need to use the same command but will the nested package path like go install greet/de
in the previous example.
Package initialization
When we run a Go program, the Go compiler follows a certain execution order for packages, files in a package and variable declaration in those files.
Package scope
A scope is a region in a code block where a defined variable is accessible. A package scope is a region within a package where a declared variable is accessible from within a package (across all the files in the package). This region is the top-most block of any file in the package.
Take a look at go run
command. This time, instead of executing one file, we have a glob pattern to include all the files inside app
package for execution.
Go is smart enough to figure out an entry point of the application which is entry.go
because it has main
function. We can also use a command like below (the filename order doesn’t matter).
$ go run src/app/version.go src/app/entry.go
💡 You can also use
go run app
command which does the same thing and this is a better approach to execute a package.go install
orgo build
command requires a package name, which includes all the files inside a package, so we don’t have to specify them like above.
Coming back to our main issue, we can use variable version
declared in version.go
file from anywhere in the package even though it is not exported (like Version
), because it is declared in package scope.
If the version
variable would have been declared inside a function, it wouldn’t have been in the package scope and the above program would have failed to compile.
You are not allowed to redeclare a global variable with the same name in the same package. Hence, once version
variable is declared, it can not be redeclared in the package scope. But you are free to re-declare elsewhere.
Variable initialization
When a variable a
depends on another variable b
, b
should be defined beforehand, else program won’t compile. Go follows this rule inside functions.
But when these variables are defined in package scope, they are declared in initialization cycles. Let’s have a look at the simple example below.
In the above example, first c
is declared because its value is already declared. In later initialization cycle, b
is declared, because it depends on c
and value of c
is already declared. In the final initialization cycle, a
is declared and assigned to the value of b
. Go can handle complex initialization cycles like below.
In the above example, first c
will be declared and then b
as its value depends on c
and finally a
as its value depends on b
. You should avoid any initialization loop like below where initialization gets into a recursive loop.
Another example of package scope would be, having a function f
in a separate file which references variable c
from the main file.
Init function
Like main
function, init
function is called by Go when a package is initialized. It does not take any arguments and doesn’t return any value.
The init
function is implicitly declared by Go, hence you can not reference it from anywhere (or call it like init()
). You can have multiple init
functions in a file or a package. Order of the execution of init
function in a file will be according to the order of their appearances.
You can have init
function anywhere in the package. These init
functions are called in lexical file name order (alphabetical order).
After all theinit
functions are executed, main
function is called. Hence, the main job of init
function is to initialize global variables that cannot be initialized in the global context. For example, initialization of an array.
Since, for
syntax is not valid in package scope, we can initialize the array integers
of size 10
using for
loop inside init
function.
Package alias
When you import a package, Go create a variable using the package declaration of the package. If you are importing multiple packages with the same name, this will lead to a conflict.
// greet/parent.go
package greet
var Message = "Hey there. I am parent."
// greet/greet/child.go
package greet
var Message = "Hey there. I am child."
Hence, we use package alias. We state a variable name in between import
keyword and package name which becomes the new variable to reference the package.
In the above example, greet/greet
package now is referenced by child
variable. If you notice, we aliased greet
package with an underscore.
Underscore is a special character in Go which acts as null
container. Since we are importing greet
package but not using it, Go compiler will complain about it. To avoid that, we are storing reference of that package into _
and Go compiler will simply ignore it.
Aliasing a package with an underscore which seems to do nothing is quite useful sometimes when you want to initialize a package but not use it.
// greet/parent.go
package greet
import "fmt"
var Message = "Hey there. I am parent."
func init() {
fmt.Println("greet/parent.go ==> init()")
}
// greet/greet/child.go
package greet
import "fmt"
var Message = "Hey there. I am child."
func init() {
fmt.Println("greet/greet/child.go ==> init()")
}
The main thing to remember is, an imported package is initialized only once per package. Hence if you have multiple import statements in a package, an imported package is going to be initialized only once in the lifetime of main package execution.
💡 If we use
.
(dot) as an alias likeimport . "greet/greet"
then all the export members ofgreet
package will be available in the local file block scope and we can referenceMessage
without using qualifierchild
. Hence,fmt.Println(Message)
would work just fine. This type of import is called as Dot Import and Go community is not very fond of it as it can cause some issues.
Program execution order
So far, we understood everything there is about packages. Now, let’s combine our understanding of how a program initializes in Go.
go run *.go
├── Main package is executed
├── All imported packages are initialized
| ├── All imported packages are initialized (recursive definition)
| ├── All global variables are initialized
| └── init functions are called in lexical file name order
└── Main package is initialized
├── All global variables are initialized
└── init functions are called in lexical file name order
Here is a small example to prove it.
// version/get-version.go
package version
import "fmt"
func init() {
fmt.Println("version/get-version.go ==> init()")
}
func getVersion() string {
fmt.Println("version/get-version.go ==> getVersion()")
return "1.0.0"
}
/***************************/
// version/entry.go
package version
import "fmt"
func init() {
fmt.Println("version/entry.go ==> init()")
}
var Version = getLocalVersion()
func getLocalVersion() string {
fmt.Println("version/entry.go ==> getLocalVersion()")
return getVersion()
}
/***************************/
// app/fetch-version.go
package main
import (
"fmt"
"version"
)
func init() {
fmt.Println("app/fetch-version.go ==> init()")
}
func fetchVersion() string {
fmt.Println("app/fetch-version.go ==> fetchVersion()")
return version.Version
}
/***************************/
// app/entry.go
package main
import "fmt"
func init() {
fmt.Println("app/entry.go ==> init()")
}
var myVersion = fetchVersion()
func main() {
fmt.Println("app/fetch-version.go ==> fetchVersion()")
fmt.Println("version ===> ", myVersion)
}
Installing 3rd party package
Installing a 3rd party package is nothing but cloning the remote code into a local src/<package-name>
directory. Unfortunately, Go does not support package version or provide package manager but a proposal is waiting here.
Go does not have a centralize official package registry, it asks you to provide hostname and path to the package.
$ go get -u github.com/jinzhu/gorm
Above command imports files from http://github.com/jinzhu/gorm
URL and saves it inside src/``github``.com/jinzhu/gorm
directory. As discussed in nested packages, you can import gorm
package like below.
package main
import "github.com/jinzhu/gorm"
// use ==> gorm.SomeExportedMember
So, if you made a package and want people to use it, just publish it on GitHub and you are good to go. If your package is executable, people can use it as a command-line tool else they can import it in a program and use it as a utility module. The only thing they need to do is use the below command.
$ go get github.com/your-username/repo-name
Migrate to Go Modules
Go provides a new feature called Modules which provides flexibility to manage your project and dependencies with ease. If you are working on Go packages, then you should consider relocating your project to Go Modules. Read more about Go Modules.