Creating and Importing Go Packages

GolangBeginner
Practice Now

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 main package containing a main function. Reusable packages do not need that entry point. A module can contain several command directories, each with its own main package.

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!"
}
  • Shout is public because it starts with an uppercase letter. This means you can access it from other packages that import propagandist.
  • secret is private because it starts with a lowercase letter. It can only be used within the propagandist package.
  • Hit is 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 fmt package for printing output and the propagandist package.
  • It then accesses the public variable Shout from the propagandist package using propagandist.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 the fmt package directly without the fmt. prefix.
  • For example, you use Println instead of fmt.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:

  1. How to create and define custom packages in Go, encapsulating reusable code.
  2. The difference between public (exported) and private (unexported) identifiers and how they impact accessibility.
  3. 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.
  4. The role of the init() function in packages and how anonymous imports can trigger its execution.
  5. 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

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.