Part 7: Packages

This is the 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 lets 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 a 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.

Lets 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 a 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

Lets 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.

Lets 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 lets 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

  1. Package level variables are initialised first
  2. 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.

Lets make some modifications to our application to understand init functions.

To start with lets add a 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 lets 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

  1. rectLen and rectWidth variables are moved to package level from the main function level.
  2. 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

  1. The imported packages are first initialised. Hence rectangle package is initialised first.
  2. Package level variables rectLen and rectWidth are initialised next.
  3. init function is called.
  4. 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.

Lets 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.

Thats it for packages. Hope you enjoyed reading. Please leave your valuable comments and feedback :).

Next tutorial - if else statement