Learning Go
Jon Bodner · O’Reilly · 2021 · 374 p.
https://www.oreilly.com/library/view/learning-go/9781492077206/ Jon Bodner - Learning Go_ An Idiomatic Approach to Real-World Go Programming-O’Reilly Media (2021).pdf
Abstract
Go is rapidly becoming the preferred language for building web services. While there are plenty of tutorials available that teach Go’s syntax to developers with experience in other programming languages, tutorials aren’t enough. They don’t teach Go’s idioms, so developers end up recreating patterns that don’t make sense in a Go context. This practical guide provides the essential background you need to write clear and idiomatic Go.
No matter your level of experience, you’ll learn how to think like a Go developer. Author Jon Bodner introduces the design patterns experienced Go developers have adopted and explores the rationale for using them. You’ll also get a preview of Go’s upcoming generics support and how it fits into the language.
- Learn how to write idiomatic code in Go and design a Go project
- Understand the reasons for the design decisions in Go
- Set up a Go development environment for a solo developer or team
- Learn how and when to use reflection, unsafe, and cgo
- Discover how Go’s features allow the language to run efficiently
- Know which Go features you should use sparingly or not at all
Go Features
- There’s no inheritance,
no generics, no aspect-oriented programming, no function overloading, no operator overloading. No pattern matching, no named parameters, no exceptions. - No built-in map, filter, reduce functions.
- In Go single quotes and double quotes are not interchangeable.
- Go doesn’t allow automatic type promotion between variables. You must use a type conversion when variable types do not match.
- Go has garbage collector.
- You cannot treat another Go type as a boolean.
- Constants in Go are a way to give names to literals. There is no way in Go to declare that a variable is immutable.
- Go requirement is that every declared local variable must be read. It is a compile-time error to declare a local variable and to not read its value.
- Any Unicode character that is considered a letter or digit is allowed in variable names.
- In Go,
nil
is an identifier that represents the lack of a value for some types. - Go is a call by value language. Every time you pass a parameter to a function, Go makes a copy of the value that’s passed in.
- According to the language specification, Go source code is always written in UTF-8. Unless you use hexadecimal escapes in a string literal, your string literals are written in UTF-8.
- Built-in embedding support (see Use Embedding for Composition in Chapter 7).
- Channels are one of the two things that set apart Go’s concurrency model.
- The
select
statement is the other thing that sets apart Go’s concurrency model.
Go Facts
- Created in 2009.
- Docker, Kubernetes, Prometheus are written in Go.
- Just 25 keywords and 1 loop type (35 keywords in Python).
- Fun fact: UTF-8 was invented in 1992 by Ken Thompson and Rob Pike, two of the creators of Go.
References
- 1. Setting Up Your Go Environment (21)
- 2. Primitive Types and Declarations (37)
- 3. Composite Types (55) (01.12)
- 4. Blocks, Shadows, and Control Structures (81) (01.12)
- 5. Functions (107) (01.12)
- 6. Pointers (127) (02.12)
- 7. Types, Methods, and Interfaces (149) (04.12)
- 8. Errors (181) (06.12)
- 9. Modules, Packages, and Imports (197) (08.12)
- 10. Concurrency in Go (223) (11.12)
- 11. The Standard Library (253)
- 12. The Context (275)
- 13. Writing Tests (291)
- 14. Here There Be Dragons: Reflect, Unsafe, and Cgo (319)
- 15. A Look at the Future: Generics in Go (345)
ToDos
- Watch GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps
- Написать пост Впечатления от Learning Go Джона Боднера для своего сайта.
Chapter 1. Setting Up Your Go Environment
Chapter 2. Primitive Types and Declarations
Literals
Unlike many other languages, in Go single quotes and double quotes are not interchangeable. Rune literals represent characters and are surrounded by single quotes.
String literals:
- interpreted string literal –
"Greetings and\n\"Salutations\""
. - raw string literal:
Booleans
Numeric Types
Integers
int8
int16
int32
int64
uint8
uint16
uint32
uint64
- A
byte
is an alias foruint8
- On most 64-bit CPUs, ==
int
== is a 64-bit signed integer, just like anint64
. - The third special name is
uint
. It follows the same rules asint
, only it is unsigned.
Floating point types
float32
float64
Tip
A floating point number cannot represent a decimal value exactly. Do not use them to represent money or any other value that must have an exact decimal representation!
Tip
While Go lets you use
==
and!=
to compare floats, don’t do it. Due to the inexact nature of floats, two floating point values might not be equal when you think they should be. Instead, define a maximum allowed variance and see if the difference between two floats is less than that.
Complex types (you’re probably not going to use these)
complex64
complex128
Strings
- Strings in Go are immutable; you can reassign the value of a string variable, but you cannot change the value of the string that is assigned to it.
Runes
The rune
type is an alias for the int32
type, just like byte
is an alias for uint8
. As you could probably guess, a rune literal’s default type is a rune
, and a string literal’s default type is a string
.
Rune literals represent characters and are surrounded by single quotes. Unlike many other languages, in Go single quotes and double quotes are not interchangeable.
Explicit Type Conversion
Go doesn’t allow automatic type promotion between variables. You must use a type conversion when variable types do not match.
Since all type conversions in Go are explicit, you cannot treat another Go type as a boolean.
If you want to convert from another data type to boolean, you must use one of the comparison operators (==
, !=, >, <, <=, or >=). For example, to check if variable x is equal to 0, the code would be x == 0
. If you want to check if string s is empty, use s == ""
.
Variable Declaration
Tip
There is one limitation on
:=
. If you are declaring a variable at package level, you must usevar
because:=
is not legal outside of functions.
Using const
const
in Go is very limited. Constants in Go are a way to give names to literals. They can only hold values that the compiler can figure out at compile time. This means that they can be assigned:
- Numeric literals.
- true and false.
- Strings.
- Runes.
- The built-in functions
complex
,real
,imag
,len
, andcap
. - Expressions that consist of operators and the preceding values.
Info
Constants in Go are a way to give names to literals. There is no way in Go to declare that a variable is immutable.
Chapter 3. Composite Types
Arrays
You can use ==
and !=
to compare arrays.
Go considers the size of the array to be part of the type of the array. This makes an array that’s declared to be [3]int
a different type from an array that’s declared to be [4]int
. This also means that you cannot use a variable to specify the size of an array, because types must be resolved at compile time, not at runtime.
Slices
Tip
Using
[...]
makes an array. Using[]
makes a slice.
A slice is the first type we’ve seen that isn’t comparable. It is a compile-time error to use ==
to see if two slices are identical or !=
to see if they are different. The only thing you can compare a slice with is nil
.
Slicing slices
Tip
Note that
e := x[:]
does not make the copy of the original slice! Changinge
in this example will changex
,y
, too! Use built-incopy()
instead!
Slices share storage sometimes
When you take a slice from a slice, you are not making a copy of the data. Instead, you now have two variables that are sharing memory. This means that changes to an element in a slice affect all slices that share that element.
Tip
Be very careful when taking a slice of a slice! Both slices share the same memory and changes to one are reflected in the other. Avoid modifying slices after they have been sliced or if they were produced by slicing. Use a three-part slice expression to prevent append from sharing capacity between slices.
copy()
See example in example_3_6a.go
:
Strings and Runes and Bytes
Under the covers, Go uses a sequence of bytes to represent a string.
The slice expression notation that we used with arrays and slices also works with strings:
Tip
Even though Go allows you to use slicing and indexing syntax with strings, you should only use it when you know that your string only contains characters that take up one byte.
Maps
Maps are like slices in several ways:
- Maps automatically grow as you add key-value pairs to them.
- If you know how many key-value pairs you plan to insert into a map, you can use
make
to create a map with a specific initial size. - Passing a map to the
len
function tells you the number of key-value pairs in a map. - The zero value for a map is
nil
. - Maps are not comparable. You can check if they are equal to
nil
, but you cannot check if two maps have identical keys and values using==
or differ using!=
.
Note
You can use the
++
operator to increment the numeric value for a map key. Because a map returns its zero value by default, this works even when there’s no existing value associated with the key.
Key-value pairs are removed from a map via the built-in delete
function:
The delete
function takes a map and a key and then removes the key-value pair with the specified key. If the key isn’t present in the map or if the map is nil
, nothing happens.
Using a map as a set
Structs
Anonymous Structs
You can also declare that a variable implements a struct type without first giving the struct type a name. This is called an anonymous struct:
Chapter 4. Blocks, Shadows, and Control Structures
Universe Block is the scope where things like types (e.g. int
, string
), constants (true
, false
), functions (make
, len
) and nil
are living.
if
Go adds the ability to declare variables that are scoped to the condition and to both the if
and else blocks
.
for
, Four Ways
for
is the single statement to loop in Go, but it has four formats.
Complete for
statement
The Condition-Only for
Statement
Equals to while
in other languages:
Infinite Loop
Use break
to exit the loop, and continue
to proceed to the next iteration.
for
-range
Statement
Used for iterating over elements in some of Go’s built-in types.
Tip
You can only use a
for
-range
loop to iterate over the built-in compound types and user-defined types that are based on them.
Iterating over strings
Note
Use a
for
-range
loop to access the runes in a string in order.
Labeling Your for Statements
Labels can be used with continue
keyword. See page 96 (76 in the book).
switch
statement
Chapter 5. Functions
Variadic Input Parameters and Slices
The variadic parameter must be the last (or only) parameter in the input parameter list. You indicate it with three dots (...
) before the type. The variable that’s created within the function is a slice of the specified type. You use it just like any other slice.
Multiple Return Values
When a Go function returns multiple values, the types of the return values are listed in parentheses, separated by commas. Also, if a function returns multiple values, you must return all of them, separated by commas. Don’t put parentheses around the returned values; that’s a compile-time error.
You must assign each value returned from a function. If you try to assign multiple return values to one variable, you get a compile-time error.
Go also allows you to specify names for your return values. When you supply names to your return values, what you are doing is pre-declaring variables that you use within the function to hold the return values. They are written as a comma-separated list within parentheses. You must surround named return values with parentheses, even if there is only a single return value. Named return values are initialized to their zero values when created. This means that we can return them before any explicit use or assignment.
Tip
If your function returns values, never use a blank return. It can make it very confusing to figure out what value is actually returned.
Functions Are Values
Just like you can use the type
keyword to define a struct
, you can use it to define a function type, too:
Anonymous Functions
Closures
Functions declared inside of functions are special; they are closures. This is a computer science word that means that functions declared inside of functions are able to access and modify variables declared in the outer function.
Returning Functions from Functions
defer
Normally, a function call runs immediately, but defer
delays the invocation until the surrounding function exits.
You can defer
multiple closures in a Go function. They run in last-in-first-out order; the last defer
registered runs first.
Chapter 6. Pointers
The &
is the address operator. It precedes a value type and returns the address of the memory location where the value is stored:
The *
is the indirection operator. It precedes a variable of pointer type and returns the pointed-to value. This is called dereferencing:
Before dereferencing a pointer, you must make sure that the pointer is non-nil. Your program will panic if you attempt to dereference a nil
pointer:
A pointer type is a type that represents a pointer. It is written with a *
before a type name. A pointer type can be based on any type:
The built-in function new
creates a pointer variable. It returns a pointer to a zero value instance of the provided type:
The right way to update the value where the pointer points to:
Tip
You should be careful when using pointers in Go. The only time you should use pointer parameters to modify a variable is when the function expects an interface. You see this pattern when working with JSON. Because JSON integration is so common, this API is sometimes treated as a common case by new Go developers, instead of the exception that it should be.
Tip
Рекомендуется умеренное использование указателей в коде, чтобы сократить объем работы сборщика мусора.
Chapter 7. Types, Methods, and Interfaces
Defining concrete types:
Methods
Go supports methods on user-defined types.
Method declarations look just like function declarations, with one addition: the receiver specification. The receiver appears between the keyword func and the name of the method. Just like all other variable declarations, the receiver name appears before the type. ==By convention, the receiver name is a short abbreviation of the type’s name, usually its first letter. It is nonidiomatic to use this
or self
.==
Just like functions, method names cannot be overloaded. You can use the same method names for different types, but you can’t use the same method name for two different methods on the same type.
Methods must be declared in the same package as their associated type; Go doesn’t allow you to add methods to types you don’t control. While you can define a method in a different file within the same package as the type declaration, it is best to keep your type definition and its associated methods together so that it’s easy to follow the implementation.
Pointer Receivers and Value Receivers
Do not write getter and setter methods for Go structs, unless you need them to meet an interface. Go encourages you to directly access a field. Reserve methods for business logic. The exceptions are when you need to update multiple fields as a single operation or when the update isn’t a straightforward assignment of a new value.
Methods Are Functions Too
Functions Versus Methods
Info
Package-level state should be effectively immutable.
Any time your logic depends on values that are configured at startup or changed while your program is running, those values should be stored in a struct and that logic should be implemented as a method.
If your logic only depends on the input parameters, then it should be a function.
iota
Is for Enumerations—Sometimes
Use Embedding for Composition
Note that Manager
contains a field of type Employee
, but no name is assigned to that field. This makes Employee
an embedded field. Any fields or methods declared on an embedded field are promoted to the containing struct and can be invoked directly on it. That makes the following code valid:
A Quick Lesson on Interfaces
It lists the methods that must be implemented by a concrete type to meet the interface. The methods defined by an interface are called the method set of the interface.
Interfaces are usually named with “er” endings. Examples: io.Reader
, io.Closer
, io.ReadCloser
, json.Marshaler
, and http.Handler
.
What makes Go’s interfaces special is that they are implemented implicitly. A concrete type does not declare that it implements an interface. If the method set for a concrete type contains all of the methods in the method set for an interface, the concrete type implements the interface. This means that the concrete type can be assigned to a variable or field declared to be of the type of the interface.
In the Go code, there is an interface, but only the caller (Client
) knows about it; there is nothing declared on LogicProvider
to indicate that it meets the interface. This is sufficient to both allow a new logic provider in the future and provide executable documentation to ensure that any type passed into the client will match the client’s need.
Info
Interfaces specify what callers need. The client code defines the interface to specify what functionality it requires.
Embedding and Interfaces
The Empty Interface Says Nothing
Sometimes in a statically typed language, you need a way to say that a variable could store a value of any type. Go uses interface{}
to represent this:
You should note that interface{}
isn’t special case syntax. An empty interface type simply states that the variable can store any value whose type implements zero or more methods. This just happens to match every type in Go.
Tip
These situations should be relatively rare. Avoid using
interface{}
. As we’ve seen, Go is designed as a strongly typed language and attempts to work around this are unidiomatic.
Chapter 8. Errors
How to Handle Errors: The Basics
Info
Go handles errors by returning a value of type error as the last return value for a function. This is entirely by convention, but it is such a strong convention that it should never be breached. When a function executes as expected,
nil
is returned for the error parameter. If something goes wrong, an error value is returned instead. The calling function then checks the error return value by comparing it to nil, handling the error, or returning an error of its own.Error messages should not be capitalized nor should they end with punctuation or a newline.
error
is a built-in interface that defines a single method:
The second way is to use the fmt.Errorf
function. This function allows you to use all of the formatting verbs for fmt.Printf
to create an error
. Like errors.New
, this string is returned when you call the Error
method on the returned error
instance:
Wrapping Errors
When you preserve an error while adding additional information, it is called wrapping the error. When you have a series of wrapped errors, it is called an error chain.
There’s a function in the Go standard library that wraps errors, and we’ve already seen it. The fmt.Errorf
function has a special verb, %w
. Use this to create an error whose formatted string includes the formatted string of another error and which contains the original error as well. The convention is to write: %w
at the end of the error format string and make the error to be wrapped the last parameter passed to fmt.Errorf
.
The standard library also provides a function for unwrapping errors, the Unwrap
function in the errors
package. You pass it an error and it returns the wrapped error, if there is one. If there isn’t, it returns nil
.
Wrapping Errors with defer
This pattern works well when you are wrapping every error with the same message. If you want to customize the wrapping error to provide more context about what caused the error, then put both the specific and the general message in every fmt.Errorf
.
panic and recover
Go generates a panic whenever there is a situation where the Go runtime is unable to figure out what should happen next. This could be due to a programming error (like an attempt to read past the end of a slice) or environmental problem (like running out of memory). As soon as a panic happens, the current function exits immediately and any defers attached to the current function start running. When those defers complete, the defers attached to the calling function run, and so on, until main is reached. The program then exits with a message and a stack trace.
Go provides a way to capture a panic to provide a more graceful shutdown or to pre‐ vent shutdown at all. The built-in recover
function is called from within a defer
to check if a panic happened. If there was a panic, the value assigned to the panic is returned. Once a recover
happens, execution continues normally.
Tip
While
panic
andrecover
look a lot like exception handling in other languages, they are not intended to be used that way. Reserve panics for fatal situations and userecover
as a way to gracefully handle these situations. If your program panics, be very careful about trying to continue executing after the panic. It’s very rare that you want to keep your program running after a panic occurs. If the panic was triggered because the computer is out of a resource like memory or disk space, the safest thing to do is userecover
to log the situation to monitoring software and shut down withos.Exit(1)
. If there’s a programming error that caused the panic, you can try to con‐ tinue, but you’ll likely hit the same problem again.
Chapter 9. Modules, Packages, and Imports
Before we can use code from packages outside of the standard library, we need to make sure that we have declared that our project is a module. Every module has a globally unique identifier. This is not unique to Go.
go.mod
Imports and Exports
Go’s import
statement allows you to access exported constants, variables, functions, and types in another package. A package’s exported identifiers (an identifier is the name of a variable, constant, type, function, method, or a field in a struct) cannot be accessed from another current package without an import
statement.
Rather than use a special keyword, Go uses capitalization to determine if a package-level identifier is visible outside of the package where it is declared. ==An identifier whose name starts with an uppercase letter is exported==. Conversely, an identifier whose name starts with a lowercase letter or underscore can only be accessed from within the package where it is declared.
You must specify an import path when importing from anywhere besides the standard library. The import path is built by appending the path to the package within the module to the module path.
Tip
While you can use a relative path to import a dependent package within the same module, don’t do this. Absolute import paths clar‐ ify what you are importing and make it easier to refactor your code.
Example on how to create packages in Go program.
Naming Packages
It’s better to create one function called Names
in a package called extract
and a second function called Names
in a package called format
. It’s OK for these two functions to have the same name, because they will always be disambiguated by their package names. The first will be referred to as extract.Names
when imported, and the second will be referred to as format.Names
.
Overriding a Package’s Name
The package name .
places all the exported identifiers in the imported package into the current package’s namespace; you don’t need a prefix to refer to them. This is discouraged because it makes your source code less clear as you no longer know whether something is defined in the current package or an imported one by simply looking at its name.
Package Comments and godoc
Go has its own format for writing comments that are automatically converted into documentation. It’s called godoc format and it’s very simple. There are no special symbols in a godoc comment. They just follow a convention. Here are the rules:
- Place the comment directly before the item being documented with no blank lines between the comment and the declaration of the item.
- Start the comment with two forward slashes (
//
) followed by the name of the item. - Use a blank comment to break your comment into multiple paragraphs.
- Insert preformatted comments by indenting the lines.
Go includes a command-line tool called go doc that views godocs. The command go doc PACKAGE_NAME
displays the package godocs for the specified package and a list of the identifiers in the package. Use go doc PACKAGE_NAME.IDENTIFIER_NAME
to display the documentation for a specific identifier in the package.
The internal
Package
Circular Dependencies
Go does not allow you to have a circular dependency between packages.
If two packages depend on each other, there’s a good chance they should be merged into a single package.
If you have a good reason to keep your packages separated, it may be possible to move just the items that cause the circular dependency to one of the two packages or to a new package.
Chapter 10. Concurrency in Go
Goroutines
Goroutines are lightweight processes managed by the Go runtime. When a Go program starts, the Go runtime creates a number of threads and launches a single goroutine to run your program. All of the goroutines created by your program, including the initial one, are assigned to these threads automatically by the Go runtime scheduler, just as the operating system schedules threads across CPU cores. This might seem like extra work, since the underlying operating system already includes a scheduler that manages threads and processes, but it has several benefits:
- Goroutine creation is faster than thread creation, because you aren’t creating an operating system–level resource.
- Goroutine initial stack sizes are smaller than thread stack sizes and can grow as needed. This makes goroutines more memory efficient.
- Switching between goroutines is faster than switching between threads because it happens entirely within the process, avoiding operating system calls that are (relatively) slow.
- The scheduler is able to optimize its decisions because it is part of the Go process.
These advantages allow Go programs to spawn hundreds, thousands, even tens of thousands of simultaneous goroutines.
Any function can be launched as a goroutine. This is different from JavaScript, where a function only runs asynchronously if the author of the function declared it with the async keyword. However, it is customary in Go to launch goroutines with a closure that wraps business logic. The closure takes care of the concurrent bookkeeping. For example, the closure reads values out of channels and passes them to the business logic, which is completely unaware that it is running in a goroutine. The result of the function is then written back to a different channel.
Channels
Channels are one of the two things that set apart Go’s concurrency model.
Goroutines communicate using channels. Like slices and maps, channels are a built-in type created using the make
function:
Like maps, channels are reference types. When you pass a channel to a function, you are really passing a pointer to the channel.
Reading, Writing, and Buffering
for
-range
and Channels
The loop continues until the channel is closed, or until a break
or return
statement is reached.
Closing a Channel
If ok
is set to true
, then the channel is open. If it is set to false
, the channel is closed.
The responsibility for closing a channel lies with the goroutine that writes to the channel. Be aware that closing a channel is only required if there is a goroutine waiting for the channel to close (such as one using a for
-range
loop to read from the channel). Since a channel is just another variable, Go’s runtime can detect channels that are no longer used and garbage collect them.
select
The select
statement is the other thing that sets apart Go’s concurrency model. It is the control structure for concurrency in Go, and it elegantly solves a common problem: if you can perform two concurrent operations, which one do you do first? You can’t favor one operation over others, or you’ll never process some cases.
The following code does not wait if there’s no value to read in ch
; it immediately executes the body of the default
:
Concurrency Practices and Patterns
Keep Your APIs Concurrency-Free
Concurrency is an implementation detail, and good API design should hide implementation details as much as possible. This allows you to change how your code works without changing how your code is invoked.
Goroutines, for Loops, and Varying Variables
Any time your goroutine uses a variable whose value might change, pass the current value of the variable into the goroutine.
Always Clean Up Your Goroutines
Whenever you launch a goroutine function, you must make sure that it will eventually exit. Unlike variables, the Go runtime can’t detect that a goroutine will never be used again. If a goroutine doesn’t exit, the scheduler will still periodically give it time to do nothing, which slows down your program.
Chapter 11. The Standard Library
time
Reference: https://pkg.go.dev/time
There are two main types used to represent time, time.Duration
and time.Time
.
An moment of time is represented with the time.Time
type, complete with a time zone. You acquire a reference to the current time with the function time.Now
. This returns a time.Time
instance set to the current local time.
The time.Parse
function converts from a string to a time.Time
, while the Format
method converts a time.Time
to a string
.
time.Format
relies on the idea of formatting the date and time January 2, 2006 at 3:04:05PM MST (Mountain Standard Time) to specify your format.
encoding/json
Go’s standard library includes support for converting Go data types to and from JSON. The word ==marshaling means converting from a Go data type to an encoding, and unmarshaling== means converting to a Go data type.
Use Struct Tags to Add Metadata
Let’s say that we have to read and write the following JSON:
Unmarshaling and Marshaling
📂 Reading | Последнее изменение: 25.08.2024 11:23