Creating and Importing Go Packages

GolangGolangBeginner
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

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("Golang")) -.-> go/BasicsGroup(["Basics"]) go(("Golang")) -.-> go/FunctionsandControlFlowGroup(["Functions and Control Flow"]) go(("Golang")) -.-> go/AdvancedTopicsGroup(["Advanced Topics"]) go/BasicsGroup -.-> go/variables("Variables") go/FunctionsandControlFlowGroup -.-> go/functions("Functions") go/AdvancedTopicsGroup -.-> go/time("Time") subgraph Lab Skills go/variables -.-> lab-149064{{"Creating and Importing Go Packages"}} go/functions -.-> lab-149064{{"Creating and Importing Go Packages"}} go/time -.-> lab-149064{{"Creating and Importing Go Packages"}} end

Declaring and Defining Packages

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 program must have one and only one package named main, which serves as the entry point for execution. Without it, the program cannot generate an executable file.

Key Points:

  1. 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.
  2. 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.
  3. Package Cohesion: All files in the same folder must belong to the same package. This ensures that related code stays together.
  4. Package Naming Conventions: Package names should be lowercase, short, and descriptive, avoiding underscores or capital letters.

Let's create our own custom package:

  1. Create a folder named propagandist and a file propagandist.go inside it:

    mkdir ~/project/propagandist
    touch ~/project/propagandist/propagandist.go
  2. 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.
  3. 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

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.

  1. Create a new Go file named pacExercise.go in the project folder:

    touch ~/project/pacExercise.go
  2. Initialize a Go module for the program:

    cd ~/project
    go mod init pacExercise
  3. 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.

  4. Write the following code in pacExercise.go to import and use the propagandist package:

    package main
    
    import (
        "fmt"
        "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.
  5. 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

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.

  1. Modify pacExercise.go to use grouped imports:

    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. This is exactly the same as the previous example and shows how to use the grouped import syntax.

  2. Run the program to confirm it still works:

    go run pacExercise.go

    The program should execute without errors and output the same result as before.

Dot Import

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.

  1. Modify pacExercise.go to use a dot import for fmt:

    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.
  1. Run the program:

    go run pacExercise.go

    Expected Output:

    I Love LabEx

Alias Import

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.

  1. Modify pacExercise.go to alias fmt as io:

    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.
  2. Run the program:

    go run pacExercise.go

Anonymous Import

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.

  1. Modify pacExercise.go to include an anonymous import for time:

    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.
  2. Run the program:

    go run pacExercise.go

    Expected Output:

    I Love LabEx

Understanding the init() Function

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:

  1. 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.
  2. 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:
      1. Package level variables are initialized first
      2. Then init() functions are executed
      3. Finally, the main() function runs (only in the main package)
  3. 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 will be executed, but the order within the same package is not guaranteed
  4. 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:

  1. 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
    }
  2. 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...")
    }
  3. 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
    }
  4. Add the module file for the helper package:

    cd ~/project/helper
    go mod init helper
  5. 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())
    }
  6. 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
  7. Run the program and observe the initialization order:

    go run pacExercise.go

    Expected Output (the exact order of the two propagandist init functions might vary):

    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:

  1. Dependent packages are initialized before the packages that import them
  2. Within a single package, all init() functions will run (though their order is not guaranteed)
  3. The main() function runs only after all package initializations are complete
  4. 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.