Introduction
In the previous section, you completed a basic Go program, which included the following lines of code:
package main
import "fmt"
How do we understand these two lines of code? How do we use the package and import statements effectively?
In this lab, you will learn how to create and import packages in Go. This will enable you to organize your code into reusable modules, making your Go projects more maintainable and scalable.
Knowledge Points:
- Definition and declaration of a package
- Understanding exported (public) and unexported (private) identifiers
- Different forms of importing packages: single, grouped, dot, alias, and anonymous imports
Declaring and Defining Packages
In this step, you will declare a reusable package and distinguish exported identifiers from package-private identifiers.
A package in Go is similar to modules in Python or libraries in C. It is a collection of source code files used to organize and reuse code. Every Go file must declare a package at the beginning of the file.
Note: A Go executable is built from a
mainpackage containing amainfunction. Reusable packages do not need that entry point. A module can contain several command directories, each with its ownmainpackage.
Key Points:
Exported (Public) Identifiers: Identifiers (variables, functions, types, etc.) that begin with an uppercase letter are accessible from other packages. Think of these as the public interface of your package.
Unexported (Private) Identifiers: Identifiers that begin with a lowercase letter are only accessible within the same package. These are considered internal implementation details of the package.
Package Cohesion: All files in the same folder must belong to the same package. This ensures that related code stays together.
Package Naming Conventions: Package names should be lowercase, short, and descriptive, avoiding underscores or capital letters.
Let's create our own custom package:
Create a folder named propagandist and a file propagandist.go inside it:
mkdir ~/project/propagandist
touch ~/project/propagandist/propagandist.go
Write the following code in propagandist.go:
package propagandist
var Shout = "I Love LabEx" // Public variable
var secret = "I love the dress" // Private variable
func Hit() string {
return "Don't hit me, please!"
}
Shoutis public because it starts with an uppercase letter. This means you can access it from other packages that importpropagandist.secretis private because it starts with a lowercase letter. It can only be used within thepropagandistpackage.Hitis a public function, accessible from other packages.
Initialize a Go module for the package:
cd ~/project/propagandist
go mod init propagandist
This command initializes a new Go module in the propagandist directory, which helps manage dependencies for the package.
Single-Item Import
In this step, you will import and use your local package with separate import declarations.
To use the propagandist package, let's create a new Go program. This step will demonstrate how to import and use a single package in your Go code.
Create a new Go file named pacExercise.go in the project folder:
touch ~/project/pacExercise.go
Initialize a Go module for the program:
cd ~/project
go mod init pacExercise
Update the go.mod file to include the local package dependency, run the following command in the terminal:
echo "replace propagandist => ./propagandist" >> go.mod
Important: This command adds a replace directive to your go.mod file. This is crucial because it tells Go that the propagandist package should be sourced from the local directory ./propagandist instead of attempting to download it from a remote repository. You should execute this command in your terminal, which will append the line replace propagandist => ./propagandist to your go.mod file. Don't directly write this line into the file manually.
Write the following code in pacExercise.go to import and use the propagandist package:
package main
import "fmt"
import "propagandist"
func main() {
fmt.Println(propagandist.Shout) // Access the public variable
}
- This code imports the
fmtpackage for printing output and thepropagandistpackage. - It then accesses the public variable
Shoutfrom thepropagandistpackage usingpropagandist.Shout.
Run the program:
go mod tidy
go run pacExercise.go
The go mod tidy command ensures that your go.mod file is updated with any new dependencies. The go run pacExercise.go command compiles and executes the program.
Expected Output:
I Love LabEx
Grouped Imports
In this step, you will group package imports without changing the program behavior.
When importing multiple packages, you can use grouped imports for better readability and organization. This is a stylistic choice and doesn't change the functionality of your code.
Modify pacExercise.go to use grouped imports:
```go
package main
import (
"fmt"
"propagandist"
)
func main() {
fmt.Println(propagandist.Shout)
}
```
In the code snippet above, the packages `fmt` and `propagandist` are imported inside a single `import` block enclosed in parentheses. This makes it easier to read and manage multiple package imports. Unlike the separate import declarations in the previous step, this groups both package paths in one declaration. The program behaves the same.
Run the program to confirm it still works:
```bash
go run pacExercise.go
```
The program should execute without errors and output the same result as before.
Dot Import
In this step, you will examine dot imports and their namespace tradeoffs.
Using a dot import, you can omit the package name prefix when calling its functions or variables. This is often discouraged in favor of explicit package names because it can lead to namespace conflicts and reduce readability. However, it's good to know what it is.
Modify pacExercise.go to use a dot import for fmt:
```go
package main
import . "fmt"
import "propagandist"
func main() {
Println(propagandist.Shout) // No `fmt.` prefix needed
}
```
- Here,
import . "fmt"means you can use functions and variables from thefmtpackage directly without thefmt.prefix. - For example, you use
Printlninstead offmt.Println.
Run the program:
```bash
go run pacExercise.go
```
Expected Output:
```bash
I Love LabEx
```
Alias Import
In this step, you will give an imported package a local alias.
You can alias an imported package for clarity or to avoid conflicts when two packages have similar names. This is helpful for making your code more readable and managing namespace collisions.
Modify pacExercise.go to alias fmt as io:
```go
package main
import io "fmt"
import "propagandist"
func main() {
io.Println(propagandist.Shout) // Use the alias `io` instead of `fmt`
}
```
- `import io "fmt"` creates an alias `io` for the `fmt` package.
- Now, you use `io.Println` instead of `fmt.Println`.
Run the program:
```bash
go run pacExercise.go
```
Anonymous Import
In this step, you will import a package only for its initialization side effects.
Anonymous imports are used to import a package for its side effects, such as running its init() function, without needing to directly reference any of its exported identifiers. This is useful for packages that register drivers or perform other initialization tasks.
Modify pacExercise.go to include an anonymous import for time:
```go
package main
import (
"fmt"
"propagandist"
_ "time" // Anonymous import
)
func main() {
fmt.Println(propagandist.Shout)
}
```
- `import _ "time"` is an anonymous import. The underscore `_` is used as a blank identifier, telling the compiler that you are importing the package for its side effects and won't be referencing anything from it directly in your code.
- The `time` package's `init()` function will execute when this program runs. The `time` package does not have any particular side effects visible here, however, many packages use this to register database drivers, or configuration settings.
Run the program:
```bash
go run pacExercise.go
```
Expected Output:
```bash
I Love LabEx
```
Understanding the init() Function
In this step, you will trace package variables and init() functions before main() runs.
The init() function is a special function in Go that plays a crucial role in package initialization. It's automatically executed by Go when a package is imported, before any other code in the main function runs. This section will explain the details of init() functions and how they work within Go's initialization process.
Key Points About init() Functions:
Definition and Purpose:
- An
init()function has no parameters and no return values:func init() {} - It's used for package initialization tasks such as setting up initial states, registering drivers, or validating prerequisites.
Execution Order:
- Go guarantees that package initialization occurs only once, even if a package is imported multiple times.
- The initialization follows a well-defined order:
- Package level variables are initialized first
- Then
init()functions are executed - Finally, the
main()function runs (only in the main package)
Multiple init() Functions:
- A single Go file can contain multiple
init()functions - Multiple files in the same package can each have their own
init()functions - All these
init()functions execute in source declaration order, using the order in which files are presented to the compiler. The Go tool presents package files in lexical filename order; avoid relying on file ordering to express dependencies.
Dependency Chain:
- When packages import other packages, Go ensures that the
init()functions in dependencies are executed first - This creates a bottom-up initialization flow: deepest dependencies initialize first
Let's create a practical example to demonstrate how init() functions work:
First, let's modify our propagandist package to include an init() function. Update propagandist.go:
package propagandist
import "fmt"
var Shout = "I Love LabEx" // Public variable
var secret = "I love the dress" // Private variable
var initialized bool
func init() {
fmt.Println("Initializing propagandist package...")
initialized = true
}
func Hit() string {
return "Don't hit me, please!"
}
func IsInitialized() bool {
return initialized
}
Now, let's create another file in the propagandist package to demonstrate multiple init() functions:
touch ~/project/propagandist/second.go
Add the following content to the file:
package propagandist
import "fmt"
func init() {
fmt.Println("Second init function in propagandist package...")
}
Create a new helper package to demonstrate initialization order:
mkdir -p ~/project/helper
touch ~/project/helper/helper.go
Add the following content to the file:
package helper
import "fmt"
var Message = "Helper package is ready"
func init() {
fmt.Println("Initializing helper package...")
}
func GetMessage() string {
return Message
}
Add the module file for the helper package:
cd ~/project/helper
go mod init helper
Update your pacExercise.go to use both packages and demonstrate the initialization order:
package main
import (
"fmt"
"helper"
"propagandist"
)
func init() {
fmt.Println("Initializing main package...")
}
func main() {
fmt.Println("Main function is running")
fmt.Println(propagandist.Shout)
fmt.Println(helper.Message)
fmt.Printf("Propagandist initialized: %v\n", propagandist.IsInitialized())
}
Update the go.mod file in the main project to include the local helper package:
cd ~/project
echo "replace helper => ./helper" >> go.mod
go mod tidy
Run the program and observe the initialization order:
go run pacExercise.go
Expected output with the supplied filenames and the Go tool:
Initializing helper package...
Initializing propagandist package...
Second init function in propagandist package...
Initializing main package...
Main function is running
I Love LabEx
Helper package is ready
Propagandist initialized: true
This output demonstrates the key aspects of Go's initialization process:
Dependent packages are initialized before the packages that import them
Within a single package, all init() functions run in declaration order as the source files are presented to the compiler
The main() function runs only after all package initializations are complete
Package-level variables are initialized before any init() functions run
This initialization sequence is an important consideration when designing Go packages, especially when managing dependencies or performing setup operations that must happen in a specific order.
Summary
In this lab, you learned:
- How to create and define custom packages in Go, encapsulating reusable code.
- The difference between public (exported) and private (unexported) identifiers and how they impact accessibility.
- Various ways to import packages, each with its use case:
- Single-item import: Importing one package at a time.
- Grouped import: Importing multiple packages in a single block for better organization.
- Dot import: Importing a package and using its identifiers directly without the package name prefix. (Use with caution)
- Alias import: Renaming imported packages for better readability or to avoid naming conflicts.
- Anonymous import: Importing a package solely for its side effects, such as initialization.
- The role of the
init()function in packages and how anonymous imports can trigger its execution. - The detailed workings of Go's initialization process, including:
- How package-level variables are initialized before
init()functions - The guaranteed execution order of
init()functions across dependent packages - How multiple
init()functions work within a package - The complete initialization flow from dependent packages to the main function
- How package-level variables are initialized before
By completing this lab, you are now equipped to structure and manage Go projects using packages effectively. You can create reusable modules, control access to identifiers, better organize your code, and understand the initialization process, leading to more maintainable and scalable Go applications.



