When we create a package in Go, we want to make it accessible for other programmers to use, either in other packages or in the entire Go project. By importing a package via the import packagename syntax, we can use the code contained within the imported package as a component for a more complex program.
However, only certain packages and only certain code parts (such as variables, functions, or types) within some packages are available for importing. It is determined by both the visibility of the package and the scope where these code parts are declared.
Public and private scope
Go determines the scope of a data type through the way it is declared. In simple terms, this means that any variable, constant, type or function will be in a public (exported) or private (unexported) scope, depending on whether the first letter of the variable is capitalized or not.
We can illustrate this with a simple example. Look at it and pay careful attention to capitalization:
package hello
var Welcome string = "I'm a Public variable! 🔓" // this is a public (exported) variable
var greeting string = "I'm a private variable. 🔒" // this is a private (unexported) variableAfter importing the hello package within the main package of our Go program, we'll be able to access the Welcome variable via the hello.Welcome syntax using the . operator. However, we won't be able to access the greeting variable, and if we tried to do so, we would get the compiler error: cannot refer to unexported name hello.greeting.
Let's look at another example with public and private functions. If we capitalize the first letter of a function within a package, other packages within our project will be able to access it. In turn, if we don't capitalize it, the function will be private and inaccessible:
package hello
...
func PublicFunc() string { // public function declarations begin with a capital letter
return "I am a Public function! 📢🔓🙋♂️"
}
func privateFunc() string { // private function declarations begin with a lowercase letter
return "I am a private function. 🤫🔒🚧"
}To access the public function PublicFunc() in the future, we can import the hello package to any other package in our project, and simply call the function via the hello.PublicFunc() syntax.
In the above example, we are assuming that the hello package is not an internal package within our Go project structure. If it was an internal package, even though the first letter of the function PublicFunc() is capitalized, we wouldn't be able to import it to another package due to internal package import restrictions.
Visibility of structs and struct fields
Suppose we declare a new struct type in a custom package (not inside the main package!). If the name declaration begins with a capital letter it will become a public (exported) type, letting us import it and access it from other packages within our Go project. On the other hand, if the name of the struct begins with a lowercase letter, we won't be able to import the struct within other packages in our project.
Looking at an example will help you understand it better. Let's suppose we have the following structure in our example project directory:
example
├── creatures
│ └── creatures.go
├── go.mod
└── main.goWithin the creatures package, we have the creatures.go file that has the Animal and human struct declarations:
package creatures
type Animal struct {
Name, Class, Emoji string
avgLifespan int // example of a private struct field: begins with a lowercase letter
Domestic bool
}
type human struct {
name string
age int
}The same rules regarding capital letters apply to struct fields. For example, the Animal struct will have all of its fields accessible except for the avgLifespan field because it doesn't begin with a capital letter.
Since the Animal struct begins with a capital letter; it is a public type that we can export and use within the main package in the main.go file, and also within other packages in our project. However, we can't use the private human struct from the creatures package. If we try to create an instance of the creatures.human struct, we will get the undefined: creatures.human compiler error, as in the following example:
package main
import "example/creatures"
func main() {
// create an instance of the `Animal` struct imported from the `creatures` package
// this works because the `Animal` struct is public
var crocodile creatures.Animal
// trying to create an instance of the `human` struct
// this will fail because the `human` struct is private
var jerry creatures.human
}
// Output:
// ./main.go:15:22: undefined: creatures.humanGlobal variables
We define global variables outside the scope of a function or a code block. Usually, we would define them below the package and import statements, and we can access them from any part of the program.
Let's take a look at an implementation of a global variable in a Go program:
package main
import "fmt"
var world = "A global world! 🌎"
func myFunc() string {
world = "A world within myFunc! 🌐"
return world // we can use the global variable 'world' anywhere in our program
}
func main() {
fmt.Println(world) // A global world! 🌎
world = "A local world! 🗺"
fmt.Println(world) // A local world! 🗺
fmt.Println(myFunc()) // A world within myFunc! 🌐
}After declaring and initializing the global variable world, we use it for the first time within the myFunc() function. Take notice that the contents of the world variable will only be accessed and updated when we invoke myFunc() within the main function of our program!
Next, we go into the main function of our program to print world for the first time, and the output is the initial content we declared the variable with. After that, we shadow the variable and print it once again to see the new output. Finally, we invoke the myFunc() function, updating and printing the content of world for the last time.
As you can see, this type of variable manipulation has only been possible because we declared world as a global variable, which has given us the flexibility to access it anywhere in our Go program.
Local variables
Variables that we declare inside a function or code block are known as local variables. We can't access these variables outside the function or code block where they are declared.
Let's implement two different local variables in our program:
...
func main() {
var number int = 4 // creating local variable 'number' within the main function
fmt.Printf("%d\n", number)
if number % 2 == 0 {
ifResult := "Even" // creating local variable 'ifResult' within the if statement block
fmt.Printf("%d is %s\n", number, ifResult)
} else {
fmt.Printf("%d is %s\n", number, "Odd")
}
fmt.Println(ifResult) // this causes an error! we can't access 'ifResult' here
// because it's declared within the 'if' statement code block
}After examining the above code snippet we can see that the program won't compile. This happens because we are trying to print the ifResult variable outside of the scope of the if statement where it was declared.
Now, let's fix our code by deleting the fmt.Println(ifResult) statement at the end of our program, and examine the output:
4
4 is EvenSince we can only access local variables within the scope where they are declared, the only place we'll be able to access the variable ifResult is within the if statement. Alternatively, the number variable can be accessed anywhere within the func main() declaration, even within the if or the else statements.
Shadowed variables
If we are not careful when writing our code, we might end up creating two or more variables with the same name and type in our Go program. This will cause us to run into a small issue — shadowed variables.
To understand the shadowed variable concept, let's take a look at the following code snippet:
package main
import "fmt"
func main() {
number := 0
if true {
number := 10
number++
}
fmt.Println(number) // 0
}You might be wondering: why is the output of the program 0 instead of 11? This happens because the number := 10 statement declares a new variable number, which shadows the original number := 0 declaration throughout the scope of the if statement.
To fix the code and get the wanted output of 11 we can simply change the line number := 10 to number = 10. With this small change, we wouldn't create a new variable number within the if statement, and instead we'd update the contents of the local variable number created within the main function.
To help detect shadowed variables we can use the go vet tool. We can install and use it by executing the following commands in our project directory:
$ go get golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
$ go vet -vettool=$(which shadow)After executing the go vet -vettool=$(which shadow) command, we should get the following output:
./main.go:8:3: declaration of "number" shadows declaration at line 6As you can see, this output helps us easily detect the shadowed variable number in our main.go file at line #8 – just at the beginning of the if statement.
Conclusion
In this topic, we've learned about the public and private scope in Go, and also about global and local variables. We've covered in detail the following theory:
What the public and private scopes of variables are in Go and their key differences.
The differences between public and private
structtypes andstructfields.How to create a global variable and its main characteristics, such as that we can use it anywhere within our Go program.
How to create a local variable, and that its scope is limited depending on where it was declared in the Go program.
What shadowed variables are and how we can find them with the help of the
go vettool.
Now, to make sure you remember this theory and can implement it in practice, let's work on a few coding and theory problems.