Chapter 1-3

When the Go program is compiled, the compiler only focuses on the libraries that are directly referenced, rather than iterating through all the dependent libraries in the dependency chain as Java, C, and C++ do.

Go’s support for concurrency is one of the language’s most important features. A Goroutine is like a thread, but it takes up much less memory and requires less code to use it. A channel is a built-in data structure that allows users to send typed messages synchronously between different Goroutines.

goroutine

A goroutine is a function that can be executed in parallel with other Goroutines, as well as with the main program (the entry to the program).

channel

A channel is a data structure that allows secure data communication between Goroutines. Channels help users avoid shared memory access problems common in other languages.

Data transfer between two Goroutines is synchronous, and once the transfer is complete, both Goroutines know that the data transfer is complete.

Channels do not provide data access protection across goroutines. If a copy of the data is transmitted over a channel, then each Goroutine holds a copy, and it is safe for each goroutine to make changes to its own copy. When passing Pointers to data, if reading and writing is done by different Goroutines, each goroutine still requires additional synchronization.

The type system

Go developers use the composition design pattern, which allows them to reuse all functionality by simply embedding one type into another.

The Go language also has a unique interface implementation mechanism that allows users to model behavior rather than types.

The init() function is called before the main() function.

A package defines a set of compiled code with namespace-like names that can be used to indirectly access identifiers declared within the package. This feature differentiates identifiers of the same name defined in different packages.

The import path is preceded by an underscore, for example

_ "github.com/goinaction/code/chapter2/sample/matchers"
Copy the code

This technique is used to allow the Go language to initialize packages without using identifiers inside the packages. To make the program more readable, the Go compiler does not allow you to declare that a package is imported without using it. The underscore tells the compiler to accept such imports and call the init function defined in all the code files in the corresponding package.

Variables are not defined in any function scope and are treated as package-level variables.

In the Go language, identifiers are either exposed from the package or not. When code imports a package, the program can directly access any public identifier in the package. These identifiers begin with a capital letter. Identifiers that begin with lowercase letters are not public and cannot be directly accessed by code in other packages.

In Go, all variables are initialized to their zero value. For numeric types, the zero value is 0; For string types, a zero value is an empty string; For Boolean types, zero is false; For Pointers, zero is nil. For reference types, the underlying data structure referenced is initialized to the corresponding zero value. But variables of reference type declared as zero return nil as their value.

The Go language declares functions using the keyword func, which is followed by the function name, parameters, and return value

Slicing is a reference type that implements a dynamic array. In Go, you can use slices to manipulate a set of data.

Simplify the variable declaration operator (:=). This operator is used to declare a variable and give it an initial value. The compiler uses the type of the return value of a function to determine the type of each variable. The simplified variable declaration operator is just a shorthand to make your code more readable. The variable declared by this operator is no different from any other variable declared using the var keyword.

If you want to declare a variable with an initial value of zero, you should use the var keyword. If an exact non-zero value is provided to initialize a variable or to create a variable using a function return value, the simplified variable declaration operator should be used.

The WaitGroup of the Sync package keeps track of all started Goroutines. In practical development, it is highly recommended to use WaitGroup to track whether or not goroutine work is complete. The WaitGroup is a counting semaphore that we can use to count whether all the Goroutines have completed their work.

The keyword range can be used to iterate over arrays, strings, slicing, maps, and channels. When iterating slices using for range, two values are returned per iteration. The first value is the index position of the iterated element in the slice, and the second value is a copy of the element’s value.

If you are calling a function that returns multiple values and does not need one of them, you can ignore it using the underscore identifier.

A goroutine is a function that runs independently of other functions. Start a Goroutine with the keyword go and schedule the goroutine concurrently.

Pointer variables make it easy to share data between functions. Using pointer variables allows functions to access and modify the state of a variable that can be declared in the scope of other functions or even other Goroutines. The value of a pointer variable is the memory address it points to, and passing a pointer variable between functions is passing that address value.

The keyword defer causes subsequent function calls to be executed when the function returns. After using the file, you need to actively close the file. Arranging to call the Close method using the keyword defer guarantees that this function will be called. Even if the function crashes unexpectedly and terminates, you are guaranteed that the function that the keyword defer arranged to call will be executed. The keyword defer reduces the number of lines of code between opening and closing files, which helps improve code readability and reduce errors.

The behavior of an interface is ultimately determined by the methods declared in that interface type.

When naming interfaces, you also need to follow the naming conventions of the Go language. If the interface type contains only one method, the name of the type ends in er. That’s what we did in our example, so the interface is called Matcher. If an interface type declares more than one method internally, its name needs to be associated with its behavior.

type Matcher interface {
    Search(feed *Feed, searchTerm string) ([]*Result, error)
}
Copy the code

If you want a user-defined type to implement an interface, the user-defined type implements all the methods declared in the interface type.

type defaultMatcher struct{}
Copy the code

The above code uses an empty structure to declare a structure type called defaultMatcher. Empty structures do not allocate any memory when creating an instance. This structure is good for creating types that have no state at all.

All.go files, except blank lines and comments, should declare the package they belong to on the first line. Each package is in a separate directory. You cannot put multiple packages in the same directory, nor can you split files from the same package into different directories. This means that all. Go files in the same directory must declare the same package name.

Packages in the standard library will be found where Go was installed. Packages created by Go developers are looked up in the directory specified by the GOPATH environment variable. These directories specified by GOPATH are the developer’s personal workspace. The compiler looks for the Go installation directory first, and then looks for the directories listed in the GOPATH variable in order.

Remote import

When the program is compiled with the import path, the go build command searches for the package on disk, using GOPATH’s Settings. In fact, the import path represents a URL to the repository on GitHub. If the path contains a URL, you can use the Go toolchain to get the package from the DVCS and save the source code for the package in the directory that matches the URL in the path that GOPATH points to. This is done using the go get command. Go Get gets the package at any specified URL, or any other package that an imported package depends on. Because of the recursive nature of Go Get, this command scans the source tree for a package and gets all the dependent packages it can find.

After the import

Used to solve the problem of the same package name. Named import refers to defining a name on the left of the package path given by the import statement and naming the imported package with the new name. For example, if the user has already used the FMT package in the standard library, and now wants to import the package named FMT in his own project, see the following code:

import (
    "fmt"
    myfmt "mylib/fmt"
)
Copy the code

Blank identifier

The underscore character (_) is called a blank identifier in Go and has many uses. This identifier is used to discard unwanted values, such as giving an empty name to an imported package, or ignoring values returned by a function that you are not interested in.

The init () function

Each package can contain any number of init functions that are called at the beginning of program execution. All init functions discovered by the compiler are scheduled to be executed before main. The init function is used to set up packages, initialize variables, or do other bootstrapping tasks that need to be done first before a program runs.

Run the code through the go run command.

The go vet command helps developers detect common errors in code, such as:

  1. The Printf class function was called with a type matching an incorrect argument.
  2. Error in method signature while defining common methods.
  3. Incorrect structure tag.
  4. Structure literals that do not specify field names.

This tool does a good job of catching some common errors. It’s a good practice to check code into the source code base every time you do go Vet.

Go code is formatted via the FMT command.

Go Doc can view documentation under related packages, such as Go Doc HTTP.

The developer starts his own document server by simply entering the following command in the terminal session: godoc-http =:6060 This command tells Godoc to start the Web server at port 6060. If your browser is already open, navigate to http://localhost:6060 to see a page containing all the Go standard libraries and documentation for the Go source code under your GOPATH.

  1. Packages are the basic unit of organization code in the Go language.
  2. The environment variable GOPATH determines where the Go source code is saved, compiled, and installed on disk.
  3. You can set a different GOPATH for each project to maintain isolation of source code and dependencies.
  4. The GO tool is the best tool for working on the command line.
  5. Developers can use Go Get to grab someone else’s package and install it into their GOPATH specified directory.
  6. Creating packages for others is as simple as putting the source code into a common code base and following a few simple rules.
  7. The Go language was designed to share code as a core feature and driving force of the language.
  8. Dependency management tools are recommended to manage dependencies.
  9. There are many community-developed dependency management tools such as GoDEp, Vender, and GB.

Chapter 4 arrays, Slicing, and mapping

The Go language has three data structures that allow users to manage collection data: arrays, slicing, and maps. Arrays are the basic data structures for slicing and mapping.

An internal implementation of an array

An array is a fixed-length data type used to store a contiguous block of elements of the same type. Arrays can be stored as built-in types, such as integers or strings, or as structural types.

Array memory is allocated continuously. Because memory is continuous, the CPU can cache data in use for longer periods of time, and access is also faster.

Array declaration and initialization

When you declare an array, you specify the type of data to store internally and the number of elements to store.

Var array [5]int // Declare an array of five integersCopy the code

Additionally, a quick way to create and initialize arrays is to use array literals. Array literals allow you to declare the number of elements in an array and specify the value of each element.

Array := [5]int{1,2,3,4,5} // declare an array of integers. Array := [5]int{1:10, 2:20} array := [5]int{1:10, 2:20} array := [5]int{1:10, 2:20}Copy the code

Multidimensional array

// Declare a two-dimensional array of integers, Var array [4][2]int // Use array literals to declare and initialize a two-dimensional array of integers := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}Copy the code

Passing the array between functions will do the copying, which is not good for memory and performance. A better approach is to use Pointers, passing the address of the array to the function. In this case, only 8 bytes of memory are allocated to the pointer. Be aware, of course, that since we are passing Pointers, if we change the value to which the pointer points, we will change the shared memory. As you can see, slicing is a better way to handle this kind of sharing.

Internal implementation and basic functions of slicing

Principle: SLicing is a data structure that facilitates the use and management of a collection of data. Slicing is built around the concept of dynamic arrays that can automatically grow and shrink as needed. Dynamic growth of slices is achieved through the built-in function Append. This function can grow slices quickly and efficiently. You can also reduce the size of a slice by slicing the slice again. Because the underlying memory of slicing is also allocated in contiguous blocks, slicing also benefits from indexing, iteration, and optimization for garbage collection.

A slice has a data structure with three fields: the pointer to the underlying array, the number of elements accessed by the slice (i.e. length), and the number of elements that the slice is allowed to grow to (i.e. capacity).

Creation and initialization of slices

Method 1: make and slice literals

Slice := make([]string, 5) // Make ([]string, 5) // Make ([]string, 5) Slice :=make([]int,3,5) // create string slice:= []string{"Red", "Blue", "Green", "Yellow", "Pink"}Copy the code

Remember that if you specify a value in the [] operator, you are creating an array, not a slice. Slices are created only if no value is specified

Array := [3]int{10, 20, 30} slice := []int{10, 20, 30}Copy the code

For slice[I :j] whose underlying array size is K

Length: J-I

Capacity: K-I

Slice growth

With Append, you need a slice to be manipulated and a value to append.

Note: Function Append handles the capacity growth of the underlying array intelligently. When the size of the slice is less than 1000 elements, it always increases exponentially. Once the number of elements exceeds 1000, the capacity growth factor is set to 1.25, which is a 25% increase in capacity each time. This growth algorithm may change as the language evolves.

The position function append is also a variable argument function. This means that multiple appending values can be passed in a single call. If using… Operator to append all elements of one slice to another.

S1 := []int{1, 2} s2 := []int{3, 4} FMT.Printf("%v\n", append(s1, s2...) ) Output: [1 2 3 4]Copy the code

For slices, len returns the length of the slice and cap returns the capacity of the slice

Pass slices between functions

On a machine with a 64-bit architecture, a slice requires 24 bytes of memory: 8 bytes for the pointer field and 8 bytes for the length and capacity fields. Since the data associated with the slice is contained in the underlying array and does not belong to the slice itself, copying the slice to any function does not affect the size of the underlying array. Copying only copies the slice itself, not the underlying array, and passing 24 bytes of data between functions is fast and easy. This is where slicing is efficient. There is no need to pass fingertips and deal with complex syntax, just copy the slice, modify the data the way you want, and pass back a new copy of the slice.

Internal implementation and basic functionality of the mapping

A map is a data structure used to store an unordered series of key-value pairs. The mapping stores values based on keys. The power of mapping is the ability to quickly retrieve data based on keys. Keys, like indexes, point to the value associated with that key. The mapping is unordered.

Internal implementation

Create and initialize

// Create a mapping, key type string, value type int dict:=make(map[string]int) Dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}Copy the code

The key of the map can be any value. The type of this value can be either a built-in type or a structural type, as long as the value can be compared using the == operator. Slices, functions, and structure types that contain slices cannot be used as keys of a map because they have reference semantics

There are two options for taking a value from the map. The first option is that you can get both the value and a flag indicating whether or not the key exists.

// Get the value of key Blue, exists := colors["Blue"] // Does this key exist? if exists { fmt.Println(value) }Copy the code

When a mapping is indexed by key, it always returns a value even if the key does not exist. In this case, a zero value for the type of the value is returned.

Pass mappings between functions

Passing a map between functions does not make a copy of the map. In fact, when a mapping is passed to a function and changes are made to the mapping, all references to the mapping are aware of the changes. This feature is similar to slicing and ensures that mappings can be copied at a fraction of the cost.

The summary of this chapter

  1. Arrays are the building blocks for constructing slices and maps.
  2. In Go, slicing is often used to deal with collections of data, and mapping is used to deal with data with key-value pair structures.
  3. The built-in function make creates slices and maps and specifies the original length and capacity. You can also use slicing and mapping literals directly, or use literals as initial values for variables.
  4. Slicing has a capacity limit, but it can be expanded using the built-in Append function.
  5. There is no capacity or limit to the growth of maps.
  6. The built-in function len can be used to get the length of a slice or map.
  7. The built-in function CAP can only be used for slicing.
  8. By combining, you can create multidimensional arrays and multidimensional slices. You can also use slices or other maps as values for maps. But slices cannot be used as a key for mapping.
  9. Passing slices or maps to functions costs little and does not copy the underlying data structure.

Chapter 5 type system of Go language

methods

Method can add new behavior to user-defined types. Methods are actually functions, but they are declared with an additional argument between the keyword func and the method name.

There are two types of receivers in Go: value receivers and pointer receivers

// notify func (u user) notify() {ftt.printf ("Sending user Email To %s<%s>\n", u.name, u.mail)}Copy the code

If a method is declared using a value receiver, the call is executed using a copy of the value.

Func (u *user) changeEmail(email string) {u.mail = email}Copy the code

The value receiver invokes the method using a copy of the value, while the pointer receiver invokes the method using the actual value.

Built-in types are a set of types provided by the language, and when you pass the value of these types to a method or function, you should pass a copy of the corresponding value.

Reference types. Go has several reference types: slice, map, channel, interface, and function types. Passing a copy of a reference type’s value by copying is essentially sharing the underlying data structure.

interface

Embedded type

The Go language allows users to extend or modify existing types of behavior. This functionality is important for code reuse, as well as when modifying existing types to fit new types. This is done via Type embedding. An embedded class declares an existing type directly into a new structure type. The embedded type is called the inner type of the new external type.

type user3 struct {
	name  string
	email string
}

type admin struct {
	user3
	level string
}
Copy the code

In the above code we declare a structure type named user and another structure type named admin. In the method declaring the admin type, we embed the user type in the admin type. To embed a type, you simply declare the name of the type. Once we embed the user type in admin, we can say that user is an internal type of the external type admin.

Public or undisclosed identifiers

When the name of an identifier begins with a lowercase letter, the identifier is not public, that is, not visible to code outside the package. If an identifier begins with a capital letter, the identifier is public, that is, visible to code outside the package.

summary

  1. A user-defined type can be declared using the keyword struct or by specifying an existing type.
  2. Method provides a way to add behavior to user-defined types.
  3. When designing a type, you need to determine whether the type is primitive or non-primitive in nature.
  4. Interfaces are types that declare a set of behaviors and support polymorphism.
  5. Embedded types provide the ability to extend types without using inheritance.
  6. The identifier is either exposed from the package or not exposed in the package.

Chapter 6 Concurrency

Parallelism in Go refers to the ability to make one function run independently of another. When a function is created as a Goroutine, Go treats it as a separate unit of work. The unit is scheduled to execute on the available logical processor.

3. The concurrent synchronization model of Go comes from a paradigm called Communicating Sequential Processes (CSP). CSP is a messaging model that delivers messages by passing data between goroutines, rather than locking data for synchronous access. The key data type used to synchronize and pass data between Goroutines is called channels.

Concurrency is not parallelism. Parallelism is having different pieces of code execute simultaneously on different physical handlers. The key to parallelism is doing a lot of things at the same time, and concurrency is managing a lot of things at the same time that might be only half done before being suspended to do something else. In many cases, concurrency is better than parallelism, because the total resources of the operating system and hardware are generally small, but it enables the system to do many things at once.

If two or more goroutines access a shared resource without synchronizing with each other and attempt to read and write to the resource at the same time, they are in a competing state, called racecandition. Read and write operations on a shared resource must be atomic; in other words, only one Goroutine can read and write to a shared resource at a time.

Lock shared resources

One way to fix the code and eliminate contention is to use the locking mechanism provided by the Go language to lock shared resources and keep goroutine synchronized. The Go language provides the traditional mechanism for synchronizing goroutine by locking shared resources. If you need to access an integer variable or a piece of code sequentially, the functions in the atomic and sync packages provide a good solution.

Atomic function

Atomic functions can access integer variables and Pointers synchronously with a very low-level locking mechanism.

For example, the atomic functions AddInt64, LoadInt64, and StoreInt64 in the Atmoic package.

The mutex

Mutex is used to create a critical section of code that only one Goroutine can execute at a time (Mutex).

Mutex.lock ()// Lock the critical area code mutex. Unlock () / / UnlockCopy the code

channel

When a resource needs to be shared between goroutines, channels provide a conduit between goroutines and a mechanism to ensure that data is exchanged synchronously. When declaring channels, you need to specify the type of data to be shared. Values or Pointers of built-in, named, structural, and reference types can be shared through channels. In Go you need to use the built-in function make to create a channel, for example:

Unbuffered := make(chan int) // Buffered := make(chan string, 10)Copy the code

Sending a value or pointer to a channel requires the <- operator

// Send a string buffered <- "Gopher" // receive a string value := <-buffered from the channelCopy the code

Note that <- is in the position of the channel, to the right when sending to the channel and to the left when receiving from the channel.

Unbuffered channels

A channel that does not have the ability to store any values before receiving. The send and receive Goroutine must be ready at the same time to complete the send and receive operation, otherwise the Goroutine will block.

There are buffered channels

A buffered channel is a channel that can store one or more values before they are received. This type of channel does not force goroutines to send and receive at the same time. The channel will block and the conditions for sending and receiving actions will be different. The receive action blocks only if there are no values to receive in the channel. The send action blocks only if the channel has no available buffer to hold the value being sent. This leads to a big difference between buffered and unbuffered channels: unbuffered channels guarantee that goroutines that send and receive will exchange data at the same time; Buffered channels have no such guarantee.

summary

  1. Concurrency means that goroutines run independently of each other.
  2. Use the keyword go to create a Goroutine to run the function.
  3. An oroutine executes on a logical processor, which has separate system threads and runqueues.
  4. A competing state is when two or more Goroutines attempt to access the same resource.
  5. Atomic functions and mutex provide a way to prevent competing states.
  6. Channels provide an easy way to share data between two Goroutines.
  7. Unbuffered channels guarantee simultaneous exchange of data, while buffered channels do not.

Chapter 7 Concurrency patterns

runner

The Runner package is used to show how channels can be used to monitor a program’s execution time, or to terminate a program if it takes too long. This pattern can be useful when developing programs that need to schedule background processing tasks.

summary

  1. Channels can be used to control the life cycle of a program.
  2. A SELECT statement with a default branch can be used to try to send or receive data to a channel without blocking.
  3. Buffered channels can be used to manage a set of reusable resources.
  4. The language runtime handles channel collaboration and synchronization.
  5. Use unbuffered channels to create goroutine pools that do the work.
  6. At any time, two Goroutines can exchange data using an unbuffered channel, ensuring that the other party receives the data when the channel operation is complete.

Chapter 8 Standard Library

The Go standard library is a set of core packages designed to extend and enhance the capabilities of the language. These packages add a lot of different types to the language.

log

A simple custom logger

package main import ( "io" "io/ioutil" "log" "os" ) var ( Trace *log.Logger Info *log.Logger Warning *log.Logger Error *log.Logger ) func init() { file, err := os.OpenFile("errors.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err ! = nil { log.Fatalln("Failed to open error log file:", err) } Trace = log.New(ioutil.Discard, "TRACE:", log.Ldate|log.Ltime|log.Lshortfile) Info = log.New(os.Stdout, "INFO:", log.Ldate|log.Ltime|log.Lshortfile) Warning = log.New(os.Stdout, "WARNING:", log.Ldate|log.Ltime|log.Lshortfile) Error = log.New(io.MultiWriter(file, os.Stderr), "ERROR:", log.Ldate|log.Ltime|log.Lshortfile) } func main() { Trace.Println("I have something standard to say") Info.Println("Special Information") Warning.Println("There is something you need to know about") Error.Println("Something has failed") }Copy the code

Encoding/decoding

Decoding JSON

Decode using json package’s NewDecoder function and Decode method.

Parse JSON data from the network

// Decode the JSON response to the structure type var gr gResponse err = json.newdecoder (resp.body).decode (&gr) if err! = nil { log.Println("ERROR:", err) return } fmt.Println(gr)Copy the code

Sometimes, the JSON document you need to process will exist as a string. In this case, the string needs to be converted into byte slices ([]byte) and deserialized using the Unmarshal function of the JSON package.

Var c Contact err := JSON.Unmarshal([]byte(JSON), &c) if err! = nil { log.Println("ERROR:", err) return }Copy the code

You can’t declare a structure type for the format of JSON, but you need a more flexible way to work with JSON documents. In this case, you can decode the JSON document into a map variable.

Var c map[string]interface{} err := JSON.Unmarshal([]byte(JSON), &c) if err! = nil { log.Println("ERROR:", err) return } fmt.Println("Name:", c["name"]) fmt.Println("Title:", c["title"]) fmt.Println("Contact") fmt.Println("H:", c["contact"].(map[string]interface{})["home"]) fmt.Println("C:", c["contact"].(map[string]interface{})["cell"])Copy the code

JSON encoding

The MarshalIndent function of the JSON package is used for encoding. This function makes it easy to convert values of map type or structure type from the Go language into JSON documents in an easy-to-read format.

data, err := json.MarshalIndent(c, "", " ") if err ! = nil { fmt.Println("ERROR:", err) return } fmt.Println(string(data))Copy the code

Chapter 9 testing and Performance

The go test command can be used to execute written test code. All you need to do is follow some rules to write tests. Furthermore, testing can be seamlessly integrated into code engineering and continuous integration systems.

Unit testing

There are two types of unit tests in Go:

  1. Basic tests use only a set of parameters and results to test a piece of code.
  2. A table test also tests a piece of code, but with multiple sets of parameters and results.

Note 1 The Go test tool considers only files ending in _test. Go as test files.

Note 2 A Test function must be a public function and begin with the word Test. Not only must the function name begin with Test, but the signature of the function must accept a pointer to testing.t and return no value.

Such as:

Func TestDownload(t * testing.t) const checkMark = "\u2713" const ballotX = "\u2717Copy the code

The standard library includes a package called HttpTest that lets developers mimic HTTP-based network calls.

func mockServer() *httptest.Server {
    f := func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Header().Set("Content-Type", "application/xml")
        fmt.Fprintln(w, feed)
    }
    return httptest.NewServer(http.HandlerFunc(f))
}
Copy the code

Benchmarking is a way to test the performance of your code. Benchmarks can be useful when you want to test the performance of different solutions to the same problem and see which solution performs better. Benchmarks can also be used to identify CPU or memory efficiency issues with a piece of code that can have a significant impact on overall application performance.