Packages in Go
This tutorial is now deprecated. Please click here for the updated version.
Welcome to tutorial number 7 in Golang tutorial series.
What are packages and why are they used?
So far we have seen go programs which have only one file which has a main function with a couple of other functions. In real world scenarios this approach to writing all source code in a single file will not work. It becomes impossible to reuse and maintain code written this way. This is where packages save the day.
Packages are used to organise go source code for better reusability and readability. Packages offer compartmentalisation of code and hence it becomes easy to maintain go applications.
For example let’s say we are creating a go image processing application which offers features such as image cropping, sharpening, blurring and color enhancement. One way to organise this application is to group all code related to a feature in its own package. For example cropping can be an individual package, sharpening can be another package. The advantage of this is, the color enhancement feature might need some of the functionalities of sharpening. The color enhancement code can simply import(we will discuss import in a minute) the sharpening package and start using its functionality. This way the code becomes easy to reuse.
We will step by step create an application which calculates the area and diagonal of a rectangle.
We will understand packages better through this application.
main function and main package
Every executable go application must contain a main function. This function is the entry point for execution. The main function should reside in the main package.
The line of code to specify that a particular source file belongs to a package is package packagename
. This should be first line of every go source file.
Let’s gets started by creating the main function and main package for our application. Create a folder inside the src folder of the go workspace and name it geometry
. Create a file geometry.go
inside the geometry
folder.
Write the following code in geometry.go
//geometry.go
package main
import "fmt"
func main() {
fmt.Println("Geometrical shape properties")
}
The line of code package main
specifies that this file belongs to the main package. The import "packagename"
statement is used to import an existing package. In this case we import the fmt
package which contains the Println method. Then there is a main function which prints Geometrical shape properties
Compile the above program by typing go install geometry
. This command searches for a file with a main function inside the geometry
folder. In this case it finds geometry.go
. It then compiles it and generates a binary named geometry
(geometry.exe
in the case of windows) inside the bin folder of the workspace. Now the workspace structure will be
src
geometry
gemometry.go
bin
geometry
Let’s run the program by typing workspacepath/bin/geometry
. Replace workspacepath
with the path of your go workspace. This command executes the geometry
binary inside the bin folder. You should get Geometrical shape properties
as the output.
Creating custom package
We will structure the code in such a way that all functionalities related to a rectangle are in rectangle
package.
Let’s create a custom package rectangle
which has functions to determine the area and diagonal of a rectangle.
Source files belonging to a package should be placed in separate folders of their own. It is a convention in Go to name this folder with the same name of the package.
So let’s create a folder named rectangle
inside the geometry
folder. All files inside the rectangle
folder should start with the line package rectangle
as they all belong to the rectangle package.
Create a file rectprops.go
inside the rectangle folder we just created and add the following code.
//rectprops.go
package rectangle
import "math"
func Area(len, wid float64) float64 {
area := len * wid
return area
}
func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid))
return diagonal
}
In the above code we have created two functions which calculate Area
and Diagonal
. The area of the rectangle is the product of the length and width. The diagonal of the rectangle is the square root of the sum of squares of the length and width. The Sqrt
function in the math
package is used to calculate the square root.
Note that the function names Area and Diagonal starts with caps. This is essential and we will explain shortly why this is needed.
Importing custom package
To use a custom package we must first import it. import path
is the syntax to import a custom package. We must specify the path to the custom package with respect to the src
folder inside the workspace. Our current folder structure is
src
geometry
geometry.go
rectangle
rectprops.go
The line import "geometry/rectangle"
will import the rectangle package.
Add the following code to geometry.go
//geometry.go
package main
import (
"fmt"
"geometry/rectangle" //importing custom package
)
func main() {
var rectLen, rectWidth float64 = 6, 7
fmt.Println("Geometrical shape properties")
/*Area function of rectangle package used
*/
fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
/*Diagonal function of rectangle package used
*/
fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
The above code imports the rectangle
package and uses the Area
and Diagonal
function of it to find the area and diagonal of the rectangle. The %.2f
format specifier in Printf is to truncate the floating point to two decimal places. The output of the application is
Geometrical shape properties
area of rectangle 42.00
diagonal of the rectangle 9.22
Exported Names
We capitalised the functions Area
and Diagonal
in the rectangle package. This has a special meaning in Go. Any variable or function which starts with a capital letter are exported names in go. Only exported functions and variables can be accessed from other packages. In this case we need to access Area
and Diagonal
functions from the main package. Hence they are capitalised.
If the function names are changed from Area(len, wid float64)
to area(len, wid float64)
in rectprops.go
and from rectangle.Area(rectLen, rectWidth)
to rectangle.area(rectLen, rectWidth)
in geometry.go
and if the program is run, the compiler will throw error geometry.go:11: cannot refer to unexported name rectangle.area
. Hence if you want to access a function outside of a package, it should be capitalised.
init function
Every package can contain a init
function. The init function should not have any return type and should not have any parameters. The init function cannot be called explicitly in our source code. The init function looks like below
func init() {
}
The init function can be used to perform initialisation tasks and can also be used to verify the correctness of the program before the execution starts.
The order of initialisation of a package is as follows
- Package level variables are initialised first
- init function is called next. A package can have multiple init functions (either in a single file or distributed across multiple files) and they are called in the order in which they are presented to the compiler.
If a package imports other packages, the imported packages are initialised first.
A package will be initialised only once even if it is imported from multiple packages.
Let’s make some modifications to our application to understand init functions.
To start with let’s add an init function to the rectprops.go
file.
//rectprops.go
package rectangle
import "math"
import "fmt"
/*
* init function added
*/
func init() {
fmt.Println("rectangle package initialized")
}
func Area(len, wid float64) float64 {
area := len * wid
return area
}
func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid))
return diagonal
}
We have added a simple init function which just prints rectangle package initialised
Now let’s modify the main package. We know that the length and width of a rectangle should be greater than zero. We will define this check using init function and package level variables in the geometry.go
file.
Modify the geometry.go
file as shown below,
//geometry.go
package main
import (
"fmt"
"geometry/rectangle" //importing custom package
"log"
)
/*
* 1. package variables
*/
var rectLen, rectWidth float64 = 6, 7
/*
*2. init function to check if length and width are greater than zero
*/
func init() {
println("main package initialized")
if rectLen < 0 {
log.Fatal("length is less than zero")
}
if rectWidth < 0 {
log.Fatal("width is less than zero")
}
}
func main() {
fmt.Println("Geometrical shape properties")
fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
The following are the changes made to geometry.go
- rectLen and rectWidth variables are moved to package level from the main function level.
- An init function has been added. The init function prints a log and terminates the program execution if either the rectLen or rectWidth is less than zero by using log.Fatal function.
The order of initialisation of the main package is
- The imported packages are first initialised. Hence rectangle package is initialised first.
- Package level variables rectLen and rectWidth are initialised next.
- init function is called.
- main function is called at last
If you run the program, you will get the following output.
rectangle package initialized
main package initialized
Geometrical shape properties
area of rectangle 42.00
diagonal of the rectangle 9.22
As expected the init function of the rectangle package is called first followed by the initialisation of the package level variables rectLen and rectWidth. The init function of the main package is called next. It checks whether rectLen and rectWidth are lesser than zero and terminates if the condition is true. We will learn about if
statement in detail in a separate tutorial. For now you can assume that if rectLen < 0
will check whether rectLen
is less than 0 and if it is so, the program will be terminated. We have written a similar condition for rectWidth
. In this case both the conditions are false and the program execution continues. Finally the main function is called.
Let’s modify this program a bit to learn the use of the init function.
Change the line var rectLen, rectWidth float64 = 6, 7
in geometry.go
to var rectLen, rectWidth float64 = -6, 7
. We have initialised rectLen
to negative.
Now if you run the application, you will see
rectangle package initialized
main package initialized
2017/04/04 00:28:20 length is less than zero
As usual the rectangle package is initialised followed by the package level variables rectLen and rectWidth in the main package. rectLen is negative. Hence when the init function runs next, the program terminates after printing length is less than zero
.
The code is available for download at github.
Use of blank identifier
It is illegal in Go to import a package and not to use it anywhere in the code. The compiler will complain if you do so. The reason for this is to avoid bloating of unused packages which will significantly increase the compilation time. Replace the code in geometry.go
with the following,
//geometry.go
package main
import (
"geometry/rectangle" //importing custom package
)
func main() {
}
The above program will throw error geometry.go:6: imported and not used: "geometry/rectangle"
But it is quite common to import packages when the application is under active development and use them somewhere in the code later if not now. The _
blank identifier saves us in those situations.
The error in the above program can be silenced by the following code,
package main
import (
"geometry/rectangle"
)
var _ = rectangle.Area //error silencer
func main() {
}
The line var _ = rectangle.Area
silences the error. We should keep track of these kind of error silencers and remove them including the imported package at the end of application development if the package is not used. Hence it is recommended to write error silencers in the package level just after the import statement.
Sometimes we need to import a package just to make sure the initialisation takes place even though we do not need to use any function or variable from the package. For example, we might need to ensure that the init function of the rectangle package is called even though we do not use that package anywhere in our code. The _ blank identifier can be used in this case too as show below.
package main
import (
_ "geometry/rectangle"
)
func main() {
}
Running the above program will output rectangle package initialized
. We have successfully initialised package even though it is not used anywhere in the code.
Hope you enjoyed reading. Please leave your feedback and comments. Please consider sharing this tutorial on twitter and LinkedIn. Have a good day.
Next tutorial - if else statement