One of the first things I learned about in Go was using an uppercase or lowercase letter as the first letter when naming a type, variable or function. It was explained that when the first letter was capitalized, the identifier was public to any piece of code that wanted to use it. When the first letter was lowercase, the identifier was private and could only be accessed within the package it was declared.
NEXT Level Go & DevOps Training
I have come to realize that the use of the language public and private is really not accurate. It is more accurate to say an identifier is exported or unexported from a package. When an identifier is exported from a package, it means the identifier can be directly accessed from any other package in the code base. When an identifier is unexported from a package, it can’t be directly accessed from any other package. What we will soon learn is that just because an identifier is unexported, it doesn’t mean it can’t be accessed outside of its package, it just means it can’t be accessed directly.
Direct Identifier Access
Let’s start with a simple example of an exported type:
Here we define a named type called AlertCounter inside the package counters. This type is an alias for the built-in type int, but in Go AlertCounter will be considered a unique and distinct type. We are using the capital letter ‘A’ as the first letter for the name of the type, which means this type is exported and accessible by other packages.
Now let’s access our AlertCounter type in the main program:
Since the AlertCounter type has been exported, this code builds fine. When we run the program we get the value of 10.
Now let’s change the exported AlertCounter type to be an unexported type by changing the name to alertCounter and see what happens:
After making the changes to the counters and main packages, we attempt to build the code again and get the following compiler error:
./main.go:11: undefined: counters.alertCounter
As expected we can’t directly access the alertCounter type because it is unexported. Even though we can’t access the alertCounter type directly anymore, there is a way for us to create and use variables of this unexported type in the main package:
In the counters package we add an exported function called NewAlertCounter. This function creates and returns values of the alertCounter type. In the main program we use this function and the programming logic stays the same.
What this example shows is that an identifier that is declared as unexported can still be accessed and used by other packages. It just can’t be accessed directly.
Defining exported and unexported members for our structs work in the exact same way. If a field or method name starts with a capital letter, the member is exported and is accessible outside of the package. If a field or method starts with a lowercase letter, the member is unexported and does not have accessibility outside of the package.
Here is an example of a struct with both exported and unexported fields. The main program has a compiler error because it attempts to access the unexported field directly:
Here is the error from the compiler:
As expected the compiler does not let the main program access the age field directly.
Let’s look at an interesting example of embedding. We start with two struct types where one type embeds the other:
We added a new exported type called Animal with two exported fields called Name and Age. Then we embed the Animal type into the exported Dog type. This means that the Dog type now has three exported fields, Name, Age and BarkStrength.
Let’s look at the implementation of the main program:
In main we use a composite literal to create and initialize a value of the exported Dog type. Then we display the structure and values of the dog value.
To make things more interesting, let’s change the Animal type from exported to unexported by changing the first letter of the type’s name to a lowercase letter ‘a’:
The animal type remains embedded in the exported Dog type, but now as an unexported type. We keep the Name and Age fields within the animal type as exported fields.
In the main program we just change the name of the type from Animal to animal:
Once again we have a main program that can’t compile because we are trying to access the unexported type animal from inside the composite literal:
./main.go:14: unknown animals.Dog field ‘animal’ in struct literal
We can fix the compiler error by initializing the exported fields from the unexported embedded type outside of the composite literal:
Now the main program builds again. The exported fields that were embedded into the Dog type from the animal type are accessible, even though they came from an unexported type. The exported fields keep their exported status when the type is embedded.
The exported Time type from the time package is a good example of a type from the standard library that provides no access to its internals:
The language designers are using the unexported fields to keep the internals of the Time type private. They are "hiding" the information so we can’t do anything contrary to how the time data works. With that being said, we still can use the unexported fields through the interface they provide. Without the ability to use and access unexported fields indirectly, we would not be able to copy values of this type or embed this type into our own struct types.
A solid understanding of how to hide and provide access to data from our packages is important. There is a lot more to exporting and unexporting identifiers than meets the eye. In setting out to write this post, I though a couple of examples would do the trick. Then I realized how involved the topic can get once we start looking at embedding unexported types into our own types.
The ability to use exported or unexported identifiers is an implementation detail, one that Go give us flexibility to use in our programs. The standard library has great examples of using unexported identifiers to hide and protect data. We looked at one example with the time.Time type. Take the time to look at the standard library to learn more.