方法-学习Go语言

阅读笔记

methods.go

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
}

笔记

Go 没有类。不过你可以为结构体类型定义方法。

方法就是一类带特殊的 **接收者** 参数的函数。

方法接收者在它自己的参数列表内,位于 `func` 关键字和方法名之间。

在此例中,`Abs` 方法拥有一个名为 `v`,类型为 `Vertex` 的接收者。

1. 这不是面向对象的类的方法吗?是否根据函数名的大小写来区分权限public 和private?

methods-funcs.go

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(Abs(v))
}

笔记

记住:方法只是个带接收者参数的函数。

methods-continued.go

package main

import (
    "fmt"
    "math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt2)
    fmt.Println(f.Abs())
}

笔记

你也可以为非结构体类型声明方法。

在此例中,我们看到了一个带 Abs 方法的数值类型 MyFloat

你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。

(译注:就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。)

  1. MyFloat内部实现还是结构体吧,只是定义一个新的结构体别名
  2. 再为这个结构体添加新的方法。

methods-pointers.go

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    fmt.Println(v.Abs())
}

笔记

你可以为指针接收者声明方法。

这意味着对于某类型 `T`,接收者的类型可以用 `*T` 的文法。(此外,`T`不能是像 `*int` 这样的指针。)

例如,这里为 `*Vertex` 定义了 `Scale` 方法。

指针接收者的方法可以修改接收者指向的值(就像 `Scale` 在这做的)。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。

试着移除第 16 行 `Scale` 函数声明中的 `*`,观察此程序的行为如何变化。

若使用值接收者,那么 `Scale` 方法会对原始 `Vertex` 值的副本进行操作。(对于函数的其它参数也是如此。)`Scale` 方法必须用指针接受者来更改 `main` 函数中声明的 `Vertex` 的值。
  1. 什么是指针接受者?什么是值接受者?
  2. 这里有点类似原型模式了。在JavaScript可以为一个方法(对象)添加原型方法,后续大家都可以调用该方法。
  3. 在这里的函数Scale参数是指针的,修改其值会传导原始的值。

methods-pointers-explained.go

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    Scale(v, 10)
    fmt.Println(Abs(v))
}

笔记

  1. v *Vertex -> v Vertex
  2. 去掉参数里的*,参数就从指针传递变成值传递,这就意味着是否可以改变原始值
  3. 第2行函数,只是改变了副本的值,并没有改变原始值。

indirection.go

package main

import "fmt"

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(2)
    ScaleFunc(&v, 10)

    p := &Vertex{4, 3}
    p.Scale(3)
    ScaleFunc(p, 8)

    fmt.Println(v, p)
}

笔记

比较前两个程序,你大概会注意到带指针参数的函数必须接受一个指针:

var v Vertex
ScaleFunc(v, 5)  // 编译错误!
ScaleFunc(&v, 5) // OK
而以指针为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK
对于语句 v.Scale(5),即便 v 是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)。
  1. 感觉这里有坑,go语言自动帮你做了转换。
  2. 如果调用的方法里有一个指针的参数,go自动会取到值的引用当作函数的参数,去调用对应的函数。
  3. 第一个例子,先将值变大了2倍,然后扩大了10倍。
  4. 第二个例子,扩大了3倍。p是一个指针,可以直接传递到函数内部。

indirection-values.go

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
    fmt.Println(AbsFunc(v))

    p := &Vertex{4, 3}
    fmt.Println(p.Abs())
    fmt.Println(AbsFunc(*p))
}

笔记

同样的事情也发生在相反的方向。

接受一个值作为参数的函数必须接受一个指定类型的值:

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // 编译错误!
而以值为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs()。

思考
1. 什么是同样的事情?
2. 第二个例子里面,p是一个指向Vertex的指针,go自动会将p解释为(*p)。

methods-with-pointer-receivers.go

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := &Vertex{3, 4}
    fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
    v.Scale(5)
    fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

笔记

使用指针接收者的原因有二:

首先,方法能够修改其接收者指向的值。

其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。

在本例中,Scale 和 Abs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。
  1. 传值和传引用,根据使用情况不同的case进行选择。
  2. 在这个例子中Scale 和Abs都变成结构体的方法,并传递了引用,所以参数值会被改变,并一直传递下去。

参考链接

  1. 方法
  2. 方法即函数
  3. 方法续
  4. 指针接收者
  5. 指针与函数
  6. 方法与指针重定向
  7. 方法与指针重定向(续)
  8. 选择值或指针作为接收者

映射-学习Go语言

阅读笔记

maps.go

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m map[string]Vertex

func main() {
    m = make(map[string]Vertex)
    m["Bell Labs"] = Vertex{
        40.68433, -74.39967,
    }
    fmt.Println(m["Bell Labs"])
}

笔记

make 函数会返回给定类型的映射,并将其初始化备用。
1. m就是映射了,将key 映射到value。
2. 内部还是用hash实现的吗?
3. 开始有点类似PHP的数组味道了。

map-literals.go

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": Vertex{
        40.68433, -74.39967,
    },
    "Google": Vertex{
        37.42202, -122.08408,
    },
}

func main() {
    fmt.Println(m)
}

笔记

  1. 可以理解,初始化一个hash数组。
  2. key为字符串,value是Vertex 结构体

map-literals-continued.go

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

func main() {
    fmt.Println(m)
}

笔记

  1. 忽略类名的写法

mutaing-maps.go

package main

import "fmt"

func main() {
    m := make(map[string]int)

    m["Answer"] = 42
    fmt.Println("The value:", m["Answer"])

    m["Answer"] = 48
    fmt.Println("The value:", m["Answer"])

    delete(m, "Answer")
    fmt.Println("The value:", m["Answer"])

    v, ok := m["Answer"]
    fmt.Println("The value:", v, "Present?", ok)
}

笔记

m[key] = elem
elem = m[key]
delete(m, key)

elem, ok = m[key]

  1. 数组的操作方式

参考链接

  1. 映射
  2. 映射的文法
  3. 映射的文法 续
  4. 修改映射

切片-学习Go语言

阅读笔记

slices.go

package main

import "fmt"

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}

    var s []int = primes[1:4]
    fmt.Println(s)
}

笔记

切片的内部实现,类似一个链表+头尾指针。

数组的切片范围为low<= x < high

slices-pointers.go

package main

import "fmt"

func main() {
    names := [4]string{
        "John",
        "Paul",
        "George",
        "Ringo",
    }
    fmt.Println(names)

    a := names[0:2]
    b := names[1:3]
    fmt.Println(a, b)

    b[0] = "XXX"
    fmt.Println(a, b)
    fmt.Println(names)
}

笔记

切片的数据被引用的。修改切片的数据会直接关联到原来的数组。

slice-literals.go

package main

import "fmt"

func main() {
    q := []int{2, 3, 5, 7, 11, 13}
    fmt.Println(q)

    r := []bool{true, false, true, true, false, true}
    fmt.Println(r)

    s := []struct {
        i int
        b bool
    }{
        {2, true},
        {3, false},
        {5, true},
        {7, true},
        {11, false},
        {13, true},
    }
    fmt.Println(s)
}

笔记

  1. 创建长度为6的int数组,构建一个引用该数组的切片。
  2. 创建长度为6的布尔数组,构建一个引用该数组的切片。
  3. 创建长度为5的结构体数组,构建一个引用该数组的切片。

slice-bounds.go

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}

    s = s[1:4]
    fmt.Println(s)

    s = s[:2]
    fmt.Println(s)

    s = s[1:]
    fmt.Println(s)
}

笔记

  1. 切片的默认值为0到数组长度。
  2. 以数组var a [10]int为例
  3. 切片等价a[0:10] = a[:10] = a[0:] = a[:]

slice-len-cap.go

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // Slice the slice to give it zero length.
    s = s[:0]
    printSlice(s)

    // Extend its length.
    s = s[:4]
    printSlice(s)

    // Drop its first two values.
    s = s[2:]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

笔记

  1. 如何理解切片的长度和容量。按照正常逻辑不是只有长度就够了吗?为啥还要新增一个容量呢?
  2. 切片的长度就是它所包含的元素个数。
  3. 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
  4. 注意看第二个例子,长度变为0 ,但是容量为什么还是6呢?
  5. 注意第四个例子,是不是起始值变了,才会去修改引用数组的容量?

nil-slices.go

package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}

笔记

  1. 切片的零值是 nil
  2. nil 切片的长度和容量为 0 且没有底层数组。

making-slices.go

package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a)

    b := make([]int, 0, 5)
    printSlice("b", b)

    c := b[:2]
    printSlice("c", c)

    d := c[2:5]
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

笔记

  1. 默认创建len 和cap 相同的切片
  2. a := make([]int, 5) // len(a)=5 cap(a)=5
  3. 注意第四个例子,cap变成了3
  4. 假如d1 := c[3:5],那么cap会变成2
  5. 切片的最大值不能超过原始数组的长度,不然会报错。

slices-of-slice.go

package main

import (
    "fmt"
    "strings"
)

func main() {
    // Create a tic-tac-toe board.
    board := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }

    // The players take turns.
    board[0][0] = "X"
    board[2][2] = "O"
    board[1][2] = "X"
    board[1][0] = "O"
    board[0][2] = "X"

    for i := 0; i < len(board); i++ {
        fmt.Printf("%s\n", strings.Join(board[i], " "))
    }
}

笔记

  1. 这尼玛不是就二维数组了吗?
  2. 创建了一个二维数组的切片,也就是切片的切片

append.go

package main

import "fmt"

func main() {
    var s []int
    printSlice(s)

    // append works on nil slices.
    s = append(s, 0)
    printSlice(s)

    // The slice grows as needed.
    s = append(s, 1)
    printSlice(s)

    // We can add more than one element at a time.
    s = append(s, 2, 3, 4)
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

笔记

  1. func append(s []T, vs …T) []T 函数原型

  2. 注意第三次追加,cap自动扩充到2^ 3 = 8

  3. 那如果再超过当前的cap呢

  4. 比如 s = append(s, 5,6,7,8),cap会自动扩容到2^4 = 16

  5. len=9 cap=16 [0 1 2 3 4 5 6 7 8]
    
  6. 这难道底层是采用hash+链表实现的吗?还有自动扩容这一说?

  7. Go 切片:用法和本质

range.go

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}

笔记

  1. 这不就是PHP数组的遍历,键值对吗?

rang-continued.go

package main

import "fmt"

func main() {
    pow := make([]int, 10)
    for i := range pow {
        pow[i] = 1 << uint(i) // == 2**i
    }
    for _, value := range pow {
        fmt.Printf("%d\n", value)
    }
    for value := range pow {
        fmt.Printf("%d\n", value)
    }
    for key, _ := range pow {
        fmt.Printf("%d\n", key)
    }
}

笔记

  1. 输出键值对的方法。

思考总结

操作数组切片的方法。
1. make
2. append

参考链接

  1. 切片
  2. 切片就像数组的引用
  3. 切片文法
  4. 切片的默认行为
  5. 切片的长度与容量
  6. nil 切片
  7. 用 make 创建切片
  8. 切片的切片
  9. 向切片追加元素
  10. Range
  11. Range续