Go is a statically typed programming language. What that means is the compiler always wants to know what the type is for every value in the program. When the compiler knows the type information ahead of time, it can help to make sure that the program is working with values in a safe way. This helps to reduce potential memory corruption and bugs, and provides the compiler the opportunity to produce more performant code.
A value’s type provides the compiler with two pieces of information: first, how much memory to allocate—the size of the value—and second, what that memory represents. In the case of many of the built-in types, size and representation are part of the type’s name. A value of type int64 requires 8 bytes of memory (64 bits) and represents an integer value. A float32 requires 4 bytes of memory (32 bits) and represents an IEEE-754 binary floating-point number. A bool requires 1 byte of memory (8 bits) and represents a Boolean value of true or false.
Some types get their representation based on the architecture of the machine the code is built for. A value of type int, for example, can either have a size of 8 bytes (64 bits) or 4 bytes (32 bits), depending on the architecture. There are other architecture-specific types as well, such as all the reference types in Go. Luckily, you don’t need to know this information to create or work with values. But if the compiler doesn’t know this information, it can’t protect you from doing things that could cause harm inside your programs and the machines they run on.
Go allows you the ability to declare your own types. When you declare a new type, the declaration is constructed to provide the compiler with size and representation information, similar to how the built-in types work. There are two ways to declare a user-defined type in Go. The most common way is to use the keyword struct, which allows you to create a composite type.
Struct types are declared by composing a fixed set of unique fields. Each field in a struct is declared with a known type, which could be a built-in type or another user-defined type.
01 // user defines a user in the program. 02 type user struct { 03 name string 04 email string 05 ext int 06 privileged bool 07 }
In listing 5.1 you see the declaration of a struct type. The declaration starts with the keyword type, then a name for the new type, and finally the keyword struct. This struct type contains four fields, each based on a different built-in type. You can see how the fields come together to compose a structure of data. Once you have a type declared, you can create values from the type.
09 // Declare a variable of type user. 10 var bill user
On line 10 in listing 5.2, the keyword var creates a variable named bill of type user. When you declare variables, the value that the variable represents is always initialized. The value can be initialized with a specific value or it can be initialized to its zero value, which is the default value for that variable’s type. For numeric types, the zero value would be 0; for strings it would be empty; and for Booleans it would be false. In the case of a struct, the zero value would apply to all the different fields in the struct.
Any time a variable is created and initialized to its zero value, it’s idiomatic to use the keyword var. Reserve the use of the keyword var as a way to indicate that a variable is being set to its zero value. If the variable will be initialized to something other than its zero value, then use the short variable declaration operator with a struct literal.
12 // Declare a variable of type user and initialize all the fields. 13 lisa := user{ 14 name: "Lisa", 15 email: "lisa@email.com", 16 ext: 123, 17 privileged: true, 18 }
Listing 5.3 shows how to declare a variable of type user and initialize the value to something other than its zero value. On line 13, we provide a variable name followed by the short variable declaration operator. This operator is the colon with the equals sign (:=). The short variable declaration operator serves two purposes in one operation: it both declares and initializes a variable. Based on the type information on the right side of the operator, the short variable declaration operator can determine the type for the variable.
Since we’re creating and initializing a struct type, we use a struct literal to perform the initialization. The struct literal takes the form of curly brackets with the initialization declared within them.
13 user{ 14 name: "Lisa", 15 email: "lisa@email.com", 16 ext: 123, 17 privileged: true, 18 }
The struct literal can take on two forms for a struct type. Listing 5.4 shows the first form, which is to declare each field and value from the struct to be initialized on a separate line. A colon is used to separate the two, and it requires a trailing comma. The order of the fields doesn’t matter. The second form is without the field names and just declares the values.
12 // Declare a variable of type user. 13 lisa := user{"Lisa", "lisa@email.com", 123, true}
The values can also be placed on separate lines, but traditionally values are placed on the same line with no trailing comma when using this form. The order of the values does matter in this case and needs to match the order of the fields in the struct declaration. When declaring a struct type, you’re not limited to just the built-in types. You can also declare fields using other user-defined types.
20 // admin represents an admin user with privileges. 21 type admin struct { 22 person user 23 level string 24 }
Listing 5.6 shows a new struct type named admin. This struct type has a field named person of type user, and then declares a second field named level of type string. When creating a variable of a struct type that has a field like person, initializing the type with a struct literal changes a little.
26 // Declare a variable of type admin. 27 fred := admin{ 28 person: user{ 29 name: "Lisa", 30 email: "lisa@email.com", 31 ext: 123, 32 privileged: true, 33 }, 34 level: "super", 35 }
In order to initialize the person field, we need to create a value of type user. This is exactly what we do on line 28 in listing 5.7. Using the struct literal form, a value of type user is created and assigned to the person field.
A second way to declare a user-defined type is by taking an existing type and using it as the type specification for the new type. These types are great when you need a new type that can be represented by an existing type. The standard library uses this type declaration to create high-level functionality from the built-in types.
type Duration int64
Listing 5.8 shows the declaration of a type from the time package of the standard library. Duration is a type that represents the duration of time down to the nano-second. The type takes its representation from the built-in type int64. In the declaration of Duration, we say that int64 is the base type of Duration. Even though int64 is acting at the base type, it doesn’t mean Go considered them to be the same. Duration and int64 are two distinct and different types.
To better clarify what this means, look at this small program that doesn’t compile.
01 package main 02 03 type Duration int64 04 05 func main() { 06 var dur Duration 07 dur = int64(1000) 08 }
The program in listing 5.9 declares a type on line 03 called Duration. Then on line 06, a variable named dur of type Duration is declared and set to its zero value. Then on line 07, we write code that produces the following compiler error when the program is built.
prog.go:7: cannot use int64(1000) (type int64) as type Duration in assignment
The compiler is clear as to what the problem is. Values of type int64 can’t be used as values of type Duration. In other words, even though type int64 is the base type for Duration, Duration is still its own unique type. Values of two different types can’t be assigned to each other, even if they’re compatible. The compiler doesn’t implicitly convert values of different types.
Methods provide a way to add behavior to user-defined types. Methods are really functions that contain an extra parameter that’s declared between the keyword func and the function name.
01 // Sample program to show how to declare methods and how the Go 02 // compiler supports them. 03 package main 04 05 import ( 06 "fmt" 07 ) 08 09 // user defines a user in the program. 10 type user struct { 11 name string 12 email string 13 } 14 15 // notify implements a method with a value receiver. 16 func (u user) notify() { 17 fmt.Printf("Sending User Email To %s<%s>\n", 18 u.name, 19 u.email) 20 } 21 22 // changeEmail implements a method with a pointer receiver. 23 func (u *user) changeEmail(email string) { 24 u.email = email 25 } 26 27 // main is the entry point for the application. 28 func main() { 29 // Values of type user can be used to call methods 30 // declared with a value receiver. 31 bill := user{"Bill", "bill@email.com"} 32 bill.notify() 33 34 // Pointers of type user can also be used to call methods 35 // declared with a value receiver. 36 lisa := &user{"Lisa", "lisa@email.com"} 37 lisa.notify() 38 39 // Values of type user can be used to call methods 40 // declared with a pointer receiver. 41 bill.changeEmail("bill@newdomain.com") 42 bill.notify() 43 44 // Pointers of type user can be used to call methods 45 // declared with a pointer receiver. 46 lisa.changeEmail("lisa@comcast.com") 47 lisa.notify() 48 }
Lines 16 and 23 of listing 5.11 show two different methods. The parameter between the keyword func and the function name is called a receiver and binds the function to the specified type. When a function has a receiver, that function is called a method. When you run the program, you get the following output.
Sending User Email To Bill<bill@email.com> Sending User Email To Lisa<lisa@email.com> Sending User Email To Bill<bill@newdomain.com> Sending User Email To Lisa<lisa@comcast.com>
Let’s examine what the program is doing. On line 10, the program declares a struct type named user and then declares a method named notify.
09 // user defines a user in the program. 10 type user struct { 11 name string 12 email string 13 } 14 15 // notify implements a method with a value receiver. 16 func (u user) notify() { 17 fmt.Printf("Sending User Email To %s<%s>\n", 18 u.name, 19 u.email) 20 }
There are two types of receivers in Go: value receivers and pointer receivers. In listing 5.13 on line 16, the notify method is declared with a value receiver.
func (u user) notify() {
The receiver for notify is declared as a value of type user. When you declare a method using a value receiver, the method will always be operating against a copy of the value used to make the method call. Let’s skip to line 32 of the program in listing 5.11 to see a method call on notify.
29 // Values of type user can be used to call methods 30 // declared with a value receiver. 31 bill := user{"Bill", "bill@email.com"} 32 bill.notify()
Listing 5.15 shows a call to the notify method using a value of type user. On line 31, a variable named bill of type user is declared and initialized with a name and email address. Then on line 32, the notify method is called using the variable bill.
bill.notify()
The syntax looks similar to when you call a function from a package. In this case however, bill is not a package name but a variable name. When we call the notify method in this case, the value of bill is the receiver value for the call and the notify method is operating on a copy of this value.
You can also call methods that are declared with a value receiver using a pointer.
34 // Pointers of type user can also be used to call methods 35 // declared with a value receiver. 36 lisa := &user{"Lisa", "lisa@email.com"} 37 lisa.notify()
Listing 5.17 shows a call to the notify method using a pointer of type user. On line 36, a variable named lisa of pointer type user is declared and initialized with a name and email address. Then on line 37, the notify method is called using the pointer variable. To support the method call, Go adjusts the pointer value to comply with the method’s receiver. You can imagine that Go is performing the following operation.
(*lisa).notify()
Listing 5.18 shows essentially what the Go compiler is doing to support the method call. The pointer value is dereferenced so the method call is in compliance with the value receiver. Once again, notify is operating against a copy, but this time a copy of the value that the lisa pointer points to.
You can also declare methods with pointer receivers.
22 // changeEmail implements a method with a pointer receiver. 23 func (u *user) changeEmail(email string) { 24 u.email = email 25 }
Listing 5.19 shows the declaration of the changeEmail method, which is declared with a pointer receiver. This time, the receiver is not a value of type user but a pointer of type user. When you call a method declared with a pointer receiver, the value used to make the call is shared with the method.
36 lisa := &user{"Lisa", "lisa@email.com"} 44 // Pointers of type user can be used to call methods 45 // declared with a pointer receiver. 46 lisa.changeEmail("lisa@newdomain.com")
In listing 5.20 you see the declaration of the lisa pointer variable followed by the method call to changeEmail on line 46. Once the call to changeEmail returns, any changes to the value that the lisa pointer points to will be reflected after the call. This is thanks to the pointer receiver. Value receivers operate on a copy of the value used to make the method call and pointer receivers operate on the actual value.
You can also call methods that are declared with a pointer receiver using a value.
31 bill := user{"Bill", "bill@email.com"} 39 // Values of type user can be used to call methods 40 // declared with a pointer receiver. 41 bill.changeEmail("bill@newdomain.com")
In listing 5.21, you see the declaration of the variable bill and then a call to the changeEmail method, which is declared with a pointer receiver. Once again, Go adjusts the value to comply with the method’s receiver to support the call.
(&bill).notify()
Listing 5.22 shows essentially what the Go compiler is doing to support the method call. In this case, the value is referenced so the method call is in compliance with the receiver type. This is a great convenience Go provides, allowing method calls with values and pointers that don’t match a method’s receiver type natively.
Determining whether to use a value or pointer receiver can sometimes be confusing. There are some basic guidelines you can follow that come directly from the standard library.
After declaring a new type, try to answer this question before declaring methods for the type. Does adding or removing something from a value of this type need to create a new value or mutate the existing one? If the answer is create a new value, then use value receivers for your methods. If the answer is mutate the value, then use pointer receivers. This also applies to how values of this type should be passed to other parts of your program. It’s important to be consistent. The idea is to not focus on what the method is doing with the value, but to focus on what the nature of the value is.
Built-in types are the set of types that are provided by the language. We know them as the set of numeric, string, and Boolean types. These types have a primitive nature to them. Because of this, when adding or removing something from a value of one of these types, a new value should be created. Based on this, when passing values of these types to functions and methods, a copy of the value should be passed. Let’s look at a function from the standard library that works with built-in values.
620 func Trim(s string, cutset string) string { 621 if s == "" || cutset == "" { 622 return s 623 } 624 return TrimFunc(s, makeCutsetFunc(cutset)) 625 }
In listing 5.23, you see the Trim function, which comes from the strings package in the standard library. The Trim function is passed a string value to operate on and a string value with characters to find. It then returns a new string value that’s the result of the operation. The function operates on copies of the caller’s original string values and returns a copy of the new string value. Strings, just like integers, floats, and Booleans, are primitive data values and should be copied when passed in and out of functions or methods.
Let’s look at a second example of how the built-in types are treated as having a primitive nature.
38 func isShellSpecialVar(c uint8) bool { 39 switch c { 40 case '*', '#', '$', '@', '!', '?', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 41 return true 42 } 43 return false 44 }
Listing 5.24 shows the isShellSpecialVar function from the env package. This function is passed a value of type uint8 and returns a value of type bool. Note how pointers aren’t being used to share the value for the parameter or return value. The caller passes a copy of their uint8 value and receives a value of true or false.
Reference types in Go are the set of slice, map, channel, interface, and function types. When you declare a variable from one of these types, the value that’s created is called a header value. Technically, a string is also a reference type value. All the different header values from the different reference types contain a pointer to an underlying data structure. Each reference type also contains a set of unique fields that are used to manage the underlying data structure. You never share reference type values because the header value is designed to be copied. The header value contains a pointer; therefore, you can pass a copy of any reference type value and share the underlying data structure intrinsically.
Let’s look at a type from the net package.
32 type IP []byte
Listing 5.25 shows a type called IP which is declared as a slice of bytes. Declaring a type like this is useful when you want to declare behavior around a built-in or reference type. The compiler will only let you declare methods for user-defined types that are named.
329 func (ip IP) MarshalText() ([]byte, error) { 330 if len(ip) == 0 { 331 return []byte(""), nil 332 } 333 if len(ip) != IPv4len && len(ip) != IPv6len { 334 return nil, errors.New("invalid IP address") 335 } 336 return []byte(ip.String()), nil 337 }
The MarshalText method in listing 5.26 has been declared using a value receiver of type IP. A value receiver is exactly what you expect to see since you don’t share reference type values. This also applies to passing reference type values as parameters to functions and methods.
318 // ipEmptyString is like ip.String except that it returns 319 // an empty string when ip is unset. 320 func ipEmptyString(ip IP) string { 321 if len(ip) == 0 { 322 return "" 323 } 324 return ip.String() 325 }
In listing 5.27 you see the ipEmptyString function. This function is passed a value of the type IP. Once again, you can see how the caller’s reference type value for this parameter is not shared with the function. The function is passed a copy of the caller’s reference type value. This also applies to return values. In the end, reference type values are treated like primitive data values.
Struct types can represent data values that could have either a primitive or nonprimitive nature. When the decision is made that a struct type value should not be mutated when something needs to be added or removed from the value, then it should follow the guidelines for the built-in and reference types. Let’s start with looking at a struct implemented by the standard library that has a primitive nature.
39 type Time struct { 40 // sec gives the number of seconds elapsed since 41 // January 1, year 1 00:00:00 UTC. 42 sec int64 43 44 // nsec specifies a non-negative nanosecond 45 // offset within the second named by Seconds. 46 // It must be in the range [0, 999999999]. 47 nsec int32 48 49 // loc specifies the Location that should be used to 50 // determine the minute, hour, month, day, and year 51 // that correspond to this Time. 52 // Only the zero Time has a nil Location. 53 // In that case it is interpreted to mean UTC. 54 loc *Location 55 }
The Time struct in listing 5.28 comes from the time package. When you think about time, you realize that any given point in time is not something that can change. This is exactly how the standard library implements the Time type. Let’s look at the Now function that creates values of type Time.
781 func Now() Time { 782 sec, nsec := now() 783 return Time{sec + unixToInternal, nsec, Local} 784 }
The code in listing 5.29 shows the implementation of the Now function. This function creates a value of type Time and returns a copy of that Time value back to the caller. A pointer is not used to share the Time value created by the function. Next, let’s look at a method declared against the Time type.
610 func (t Time) Add(d Duration) Time { 611 t.sec += int64(d / 1e9) 612 nsec := int32(t.nsec) + int32(d%1e9) 613 if nsec >= 1e9 { 614 t.sec++ 615 nsec -= 1e9 616 } else if nsec < 0 { 617 t.sec-- 618 nsec += 1e9 619 } 620 t.nsec = nsec 621 return t 622 }
The method Add in listing 5.30 is a great example of how the standard library treats the Time type as having a primitive nature. The method is declared using a value receiver and returns a new Time value. The method is operating on its own copy of the caller’s Time value and returns a copy of its local Time value back to the caller. It’s up to the caller whether they want to replace their Time value with what’s returned or declare a new Time variable to hold the result.
In most cases, struct types don’t exhibit a primitive nature, but a nonprimitive one. In these cases, adding or removing something from the value of the type should mutate the value. When this is the case, you want to use a pointer to share the value with the rest of the program that needs it. Let’s take a look at a struct type implemented by the standard library that has a nonprimitive nature.
15 // File represents an open file descriptor. 16 type File struct { 17 *file 18 } 19 20 // file is the real representation of *File. 21 // The extra level of indirection ensures that no clients of os 22 // can overwrite this data, which could cause the finalizer 23 // to close the wrong file descriptor. 24 type file struct { 25 fd int 26 name string 27 dirinfo *dirInfo // nil unless directory being read 28 nepipe int32 // number of consecutive EPIPE in Write 29 }
In listing 5.31 you see the declaration of the File type from the standard library. The nature of this type is nonprimitive. Values of this type are actually unsafe to be copied. The comments for the unexported type make this clear. Since there’s no way to prevent programmers from making copies, the implementation of the File type uses an embedded pointer of an unexported type. We’ll talk about embedding types later in this chapter, but this extra level of indirection provides protection from copies. Not every struct type requires or should be implemented with this extra protection. Programmers should respect the nature of each type and use it accordingly.
Let’s look at the implementation of the Open function.
238 func Open(name string) (file *File, err error) { 239 return OpenFile(name, O_RDONLY, 0) 240 }
The implementation of the Open function in listing 5.32 shows how a pointer is used to share File type values with the caller of the function. Open creates a value of type File and returns a pointer to that value. When a factory function returns a pointer, it’s a good indication that the nature of the value being returned is nonprimitive.
Even if a function or method is never going to directly change the state of a nonprimitive value, it should still be shared.
224 func (f *File) Chdir() error { 225 if f == nil { 226 return ErrInvalid 227 } 228 if e := syscall.Fchdir(f.fd); e != nil { 229 return &PathError{"chdir", f.name, e} 230 } 231 return nil 232 }
The Chdir method in listing 5.33 shows how a pointer receiver is declared even though no changes are made to the receiver value. Since values of type File have a nonprimitive nature, they’re always shared and never copied.
The decision to use a value or pointer receiver should not be based on whether the method is mutating the receiving value. The decision should be based on the nature of the type. One exception to this guideline is when you need the flexibility that value type receivers provide when working with interface values. In these cases, you may choose to use a value receiver even though the nature of the type is nonprimitive. It’s entirely based on the mechanics behind how interface values call methods for the values stored inside of them. In the next section, you’ll learn about what interface values are and the mechanics behind using them to call methods.
Polymorphism is the ability to write code that can take on different behavior through the implementation of types. Once a type implements an interface, an entire world of functionality can be opened up to values of that type. The standard library is a great example of this. The io package provides an incredible set of interfaces and functions that make streaming data easy to apply to our code. Just by implementing two interfaces, we can take advantage of all the engineering behind the io package.
But a lot of details go into declaring and implementing interfaces for use in our own programs. Even the implementation of existing interfaces requires an understanding of how interfaces work. Before we get into the details of how interfaces work and how to implement them, let’s look at a quick example of the use of interfaces from the standard library.
Let’s start by looking at a sample program that implements a version of a popular program named curl.
01 // Sample program to show how to write a simple version of curl using 02 // the io.Reader and io.Writer interface support. 03 package main 04 05 import ( 06 "fmt" 07 "io" 08 "net/http" 09 "os" 10 ) 11 12 // init is called before main. 13 func init() { 14 if len(os.Args) != 2 { 15 fmt.Println("Usage: ./example2 <url>") 16 os.Exit(-1) 17 } 18 } 19 20 // main is the entry point for the application. 21 func main() { 22 // Get a response from the web server. 23 r, err := http.Get(os.Args[1]) 24 if err != nil { 25 fmt.Println(err) 26 return 27 } 28 29 // Copies from the Body to Stdout. 30 io.Copy(os.Stdout, r.Body) 31 if err := r.Body.Close(); err != nil { 32 fmt.Println(err) 33 } 34 }
Listing 5.34 shows the power of interfaces and their use in the standard library. In a few lines of code, we have a curl program by leveraging two functions that work with interface values. On line 23, we call the Get function from the http package. The http.Get function returns a pointer of type http.Request after it successfully communicates with the server. The http.Request type contains a field named Body, which is an interface value of type io.ReadCloser.
On line 30, the Body field is passed into the io.Copy function as the second parameter. The io.Copy function accepts values of interface type io.Reader for its second parameter, and this value represents a source of data to stream from. Luckily, the Body field implements the io.Reader interface, so we can pass the Body field into io.Copy and use a web server as our source.
The first parameter for io.Copy represents the destination and must be a value that implements the io.Writer interface. For our destination, we pass a special interface value from the os package called Stdout. This interface value represents the standard out device and already implements the io.Writer interface. When we pass the Body and Stdout values to the io.Copy function, the function streams data from the web server to the terminal window in small chunks. Once the last chunk is read and written, the io.Copy function returns.
The io.Copy function can perform this work flow for many different types that already exist in the standard library.
01 // Sample program to show how a bytes.Buffer can also be used 02 // with the io.Copy function. 03 package main 04 05 import ( 06 "bytes" 07 "fmt" 08 "io" 09 "os" 10 ) 11 12 // main is the entry point for the application. 13 func main() { 14 var b bytes.Buffer 15 16 // Write a string to the buffer. 17 b.Write([]byte("Hello")) 18 19 // Use Fprintf to concatenate a string to the Buffer. 20 fmt.Fprintf(&b, "World!") 21 22 // Write the content of the Buffer to stdout. 23 io.Copy(os.Stdout, &b) 24 }
Listing 5.35 shows a program that uses interfaces to concatenate and then stream data to standard out. On line 14, a variable of type Buffer from the bytes package is created, and then the Write method is used on line 17 to add the string Hello to the buffer. On line 20, the Fprintf function from the fmt package is called to append a second string to the buffer.
The fmt.Fprintf function accepts an interface value of type io.Writer as its first parameter. Since pointers of type bytes.Buffer implement the io.Writer interface, it can be passed in and the fmt.Fprintf function performs the concatenation. Finally, on line 23 the io.Copy function is used once again to write characters to the terminal window. Since pointers of type bytes.Buffer also implement the io.Reader interface, the io.Copy function can be used to display the contents of the buffer to the terminal window.
These two small examples hopefully show you some of the benefits of interfaces and how they’re used in the standard library. Next, let’s explore in greater detail how interfaces are implemented.
Interfaces are types that just declare behavior. This behavior is never implemented by the interface type directly but instead by user-defined types via methods. When a user-defined type implements the set of methods declared by an interface type, values of the user-defined type can be assigned to values of the interface type. This assignment stores the value of the user-defined type into the interface value.
If a method call is made against an interface value, the equivalent method for the stored user-defined value is executed. Since any user-defined type can implement any interface, method calls against an interface value are polymorphic in nature. The user-defined type in this relationship is often called a concrete type, since interface values have no concrete behavior without the implementation of the stored user-defined value.
There are rules around whether values or pointers of a user-defined type satisfy the implementation of an interface. Not all values are created equal. These rules come from the specification under the section called method sets. Before you begin to investigate the details of method sets, it helps to understand what interface type values look like and how user-defined type values are stored inside them.
In figure 5.1 you see what the value of the interface variable looks like after the assignment of the user type value. Interface values are two-word data structures. The first word contains a pointer to an internal table called an iTable, which contains type information about the stored value. The iTable contains the type of value that has been stored and a list of methods associated with the value. The second word is a pointer to the stored value. The combination of type information and pointer binds the relationship between the two values.
Figure 5.2 shows what happens when a pointer is assigned to an interface value. In this case, the type information will reflect that a pointer of the assigned type has been stored, and the address being assigned is stored in the second word of the interface value.
Method sets define the rules around interface compliance. Take a look at the following code to help you understand how method sets play an important role with interfaces.
01 // Sample program to show how to use an interface in Go. 02 package main 03 04 import ( 05 "fmt" 06 ) 07 08 // notifier is an interface that defined notification 09 // type behavior. 10 type notifier interface { 11 notify() 12 } 13 14 // user defines a user in the program. 15 type user struct { 16 name string 17 email string 18 } 19 20 // notify implements a method with a pointer receiver. 21 func (u *user) notify() { 22 fmt.Printf("Sending user email to %s<%s>\n", 23 u.name, 24 u.email) 25 } 26 27 // main is the entry point for the application. 28 func main() { 29 // Create a value of type User and send a notification. 30 u := user{"Bill", "bill@email.com"} 31 32 sendNotification(u) 33 34 // ./listing36.go:32: cannot use u (type user) as type 35 // notifier in argument to sendNotification: 36 // user does not implement notifier 37 // (notify method has pointer receiver) 38 } 39 40 // sendNotification accepts values that implement the notifier 41 // interface and sends notifications. 42 func sendNotification(n notifier) { 43 n.notify() 44 }
In listing 5.36 you see code that you would expect to compile, but it doesn’t. On line 10, we declare an interface named notifier with a single method named notify. Then on line 15, we have the declaration of our concrete type named user and the implementation of the notifier interface via the method declaration on line 21. The method is implemented with a pointer receiver of type user.
40 // sendNotification accepts values that implement the notifier 41 // interface and sends notifications. 42 func sendNotification(n notifier) { 43 n.notify() 44 }
On line 42 in listing 5.37, a function named sendNotification is declared and accepts a single value of the interface type notifier. Then the interface value is used to call the notify method against the stored value. Any value that implements the notifier interface can be passed into the sendNotification function. Now let’s look at the main function.
28 func main() { 29 // Create a value of type User and send a notification. 30 u := user{"Bill", "bill@email.com"} 31 32 sendNotification(u) 33 34 // ./listing36.go:32: cannot use u (type user) as type 35 // notifier in argument to sendNotification: 36 // user does not implement notifier 37 // (notify method has pointer receiver) 38 }
In the main function, a value of the concrete type user is created and assigned to the variable u on line 30 in listing 5.38. Then the value of u is passed to the send-Notification function on line 32. But the call to sendNotification results in a compiler error.
./listing36.go:32: cannot use u (type user) as type notifier in argument to sendNotification: user does not implement notifier (notify method has pointer receiver)
So why do we receive a compiler error when the user type implements the notify method on line 21? Let’s take a look at that code again.
08 // notifier is an interface that defined notification 09 // type behavior. 10 type notifier interface { 11 notify() 12 } 21 func (u *user) notify() { 22 fmt.Printf("Sending user email to %s<%s>\n", 23 u.name, 24 u.email) 25 }
Listing 5.40 shows how the interface has been implemented, yet the compiler tells us that a value of type user doesn’t implement the interface. If you look closer at the compiler message, it actually tells us why.
(notify method has pointer receiver)
To understand why values of type user don’t implement the interface when an interface is implemented with a pointer receiver, you need to understand what method sets are. Method sets define the set of methods that are associated with values or pointers of a given type. The type of receiver used will determine whether a method is associated with a value, pointer, or both.
Let’s start with explaining the rules for method sets as it’s documented by the Go specification.
Values Methods Receivers ----------------------------------------------- T (t T) *T (t T) and (t *T)
Listing 5.42 shows how the specification describes method sets. It says that a value of type T only has methods declared that have a value receiver, as part of its method set. But pointers of type T have methods declared with both value and pointer receivers, as part of its method set. Looking at these rules from the perspective of the value is confusing. Let’s look at these rules from the perspective of the receiver.
Methods Receivers Values ----------------------------------------------- (t T) T and *T (t *T) *T
Listing 5.43 shows the same rules, but from the perspective of the receiver. It says that if you implement an interface using a pointer receiver, then only pointers of that type implement the interface. If you implement an interface using a value receiver, then both values and pointers of that type implement the interface. If you look at the code in listing 5.36 again, you now have the context to understand the compiler error.
28 func main() { 29 // Create a value of type User and send a notification. 30 u := user{"Bill", "bill@email.com"} 31 32 sendNotification(u) 33 34 // ./listing36.go:32: cannot use u (type user) as type 35 // notifier in argument to sendNotification: 36 // user does not implement notifier 37 // (notify method has pointer receiver) 38 }
We implemented the interface using a pointer receiver and attempted to pass a value of type user to the sendNotification function. Lines 30 and 32 in listing 5.44 show this clearly. But if we pass the address of the user value instead, you’ll see that it now compiles and works.
28 func main() { 29 // Create a value of type User and send a notification. 30 u := user{"Bill", "bill@email.com"} 31 32 sendNotification(&u) 33 34 // PASSED THE ADDRESS AND NO MORE ERROR. 35 }
In listing 5.45, we now have a program that compiles and runs. Only pointers of type user can be passed to the sendNotification function, since a pointer receiver was used to implement the interface.
The question now is why the restriction? The answer comes from the fact that it’s not always possible to get the address of a value.
01 // Sample program to show how you can't always get the 02 // address of a value. 03 package main 04 05 import "fmt" 06 07 // duration is a type with a base type of int. 08 type duration int 09 10 // format pretty-prints the duration value. 11 func (d *duration) pretty() string { 12 return fmt.Sprintf("Duration: %d", *d) 13 } 14 15 // main is the entry point for the application. 16 func main() { 17 duration(42).pretty() 18 19 // ./listing46.go:17: cannot call pointer method on duration(42) 20 // ./listing46.go:17: cannot take the address of duration(42) 21 }
The code in listing 5.46 attempts to get the address of a value of type duration and can’t. This shows that it’s not always possible to get the address of a value. Let’s look at the method set rules again.
Values Methods Receivers ----------------------------------------------- T (t T) *T (t T) and (t *T) Methods Receivers Values ----------------------------------------------- (t T) T and *T (t *T) *T
Because it’s not always possible to get the address of a value, the method set for a value only includes methods that are implemented with a value receiver.
Now that you understand the mechanics behind interfaces and method sets, let’s look at one final example that shows the polymorphic behavior of interfaces.
01 // Sample program to show how polymorphic behavior with interfaces. 02 package main 03 04 import ( 05 "fmt" 06 ) 07 08 // notifier is an interface that defines notification 09 // type behavior. 10 type notifier interface { 11 notify() 12 } 13 14 // user defines a user in the program. 15 type user struct { 16 name string 17 email string 18 } 19 20 // notify implements the notifier interface with a pointer receiver. 21 func (u *user) notify() { 22 fmt.Printf("Sending user email to %s<%s>\n", 23 u.name, 24 u.email) 25 } 26 27 // admin defines a admin in the program. 28 type admin struct { 29 name string 30 email string 31 } 32 33 // notify implements the notifier interface with a pointer receiver. 34 func (a *admin) notify() { 35 fmt.Printf("Sending admin email to %s<%s>\n", 36 a.name, 37 a.email) 38 } 39 40 // main is the entry point for the application. 41 func main() { 42 // Create a user value and pass it to sendNotification. 43 bill := user{"Bill", "bill@email.com"} 44 sendNotification(&bill) 45 46 // Create an admin value and pass it to sendNotification. 47 lisa := admin{"Lisa", "lisa@email.com"} 48 sendNotification(&lisa) 49 } 50 51 // sendNotification accepts values that implement the notifier 52 // interface and sends notifications. 53 func sendNotification(n notifier) { 54 n.notify() 55 }
In listing 5.48, we have a final example of how interfaces provide polymorphic behavior. On line 10, we have the same notifier interface that we declared in previous listings. Then on lines 15 through 25, we have the declaration of a struct named user with the implementation of the notifier interface using a pointer receiver. On lines 28 through 38, we have the declaration of a struct named admin with the implementation of the notifier interface as well. We have two concrete types implementing the notifier interface.
On line 53, we have our polymorphic sendNotification function again that accepts values that implement the notifier interface. Since any concrete type value can implement the interface, this function can execute the notify method for any concrete type value that’s passed in, thus providing polymorphic behavior.
40 // main is the entry point for the application. 41 func main() { 42 // Create a user value and pass it to sendNotification. 43 bill := user{"Bill", "bill@email.com"} 44 sendNotification(&bill) 45 46 // Create an admin value and pass it to sendNotification. 47 lisa := admin{"Lisa", "lisa@email.com"} 48 sendNotification(&lisa) 49 }
Finally, in listing 5.49 you see it all come together. A value of type user is created on line 43 in the main function, and then the address of that value is passed into send-Notification on line 44. This causes the notify method declared by the user type to be executed. Then we do the same with a value of type admin on lines 47 and 48. In the end, because sendNotification accepts interface values of type notifier, the function can execute the behavior implemented by both user and admin.
Go allows you to take existing types and both extend and change their behavior. This capability is important for code reuse and for changing the behavior of an existing type to suit a new need. This is accomplished through type embedding. It works by taking an existing type and declaring that type within the declaration of a new struct type. The type that is embedded is then called an inner type of the new outer type.
Through inner type promotion, identifiers from the inner type are promoted up to the outer type. These promoted identifiers become part of the outer type as if they were declared explicitly by the type itself. The outer type is then composed of everything the inner type contains, and new fields and methods can be added. The outer type can also declare the same identifiers as the inner type and override any fields or methods it needs to. This is how an existing type can be both extended and changed.
Let’s start with a sample program that shows the basics of type embedding.
01 // Sample program to show how to embed a type into another type and 02 // the relationship between the inner and outer type. 03 package main 04 05 import ( 06 "fmt" 07 ) 08 09 // user defines a user in the program. 10 type user struct { 11 name string 12 email string 13 } 14 15 // notify implements a method that can be called via 16 // a value of type user. 17 func (u *user) notify() { 18 fmt.Printf("Sending user email to %s<%s>\n", 19 u.name, 20 u.email) 21 } 22 23 // admin represents an admin user with privileges. 24 type admin struct { 25 user // Embedded Type 26 level string 27 } 28 29 // main is the entry point for the application. 30 func main() { 31 // Create an admin user. 32 ad := admin{ 33 user: user{ 34 name: "john smith", 35 email: "john@yahoo.com", 36 }, 37 level: "super", 38 } 39 40 // We can access the inner type's method directly. 41 ad.user.notify() 42 43 // The inner type's method is promoted. 44 ad.notify() 45 }
In listing 5.50, we have a program that shows how to embed a type and access the embedded identifiers. We start with the declaration of two struct types on lines 10 and 24.
09 // user defines a user in the program. 10 type user struct { 11 name string 12 email string 13 } 23 // admin represents an admin user with privileges. 24 type admin struct { 25 user // Embedded Type 26 level string 27 }
On line 10 in listing 5.51, we have the declaration of a struct type named user, and then on line 24 we have the declaration of a second struct type named admin. Inside the declaration of the admin type on line 25, we have the embedding of the user type as an inner type of admin. To embed a type, all that needs to happen is for the type name to be declared. One line 26, we have the declaration of a field named level. Notice the difference between declaring a field and embedding a type.
Once we embed the user type inside of admin, we can say that user is an inner type of the outer type admin. The concept of having an inner and outer type makes it easier to understand the relationship between the two.
15 // notify implements a method that can be called via 16 // a value of type user. 17 func (u *user) notify() { 18 fmt.Printf("Sending user email to %s<%s>\n", 19 u.name, 20 u.email) 21 }
Listing 5.52 shows the declaration of a method named notify using a pointer receiver of type user. The method just displays a friendly message stating an email is being sent to a specific user and email address. Now let’s look at the main function.
30 func main() { 31 // Create an admin user. 32 ad := admin{ 33 user: user{ 34 name: "john smith", 35 email: "john@yahoo.com", 36 }, 37 level: "super", 38 } 39 40 // We can access the inner type's method directly. 41 ad.user.notify() 42 43 // The inner type's method is promoted. 44 ad.notify() 45 }
The main function in listing 5.53 shows the mechanics behind type embedding. On line 32, a value of type admin is created. The initialization of the inner type is performed using a struct literal, and to access the inner type we just need to use the type’s name. Something special about an inner type is that it always exists in and of itself. This means the inner type never loses its identity and can always be accessed directly.
40 // We can access the inner type's method directly. 41 ad.user.notify()
On line 41 in listing 5.54, you see a call to the notify method. This call is made by accessing the user inner type directly through the admin outer type variable ad. This shows how the inner type exists in and of itself and is always accessible. But thanks to inner type promotion, the notify method can also be accessed directly from the ad variable.
43 // The inner type's method is promoted. 44 ad.notify() 45 }
Listing 5.55 on line 44 shows the method call to notify from the outer type variable. Since the identifiers of the inner type are promoted up to the outer type, we can access the inner type’s identifiers through values of the outer type. Let’s change the sample by adding an interface.
01 // Sample program to show how embedded types work with interfaces. 02 package main 03 04 import ( 05 "fmt" 06 ) 07 08 // notifier is an interface that defined notification 09 // type behavior. 10 type notifier interface { 11 notify() 12 } 13 14 // user defines a user in the program. 15 type user struct { 16 name string 17 email string 18 } 19 20 // notify implements a method that can be called via 21 // a value of type user. 22 func (u *user) notify() { 23 fmt.Printf("Sending user email to %s<%s>\n", 24 u.name, 25 u.email) 26 } 27 28 // admin represents an admin user with privileges. 29 type admin struct { 30 user 31 level string 32 } 33 34 // main is the entry point for the application. 35 func main() { 36 // Create an admin user. 37 ad := admin{ 38 user: user{ 39 name: "john smith", 40 email: "john@yahoo.com", 41 }, 42 level: "super", 43 } 44 45 // Send the admin user a notification. 46 // The embedded inner type's implementation of the 47 // interface is "promoted" to the outer type. 48 sendNotification(&ad) 49 } 50 51 // sendNotification accepts values that implement the notifier 52 // interface and sends notifications. 53 func sendNotification(n notifier) { 54 n.notify() 55 }
The sample code in listing 5.56 uses the same code from before but with a few changes.
08 // notifier is an interface that defined notification 09 // type behavior. 10 type notifier interface { 11 notify() 12 } 51 // sendNotification accepts values that implement the notifier 52 // interface and sends notifications. 53 func sendNotification(n notifier) { 54 n.notify() 55 }
On line 08 in listing 5.57, we have the declaration of the notifier interface. Then on line 53, we have the sendNotification function that accepts an interface value of type notifier. We know from the code before that the user type has declared a method named notify that implements the notifier interface with a pointer receiver. Therefore, we can move on to the changes made to the main function.
35 func main() { 36 // Create an admin user. 37 ad := admin{ 38 user: user{ 39 name: "john smith", 40 email: "john@yahoo.com", 41 }, 42 level: "super", 43 } 44 45 // Send the admin user a notification. 46 // The embedded inner type's implementation of the 47 // interface is "promoted" to the outer type. 48 sendNotification(&ad) 49 }
This is where things get interesting. On line 37 in listing 5.58, we create the admin outer type variable ad. Then on line 48, we pass the address of the outer type variable to the sendNotification function. The compiler accepts the assignment of the outer type pointer as a value that implements the notifier interface. But if you look at the entire sample program, you won’t see the admin type implement the interface.
Thanks to inner type promotion, the implementation of the interface by the inner type has been promoted up to the outer type. That means the outer type now implements the interface, thanks to the inner type’s implementation. When we run this sample program, we get the following output.
Output: Sending user email to john smith<john@yahoo.com> 20 // notify implements a method that can be called via 21 // a value of type user. 22 func (u *user) notify() { 23 fmt.Printf("Sending user email to %s<%s>\n", 24 u.name, 25 u.email) 26 }
You can see in listing 5.59 that the inner type’s implementation of the interface was called.
What if the outer type doesn’t want to use the inner type’s implementation because it needs an implementation of its own? Let’s look at another sample program that solves that problem.
01 // Sample program to show what happens when the outer and inner 02 // types implement the same interface. 03 package main 04 05 import ( 06 "fmt" 07 ) 08 09 // notifier is an interface that defined notification 10 // type behavior. 11 type notifier interface { 12 notify() 13 } 14 15 // user defines a user in the program. 16 type user struct { 17 name string 18 email string 19 } 20 21 // notify implements a method that can be called via 22 // a value of type user. 23 func (u *user) notify() { 24 fmt.Printf("Sending user email to %s<%s>\n", 25 u.name, 26 u.email) 27 } 28 29 // admin represents an admin user with privileges. 30 type admin struct { 31 user 32 level string 33 } 34 35 // notify implements a method that can be called via 36 // a value of type admin. 37 func (a *admin) notify() { 38 fmt.Printf("Sending admin email to %s<%s>\n", 39 a.name, 40 a.email) 41 } 42 43 // main is the entry point for the application. 44 func main() { 45 // Create an admin user. 46 ad := admin{ 47 user: user{ 48 name: "john smith", 49 email: "john@yahoo.com", 50 }, 51 level: "super", 52 } 53 54 // Send the admin user a notification. 55 // The embedded inner type's implementation of the 56 // interface is NOT "promoted" to the outer type. 57 sendNotification(&ad) 58 59 // We can access the inner type's method directly. 60 ad.user.notify() 61 62 // The inner type's method is NOT promoted. 63 ad.notify() 64 } 65 66 // sendNotification accepts values that implement the notifier 67 // interface and sends notifications. 68 func sendNotification(n notifier) { 69 n.notify() 70 }
The sample code in listing 5.60 uses the same code from before, but with a few more changes.
35 // notify implements a method that can be called via 36 // a value of type admin. 37 func (a *admin) notify() { 38 fmt.Printf("Sending admin email to %s<%s>\n", 39 a.name, 40 a.email) 41 }
This code sample adds an implementation of the notifier interface by the admin type. When the admin type’s implementation is called, it will display "Sending admin email" as opposed to the user type’s implementation that displays "Sending user email".
There are some more changes to the main function as well.
43 // main is the entry point for the application. 44 func main() { 45 // Create an admin user. 46 ad := admin{ 47 user: user{ 48 name: "john smith", 49 email: "john@yahoo.com", 50 }, 51 level: "super", 52 } 53 54 // Send the admin user a notification. 55 // The embedded inner type's implementation of the 56 // interface is NOT "promoted" to the outer type. 57 sendNotification(&ad) 58 59 // We can access the inner type's method directly. 60 ad.user.notify() 61 62 // The inner type's method is NOT promoted. 63 ad.notify() 64 }
On line 46 in listing 5.62, we have the creation of the outer type ad variable again. On line 57 the address of the ad variable is passed to the sendNotification function, and the value is accepted as implementing the interface. On line 60 the code calls the notify method from accessing the user inner type directly. Finally, on line 63 the notify method is called using the outer type variable ad. When you look at the output of this sample program, you see a different story.
Sending Admin Email To john smith<john@yahoo.com> Sending user email to john smith<john@yahoo.com> Sending admin email to john smith<john@yahoo.com>
This time you see how the admin type’s implementation of the notifier interface was executed both by the sendNotification function and through the use of the outer type variable ad. This shows how the inner type’s implementation was not promoted once the outer type implemented the notify method. But the inner type is always there, in and of itself, so the code is still able to call the inner type’s implementation directly.
The ability to apply visibility rules to the identifiers you declare is critical for good API design. Go supports the exporting and unexporting of identifiers from a package to provide this functionality. In chapter 3, we talked about packaging and how to import the identifiers from one package into another. Sometimes, you may not want identifiers such as types, functions, or methods to be a part of the public API for a package. In these cases, you need a way to declare those identifiers so they’re unknown outside the package. You need to declare them to be unexported.
Let’s start with a sample program that shows how to unexport identifiers from a package.
counters/counters.go ----------------------------------------------------------------------- 01 // Package counters provides alert counter support. 02 package counters 03 04 // alertCounter is an unexported type that 05 // contains an integer counter for alerts. 06 type alertCounter int listing64.go ----------------------------------------------------------------------- 01 // Sample program to show how the program can't access an 02 // unexported identifier from another package. 03 package main 04 05 import ( 06 "fmt" 07 08 "github.com/goinaction/code/chapter5/listing64/counters" 09 ) 10 11 // main is the entry point for the application. 12 func main() { 13 // Create a variable of the unexported type and initialize 14 // the value to 10. 15 counter := counters.alertCounter(10) 16 17 // ./listing64.go:15: cannot refer to unexported name 18 // counters.alertCounter 19 // ./listing64.go:15: undefined: counters.alertCounter 20 21 fmt.Printf("Counter: %d\n", counter) 22 }
In this example we have two code files. One is named counters.go and lives inside its own package named counters. The second code file is named listing64.go and is importing the counters package. Let’s start with the code inside the counters package.
01 // Package counters provides alert counter support. 02 package counters 03 04 // alertCounter is an unexported type that 05 // contains an integer counter for alerts. 06 type alertCounter int
Listing 5.65 isolates the code for just the counters package. The first thing you should notice is on line 02. Up until now, all the code samples have used package main, but here you see package counters. When you’re writing code that will live in its own package, it’s good practice to name the package the same as the folder the code is in. All the Go tooling expects this convention, so it’s a good practice to follow.
In package counters, we’ve declared a single identifier named alertCounter on line 06. This identifier is a type using int as its base type. An important aspect of this identifier is that it has been unexported.
When an identifier starts with a lowercase letter, the identifier is unexported or unknown to code outside the package. When an identifier starts with an uppercase letter, it’s exported or known to code outside the package. Let’s look at the code that imports this package.
01 // Sample program to show how the program can't access an 02 // unexported identifier from another package. 03 package main 04 05 import ( 06 "fmt" 07 08 "github.com/goinaction/code/chapter5/listing64/counters" 09 ) 10 11 // main is the entry point for the application. 12 func main() { 13 // Create a variable of the unexported type and initialize 14 // the value to 10. 15 counter := counters.alertCounter(10) 16 17 // ./listing64.go:15: cannot refer to unexported name 18 // counters.alertCounter 19 // ./listing64.go:15: undefined: counters.alertCounter 20 21 fmt.Printf("Counter: %d\n", counter) 22 }
The code in listing64.go from listing 5.66 is declaring the main package on line 03, and then on line 08 the counters package is imported. With the counters package imported, we move to the main function and line 15.
13 // Create a variable of the unexported type and initialize 14 // the value to 10. 15 counter := counters.alertCounter(10) 16 17 // ./listing64.go:15: cannot refer to unexported name 18 // counters.alertCounter 19 // ./listing64.go:15: undefined: counters.alertCounter
On line 15 in listing 5.67, the code attempts to create a value of the unexported type alertCounter. But this code produces a compiler error that states that the code at line 15 can’t refer to the unexported identifier counters.alertCounter. This identifier is undefined.
Since the alertCounter type from the counters package was declared using a lowercase letter, it was unexported and therefore unknown to the code in listing64.go. If we change the type to start with a capital letter, then the compiler error will go away. Let’s look at a new sample program that implements a factory function for the counters package.
counters/counters.go ----------------------------------------------------------------------- 01 // Package counters provides alert counter support. 02 package counters 03 04 // alertCounter is an unexported type that 05 // contains an integer counter for alerts. 06 type alertCounter int 07 08 // New creates and returns values of the unexported 09 // type alertCounter 10 func New(value int) alertCounter { 11 return alertCounter(value) 12 } listing68.go ----------------------------------------------------------------------- 01 // Sample program to show how the program can access a value 02 // of an unexported identifier from another package. 03 package main 04 05 import ( 06 "fmt" 07 08 "github.com/goinaction/code/chapter5/listing68/counters" 09 ) 10 11 // main is the entry point for the application. 12 func main() { 13 // Create a variable of the unexported type using the exported 14 // New function from the package counters. 15 counter := counters.New(10) 16 17 fmt.Printf("Counter: %d\n", counter) 18 }
This example has been changed to use a factory function to create values of the unexported alertCounter type. Let’s look at the code in the counters package first.
01 // Package counters provides alert counter support. 02 package counters 03 04 // alertCounter is an unexported type that 05 // contains an integer counter for alerts. 06 type alertCounter int 07 08 // New creates and returns values of the unexported 09 // type alertCounter. 10 func New(value int) alertCounter { 11 return alertCounter(value) 12 }
Listing 5.69 shows the changes we made to the counters package. The alertCounter type is still unexported, but now on line 10 we have a function called New. It’s a convention in Go to give factory functions the name of New. This New function does something interesting: it creates a value of the unexported type and returns that value back to the caller. Let’s look at the main function from listing68.go.
11 // main is the entry point for the application. 12 func main() { 13 // Create a variable of the unexported type using the exported 14 // New function from the package counters. 15 counter := counters.New(10) 16 17 fmt.Printf("Counter: %d\n", counter) 18 }
On line 15 in listing 5.70, you see a call to the New function from the counters package. The value returned by the New function is then assigned to a variable named counter. This program compiles and runs, but why? The New function is returning a value of the unexported type alertCounter, yet main is able to accept that value and create a variable of the unexported type.
This is possible for two reasons. First, identifiers are exported or unexported, not values. Second, the short variable declaration operator is capable of inferring the type and creating a variable of the unexported type. You can never explicitly create a variable of an unexported type, but the short variable declaration operator can.
Let’s look at a new sample program that shows how fields from a struct type are affected by these visibility rules.
entities/entities.go ----------------------------------------------------------------------- 01 // Package entities contains support for types of 02 // people in the system. 03 package entities 04 05 // User defines a user in the program. 06 type User struct { 07 Name string 08 email string 09 } listing71.go ----------------------------------------------------------------------- 01 // Sample program to show how unexported fields from an exported 02 // struct type can't be accessed directly. 03 package main 04 05 import ( 06 "fmt" 07 08 "github.com/goinaction/code/chapter5/listing71/entities" 09 ) 10 11 // main is the entry point for the application. 12 func main() { 13 // Create a value of type User from the entities package. 14 u := entities.User{ 15 Name: "Bill", 16 email: "bill@email.com", 17 } 18 19 // ./example69.go:16: unknown entities.User field 'email' in 20 // struct literal 21 22 fmt.Printf("User: %v\n", u) 23 }
The code in listing 5.71 changed things a bit. Now we have a package called entities that declares a struct type named User.
01 // Package entities contains support for types of 02 // people in the system. 03 package entities 04 05 // User defines a user in the program. 06 type User struct { 07 Name string 08 email string 09 }
The User type on line 06 in listing 5.72 is declared to be exported. Two fields are declared with the User type, an exported field named Name and an unexported field named email. Let’s look at the code in listing71.go.
01 // Sample program to show how unexported fields from an exported 02 // struct type can't be accessed directly. 03 package main 04 05 import ( 06 "fmt" 07 08 "github.com/goinaction/code/chapter5/listing71/entities" 09 ) 10 11 // main is the entry point for the application. 12 func main() { 13 // Create a value of type User from the entities package. 14 u := entities.User{ 15 Name: "Bill", 16 email: "bill@email.com", 17 } 18 19 // ./example71.go:16: unknown entities.User field 'email' in 20 // struct literal 21 22 fmt.Printf("User: %v\n", u) 23 }
The entities package is imported on line 08 in listing 5.73. On line 14 a variable named u of the exported type User from the entities package is declared and its fields initialized. But there’s a problem. On line 16 the code attempts to initialize the unexported field email, and the compiler complains the field is unknown. That identifier can’t be accessed outside the entities package, since it has been unexported.
Let’s look at one final example to show how the exporting and unexporting of embedded types work.
entities/entities.go ----------------------------------------------------------------------- 01 // Package entities contains support for types of 02 // people in the system. 03 package entities 04 05 // user defines a user in the program. 06 type user struct { 07 Name string 08 Email string 09 } 10 11 // Admin defines an admin in the program. 12 type Admin struct { 13 user // The embedded type is unexported. 14 Rights int 15 } listing74.go ----------------------------------------------------------------------- 01 // Sample program to show how unexported fields from an exported 02 // struct type can't be accessed directly. 03 package main 04 05 import ( 06 "fmt" 07 08 "github.com/goinaction/code/chapter5/listing74/entities" 09 ) 10 11 // main is the entry point for the application. 12 func main() { 13 // Create a value of type Admin from the entities package. 14 a := entities.Admin{ 15 Rights: 10, 16 } 17 18 // Set the exported fields from the unexported 19 // inner type. 20 a.Name = "Bill" 21 a.Email = "bill@email.com" 22 23 fmt.Printf("User: %v\n", a) 24 }
Now, in listing 5.74 the entities package contains two struct types.
01 // Package entities contains support for types of 02 // people in the system. 03 package entities 04 05 // user defines a user in the program. 06 type user struct { 07 Name string 08 Email string 09 } 10 11 // Admin defines an admin in the program. 12 type Admin struct { 13 user // The embedded type is unexported. 14 Rights int 15 }
On line 06 in listing 5.75, an unexported struct type named user is declared. It contains two exported fields named Name and Email. On line 12 an exported struct type named Admin is declared. Admin has an exported field named Rights, but it also embeds the unexported user type. Let’s look at the code in the main function for listing74.go.
11 // main is the entry point for the application. 12 func main() { 13 // Create a value of type Admin from the entities package. 14 a := entities.Admin{ 15 Rights: 10, 16 } 17 18 // Set the exported fields from the unexported 19 // inner type. 20 a.Name = "Bill" 21 a.Email = "bill@email.com" 22 23 fmt.Printf("User: %v\n", a) 24 }
The main function starts out on line 14 in listing 5.76 by creating a value of type Admin from the entities package. Since the user inner type is unexported, this code can’t access the inner type to initialize it inside the struct literal. Even though the inner type is unexported, the fields declared within the inner type are exported. Since the identifiers from the inner type are promoted to the outer type, those exported fields are known through a value of the outer type.
Therefore, on line 20 and 21 the Name and Email fields from the unexported inner type can be accessed and initialized through the outer type variable a. There’s no access to the inner type directly, since the user type is unexported.