Xiayf的技术笔记

《Effective Go》学习笔记

类型切换

可以使用switch去发现一个接口变量的动态类型。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
  default:
  fmt.Printf("unexpected type %T", t)  // %T打印t的类型
  case bool:
  fmt.Printf("boolean %t\n", t)
  case int:
  fmt.Printf("integer %d\n", t)
  case *bool:
  fmt.Printf("pointer to boolean %t\n", *t)
  case *int:
  fmt.Printf("pointer to integer %d\n", *t)
}

Defer

Go's defer statement schedules a function call (the deferred function) to be run immediately before the function executing the defer returns. 该方法显得不同常规,但却是处理一些情况的有效方式,如无论函数怎样返回,都必须进行资源释放。

数组

Go和C中的数组的主要区别在于,在Go中,

  • 数组是值。将一个数组赋值给另一个将复制其所有的元素。
  • 特别地,如果你传递一个数组给函数,它将收到此数组的一个副本,而不是一个指向它的指针。
  • 数组的尺寸是其类型的一部分。[10]int和[20]int是不同的类型。

数组是值的属性很有用,但代价昂贵; 如果你想要类似C的行为和效率,可以传递一个指向数组的指针。但这种风格并不是Go的习惯用法,切片才是。

切片(slice)

切片通过包装数组而给出了对数据序列的通用、强大和方便的接口。多数情况下,Go中的数组编程是通过切片而非简单数组来完成的。

切片保存了对其底层数组的引用,如果你将一个切片赋值给另外一个,这两个切片将引用同一个底层数组。

常量

Go中的常量就是不可变常数,它们在编译时被创建,即便在函数中定义的局部常量也是如此,常量只能是数字、字符(rune)、字符串或布尔值。由于编译时的限制,定义它们的表达式必须是可以被编译器求值的常量表达式。

init函数

Pointer vs. Values

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers. This is because pointer methods can modify the receiver; invoking them on a copy of the value would cause those modifications to be discarded.

Interface conversions and type assertions

A type assertion takes an interface value and extracts from it a value of the specified explicit type. :

value.(typeName)

for example, to extract the string we know is in the value, we could write:

str := value.(string)

But if it turns out that the value does not contain a string, the program will crash with a run-time error. To guard against that, use the "comma, ok" idiom to test, safely, whether the value is a string:

str, ok := value.(string)
if ok {
  fmt.Printf("string value is: %q\n", str)
} else {
  fmt.Printf("value is not a string\n")
}

if the type assertion fails, str will still exist and be of type string, but it will have the zero value, an empty string.

Interfaces and methods

Since almost anything can have methods attached, almost anything can satisfy an interface. One illustrative example is in the http package, which defines the Handler interface. Any object that implements Handler can serve HTTP requests.

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter is itself an interface that provides access to the methods needed to return the response to the client. Those methods include the standard Write method, so an http.ResponseWriter can be used wherever an io.Writer can be used. Request is a struct containing a parsed representation of the request from the client.

Import for side effect

Sometimes it is useful to import a package only for its side effects, without any explicit use. For example, during its init function, the net/http/pprof package registers HTTP handlers that provide debugging information. It has an exported API, but most clients need only the handler registration and access the data through a web page. To import the package only for its side effects, rename the package to the blank identifier:

import _ "net/http/pprof"

This form of import makes clear that the package is being imported for its side effects, because there is no other possible use of the package.

Embedding

Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to "borrow" pieces of an implementation by embedding types within a struct or interface.

type Reader interface {
  Read(p []byte) (n int, err error)
}
type Writer interface {
  Write(p []byte) (n int, err error)
}
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
  Reader
  Writer
}

Only interfaces can be embedded within interfaces.

The same basic idea applies to structs, but with more far-reaching implications.

// ReadWriter stores pointers to a Reader and a Writer
// It implements io.ReadWriter.
type ReadWriter struct {
  *Reader    // *bufio.Reader
  *Writer    // *bufio.Writer
}

There's an important way in which embedding differs from subclassing. When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one.

Embedding types introduces the problem of name conflicts but the rules to resolve them are simple. First, a field or method X hides any other item X in a more deeply nested part of the type. If log.Logger contained a field or method called Command, the Command field of Job would dominate it.

Second, if the same name appears at the same nesting level, it is usually an error; it would be erroneous to embed log.Logger if the Job struct contained another field or method called Logger. However, if the duplicate name is never mentioned in the program outside the type definition, it is OK.

Concurrency

Share by communicating

Go encourages a different approach in which shared values are passed around on channels and, in fact, never actively shared by separate threads of execution. Only one goroutine has access to the value at any given time. Data races cannot occur, by design. To encourage this way of thinking we have reduced it to a slogan:

Do not communicate by sharing memory; instead, share memory by communicating.

Goroutines

A goroutine has a simple model: it is a function executing concurrently with other goroutines in the same address space.

Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run.

A function literal can be handy in a goroutine invocation.

func Announce(message string, delay time.Duration) {
  go func() {
    time.Sleep(delay)
    fmt.Println(message)
  }()  // Note the parentheses - must call the function.
}

In Go, function literals are closures: the implementation makes sure the variables referred to by the function survive as long as they are active.