golang基础语法-2

哈哥_ · · 568 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

该篇接着上一篇延续,是一个完整的系列

第 5 章 方法

5.1 方法定义

方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。

  • 只能为当前包内命名类型定义方法。
  • 参数 receiver 可任意命名。如方法中未曾使用,可省略参数名。
  • 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
  • 不支持方法重载,receiver 只是参数签名的组成部分。
  • 可用实例 value 或 pointer 调用全部方法,编译器自动转换。

没有构造和析构方法,通常用简单工厂模式返回对象实例。

type Queue struct {
    elements []interface{}
}

func NewQueue() *Queue { // 创建对象实例。
    return &Queue{make([]interface{}, 10)}
}

func (*Queue) Push(e interface{}) error { // 省略 receiver 参数名。
    panic("not implemented")
}

// func (Queue) Push(e int) error { // Error: method redeclared: Queue.Push
// panic("not implemented")
// }

func (self *Queue) length() int { // receiver 参数名可以是 self、this 或其他。
    return len(self.elements)
}

方法不过是一种特殊的函数,只需将其还原,就知道 receiver T 和 *T 的差别。

type Data struct{
    x int
}

func (self Data) ValueTest() { // func ValueTest(self Data);
    fmt.Printf("Value: %p\n", &self)
}

func (self *Data) PointerTest() { // func PointerTest(self *Data);
    fmt.Printf("Pointer: %p\n", self)
}

func main() {
    d := Data{}
    p := &d
    fmt.Printf("Data: %p\n", p)

    d.ValueTest() // ValueTest(d)
    d.PointerTest() // PointerTest(&d)

    p.ValueTest() // ValueTest(*p)
    p.PointerTest() // PointerTest(p)
}

输出:

Data : 0x2101ef018
Value : 0x2101ef028
Pointer: 0x2101ef018
Value : 0x2101ef030
Pointer: 0x2101ef018

从1.4开始,不再支持多级指针查找方法成员。

type X struct{}

func (*X) test() {
    println("X.test")
}

func main() {
    p := &X{}
    p.test()

    // Error: calling method with receiver &p (type **X) requires explicit dereference
    // (&p).test()
}

5.2 匿名字段

可以像字段成员那样访问匿名字段方法,编译器负责查找。

type User struct {
    id int
    name string
}

type Manager struct {
    User
}

func (self *User) ToString() string { // receiver = &(Manager.User)
    return fmt.Sprintf("User: %p, %v", self, self)
}

func main() {
    m := Manager{User{1, "Tom"}}

    fmt.Printf("Manager: %p\n", &m)
    fmt.Println(m.ToString())
}

输出:

Manager: 0x2102281b0
User : 0x2102281b0, &{1 Tom}

通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 "override"。

type User struct {
    id int
    name string
}

type Manager struct {
    User
    title string
}

func (self *User) ToString() string {
    return fmt.Sprintf("User: %p, %v", self, self)
}

func (self *Manager) ToString() string {
    return fmt.Sprintf("Manager: %p, %v", self, self)
}

func main() {
    m := Manager{User{1, "Tom"}, "Administrator"}

    fmt.Println(m.ToString())
    fmt.Println(m.User.ToString())
}

输出:

Manager: 0x2102271b0, &{{1 Tom} Administrator}
User : 0x2102271b0, &{1 Tom}

5.3 方法集

每个类型都有与之关联的方法集,这会影响到接口实现规则。

  • 类型 T 方法集包含全部 receiver T 方法。
  • 类型 *T 方法集包含全部 receiver T + *T 方法。
  • 如类型 S 包含匿名字段 T,则 S 方法集包含 T 方法。
  • 如类型 S 包含匿名字段 *T,则 S 方法集包含 T + *T 方法。
  • 不管嵌入 T 或 T,S 方法集总是包含 T + *T 方法。

用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。

5.4 表达式

根据调用者不同,方法分为两种表现形式:

instance.method(args...) ---> <type>.func(instance, args...)

前者称为 method value,后者 method expression

两者都可像普通函数那样赋值和传参,区别在于 method value 绑定实例,而 method expression 则须显式传参。

type User struct {
    id int
    name string
}

func (self *User) Test() {
    fmt.Printf("%p, %v\n", self, self)
}

func main() {
    u := User{1, "Tom"}
    u.Test()

    mValue := u.Test
    mValue() // 隐式传递 receiver

    mExpression := (*User).Test
    mExpression(&u) // 显式传递 receiver
}

输出:

0x210230000, &{1 Tom}
0x210230000, &{1 Tom}
0x210230000, &{1 Tom}

需要注意,method value 会复制 receiver

type User struct {
    id int
    name string
}

func (self User) Test() {
    fmt.Println(self)
}

func main() {
    u := User{1, "Tom"}
    mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。

    u.id, u.name = 2, "Jack"
    u.Test()

    mValue()
}

输出:

{2 Jack}
{1 Tom}

在汇编层面,method value 和闭包的实现方式相同,实际返回 FuncVal 类型对象。

FuncVal { method_address, receiver_copy }

可依据方法集转换 method expression,注意 receiver 类型的差异。

type User struct {
    id int
    name string
}

func (self *User) TestPointer() {
    fmt.Printf("TestPointer: %p, %v\n", self, self)
}

func (self User) TestValue() {
    fmt.Printf("TestValue: %p, %v\n", &self, self)
}

func main() {
    u := User{1, "Tom"}
    fmt.Printf("User: %p, %v\n", &u, u)

    mv := User.TestValue
    mv(u)

    mp := (*User).TestPointer
    mp(&u)

    mp2 := (*User).TestValue // *User 方法集包含 TestValue。
    mp2(&u)                  // 签名变为 func TestValue(self *User)。
}                            // 实际依然是 receiver value copy。

输出:

User : 0x210231000, {1 Tom}
TestValue : 0x210231060, {1 Tom}
TestPointer: 0x210231000, &{1 Tom}
TestValue : 0x2102310c0, {1 Tom}

将方法 "还原" 成函数,就容易理解下面的代码了。

type Data struct{}

func (Data) TestValue() {}
func (*Data) TestPointer() {}

func main() {
    var p *Data = nil
    p.TestPointer()

    (*Data)(nil).TestPointer() // method value
    (*Data).TestPointer(nil) // method expression

    // p.TestValue() // invalid memory address or nil pointer dereference
    // (Data)(nil).TestValue() // cannot convert nil to type Data
    // Data.TestValue(nil) // cannot use nil as type Data in function argument
}

第 6 章 接口

6.1 接口定义

接口是一个或多个方法签名的集合,任何类型的方法集中只要拥有与之对应的全部方法,就表示它 "实现" 了该接口,无须在该类型上显式添加接口声明。

所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。当然,该类型还可以有其他方法。

  • 接口命名习惯以 er 结尾,结构体。
  • 接口只有方法签名,没有实现。
  • 接口没有数据字段。
  • 可在接口中嵌入其他接口。
  • 类型可实现多个接口。
type Stringer interface {
    String() string
}

type Printer interface {
    Stringer // 接口嵌入。
    Print()
}

type User struct {
    id int
    name string
}

func (self *User) String() string {
    return fmt.Sprintf("user %d, %s", self.id, self.name)
}

func (self *User) Print() {
    fmt.Println(self.String())
}

func main() {
    var t Printer = &User{1, "Tom"} // *User 方法集包含 String、Print。
    t.Print()
}

输出:

user 1, Tom

空接口 interface{} 没有任何方法签名,也就意味着任何类型都实现了空接口。其作用类似面向对象语言中的根对象 object

func Print(v interface{}) {
    fmt.Printf("%T: %v\n", v, v)
}

func main() {
    Print(1)
    Print("Hello, World!")
}

输出:

int: 1
string: Hello, World!

匿名接口可用作变量类型,或结构成员。

type Tester struct {
    s interface {
        String() string
    }
}

type User struct {
    id int
    name string
}

func (self *User) String() string {
    return fmt.Sprintf("user %d, %s", self.id, self.name)
}

func main() {
    t := Tester{&User{1, "Tom"}}
    fmt.Println(t.s.String())
}

输出:

user 1, Tom

6.2 执行机制

接口对象由接口表 (interface table) 指针和数据指针组成。

runtime.h

struct Iface
{
    Itab* tab;
    void* data;
};

struct Itab
{
    InterfaceType* inter;
    Type* type;
    void (*fun[])(void);
};

接口表存储元数据信息,包括接口类型、动态类型,以及实现接口的方法指针。无论是反射还是通过接口调用方法,都会用到这些信息。

数据指针持有的是目标对象的只读复制品,复制完整对象或指针。

type User struct {
    id int
    name string
}

func main() {
    u := User{1, "Tom"}
    var i interface{} = u

    u.id = 2
    u.name = "Jack"

    fmt.Printf("%v\n", u)
    fmt.Printf("%v\n", i.(User))
}

输出:

{2 Jack}
{1 Tom}

接口转型返回临时对象,只有使用指针才能修改其状态。

type User struct {
    id int
    name string
}

func main() {
    u := User{1, "Tom"}
    var vi, pi interface{} = u, &u

    // vi.(User).name = "Jack" // Error: cannot assign to vi.(User).name
    pi.(*User).name = "Jack"

    fmt.Printf("%v\n", vi.(User))
    fmt.Printf("%v\n", pi.(*User))
}

输出:

{1 Tom}
&{1 Jack}

只有 tab 和 data 都为 nil 时,接口才等于 nil。

var a interface{} = nil // tab = nil, data = nil
var b interface{} = (*int)(nil) // tab 包含 *int 类型信息, data = nil

type iface struct {
    itab, data uintptr
}

ia := *(*iface)(unsafe.Pointer(&a))
ib := *(*iface)(unsafe.Pointer(&b))

fmt.Println(a == nil, ia)
fmt.Println(b == nil, ib, reflect.ValueOf(b).IsNil())

输出:

true {0 0}
false {505728 0} true

6.3 接口转换
利用类型推断,可判断接口对象是否某个具体的接口或类型。

type User struct {
    id int
    name string
}

func (self *User) String() string {
    return fmt.Sprintf("%d, %s", self.id, self.name)
}

func main() {
    var o interface{} = &User{1, "Tom"}

    if i, ok := o.(fmt.Stringer); ok { // ok-idiom
        fmt.Println(i)
    }

    u := o.(*User)
    // u := o.(User) // panic: interface is *main.User, not main.User
    fmt.Println(u)
}

还可用 switch 做批量类型判断,不支持 fallthrough。

func main() {
    var o interface{} = &User{1, "Tom"}

    switch v := o.(type) {
    case nil: // o == nil
        fmt.Println("nil")
    case fmt.Stringer: // interface
        fmt.Println(v)
    case func() string: // func
        fmt.Println(v())
    case *User: // *struct
        fmt.Printf("%d, %s\n", v.id, v.name)
    default:
    fmt.Println("unknown")
    }
}

超集接口对象可转换为子集接口,反之出错。

type Stringer interface {
    String() string
}

type Printer interface {
    String() string
    Print()
}

type User struct {
    id int
    name string
}

func (self *User) String() string {
    return fmt.Sprintf("%d, %v", self.id, self.name)
}

func (self *User) Print() {
    fmt.Println(self.String())
}

func main() {
    var o Printer = &User{1, "Tom"}
    var s Stringer = o
    fmt.Println(s.String())
}

6.4 接口技巧
让编译器检查,以确保某个类型实现接口。

var _ fmt.Stringer = (*Data)(nil)

某些时候,让函数直接 "实现" 接口能省不少事。

type Tester interface {
    Do()
}

type FuncDo func()
func (self FuncDo) Do() { self() }

func main() {
    var t Tester = FuncDo(func() { println("Hello, World!") })
    t.Do()
}

第 7 章 并发
7.1 Goroutine
Go 在语言层面对并发编程提供支持,一种类似协程,称作 goroutine 的机制。

只需在函数调用语句前添加 go 关键字,就可创建并发执行单元。开发人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。goroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务。

事实上,入口函数 main 就以 goroutine 运行。另有与之配套的 channel 类型,用以实现 "以通讯来共享内存" 的 CSP 模式。相关实现细节可参考本书第二部分的源码剖析。

go func() {
    println("Hello, World!")
}()

调度器不能保证多个 goroutine 执行次序,且进程退出时不会等待它们结束。

默认情况下,进程启动后仅允许一个系统线程服务于 goroutine。可使用环境变量或标准库函数 runtime.GOMAXPROCS 修改,让调度器用多个线程实现多核并行,而不仅仅是并发。

func sum(id int) {
    var x int64
    for i := 0; i < math.MaxUint32; i++ {
        x += int64(i)
    }

    println(id, x)
}

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(2)

    for i := 0; i < 2; i++ {
        go func(id int) {
            defer wg.Done()
            sum(id)
        }(i)
    }

    wg.Wait()
}

输出:

$ go build -o test

$ time -p ./test

0 9223372030412324865
1 9223372030412324865

real 7.70 // 程序开始到结束时间差 (非 CPU 时间)
user 7.66 // 用户态所使用 CPU 时间片 (多核累加)
sys 0.01 // 内核态所使用 CPU 时间片

$ GOMAXPROCS=2 time -p ./test

0 9223372030412324865
1 9223372030412324865

real 4.18
user 7.61 // 虽然总时间差不多,但由 2 个核并行,real 时间自然少了许多。
sys 0.02
调用 runtime.Goexit 将立即终止当前 goroutine 执行,调度器确保所有已注册 defer 延迟调用被执行。
func main() {
    wg := new(sync.WaitGroup)
    wg.Add(1)

    go func() {
        defer wg.Done()
        defer println("A.defer")

        func() {
            defer println("B.defer")
            runtime.Goexit() // 终止当前 goroutine
            println("B") // 不会执行
        }()

        println("A") // 不会执行
    }()

    wg.Wait()
}

输出:

B.defer
A.defer

和协程 yield 作用类似,Gosched 让出底层线程,将当前 goroutine 暂停,放回队列等待下次被调度执行。

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(2)

    go func() {
        defer wg.Done()

        for i := 0; i < 6; i++ {
            println(i)
            if i == 3 { runtime.Gosched() }
        }
    }()

    go func() {
        defer wg.Done()
        println("Hello, World!")
    }()

    wg.Wait()
}

输出:

$ go run main.go
0
1
2
3
Hello, World!
4
5

7.2 Channel
引用类型 channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

默认为同步模式,需要发送和接收配对。否则会被阻塞,直到另一方准备好后被唤醒。

func main() {
    data := make(chan int) // 数据交换队列
    exit := make(chan bool) // 退出通知

    go func() {
        for d := range data { // 从队列迭代接收数据,直到 close 。
            fmt.Println(d)
        }

        fmt.Println("recv over.")
        exit <- true // 发出退出通知。
    }()

    data <- 1 // 发送数据。
    data <- 2
    data <- 3
    close(data) // 关闭队列。

    fmt.Println("send over.")
    <-exit // 等待退出通知。
}

输出:

1
2
3
send over.
recv over.

异步方式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空,接收被阻塞。

通常情况下,异步 channel 可减少排队阻塞,具备更高的效率。但应该考虑使用指针规避大对象拷贝,将多个元素打包,减小缓冲区大小等。

func main() {
    data := make(chan int, 3) // 缓冲区可以存储 3 个元素
    exit := make(chan bool)

    data <- 1 // 在缓冲区未满前,不会阻塞。
    data <- 2
    data <- 3

    go func() {
        for d := range data { // 在缓冲区未空前,不会阻塞。
            fmt.Println(d)
        }

        exit <- true
    }()

    data <- 4 // 如果缓冲区已满,阻塞。
    data <- 5
    close(data)

    <-exit
}

缓冲区是内部属性,并非类型构成要素。

var a, b chan int = make(chan int), make(chan int, 3)

除用 range 外,还可用 ok-idiom 模式判断 channel 是否关闭。

for {
    if d, ok := <-data; ok {
        fmt.Println(d)
    } else {
        break
    }
}

向 closed channel 发送数据引发 panic 错误,接收立即返回零值。而 nil channel,无论收发都会被阻塞。

内置函数 len 返回未被读取的缓冲元素数量,cap 返回缓冲区大小。

d1 := make(chan int)
d2 := make(chan int, 3)

d2 <- 1

fmt.Println(len(d1), cap(d1)) // 0 0
fmt.Println(len(d2), cap(d2)) // 1 3

7.2.1 单向
可以将 channel 隐式转换为单向队列,只收或只发。

c := make(chan int, 3)

var send chan<- int = c // send-only
var recv <-chan int = c // receive-only

send <- 1
// <-send // Error: receive from send-only type chan<- int

<-recv
// recv <- 2 // Error: send to receive-only type <-chan int
不能将单向 channel 转换为普通 channel。

d := (chan int)(send) // Error: cannot convert type chan<- int to type chan int
d := (chan int)(recv) // Error: cannot convert type <-chan int to type chan int

7.2.2 选择
如果需要同时处理多个 channel,可使用 select 语句。它随机选择一个可用 channel 做收发操作,或执行 default case。

func main() {
    a, b := make(chan int, 3), make(chan int)

    go func() {
        v, ok, s := 0, false, ""

        for {
            select { // 随机选择可用 channel,接收数据。
            case v, ok = <-a: s = "a"
            case v, ok = <-b: s = "b"
            }

            if ok {
                fmt.Println(s, v)
            } else {
                os.Exit(0)
            }
        }
    }()

    for i := 0; i < 5; i++ {
        select { // 随机选择可用 channel,发送数据。
        case a <- i:
        case b <- i:
        }
    }

    close(a)
    select {} // 没有可用 channel,阻塞 main goroutine。
}

输出:

b 3
a 0
a 1
a 2
b 4

在循环中使用 select default case 需要小心,避免形成洪水。

7.2.3 模式
用简单工厂模式打包并发任务和 channel。

func NewTest() chan int {
    c := make(chan int)
    rand.Seed(time.Now().UnixNano())

    go func() {
        time.Sleep(time.Second)
        c <- rand.Int()
    }()

    return c
}

func main() {
    t := NewTest()
    println(<-t) // 等待 goroutine 结束返回。
}

用 channel 实现信号量 (semaphore)。

func main() {
    wg := sync.WaitGroup{}
    wg.Add(3)
    sem := make(chan int, 1)

    for i := 0; i < 3; i++ {
        go func(id int) {
            defer wg.Done()

            sem <- 1 // 向 sem 发送数据,阻塞或者成功。

            for x := 0; x < 3; x++ {
                fmt.Println(id, x)
            }

            <-sem // 接收数据,使得其他阻塞 goroutine 可以发送数据。
        }(i)
    }

    wg.Wait()
}

输出:

$ GOMAXPROCS=2 go run main.go
0 0
0 1
0 2
1 0
1 1
1 2
2 0
2 1
2 2

用 closed channel 发出退出通知。

func main() {
    var wg sync.WaitGroup
    quit := make(chan bool)

    for i := 0; i < 2; i++ {
        wg.Add(1)

        go func(id int) {
            defer wg.Done()

            task := func() {
                println(id, time.Now().Nanosecond())
                time.Sleep(time.Second)
            }

            for {
                select {
                case <-quit: // closed channel 不会阻塞,因此可用作退出通知。
                    return
                default: // 执行正常任务。
                    task()
                }
            }
        }(i)
    }

    time.Sleep(time.Second * 5) // 让测试 goroutine 运行一会。

    close(quit) // 发出退出通知。
    wg.Wait()
}

用 select 实现超时 (timeout)。

func main() {
    w := make(chan bool)
    c := make(chan int, 2)

    go func() {
        select {
        case v := <-c: fmt.Println(v)
        case <-time.After(time.Second * 3): fmt.Println("timeout.")
        }

        w <- true
    }()

    // c <- 1 // 注释掉,引发 timeout。
    <-w
}
channel 是第一类对象,可传参 (内部实现为指针) 或者作为结构成员。

type Request struct {
    data []int
    ret chan int
}

func NewRequest(data ...int) *Request {
    return &Request{ data, make(chan int, 1) }
}

func Process(req *Request) {
    x := 0
    for _, i := range req.data {
        x += i
    }

    req.ret <- x
}

func main() {
    req := NewRequest(10, 20, 30)
    Process(req)
    fmt.Println(<-req.ret)
}

第 8 章 包

8.1 工作空间编译工具对源码目录有严格要求,每个工作空间 (workspace) 必须由 bin、pkg、src 三个目录组成。

image

可在 GOPATH 环境变量列表中添加多个工作空间,但不能和 GOROOT 相同。

export GOPATH=$HOME/projects/golib:$HOME/projects/go

通常 go get 使用第一个工作空间保存下载的第三方库。

8.2 源文件

编码:源码文件必须是 UTF-8 格式,否则会导致编译器出错。 结束:语句以 ";" 结束,多数时候可以省略。 注释:支持 "//"、"/**/" 两种注释方式,不能嵌套。 命名:采用 camelCasing 风格,不建议使用下划线。

8.3 包结构

所有代码都必须组织在 package 中。

  • 源文件头部以 "package <name style="box-sizing: border-box; padding: 0px; margin: 0px;">" 声明包名称。</name>
  • 包由同一目录下的多个源码文件组成。
  • 包名类似 namespace,与包所在目录名、编译文件名无关。
  • 目录名最好不用 main、all、std 这三个保留名称。
  • 可执行文件必须包含 package main,入口函数 main。
说明:os.Args 返回命令行参数,os.Exit 终止进程。
     要获取正确的可执行文件路径,可用 filepath.Abs(exec.LookPath(os.Args[0]))。

包中成员以名称首字母大小写决定访问权限。

  • public: 首字母大写,可被包外访问。
  • internal: 首字母小写,仅包内成员可以访问。
  • 该规则适用于全局变量、全局常量、类型、结构字段、函数、方法等。

8.3.1 导入包

使用包成员前,必须先用 import 关键字导入,但不能形成导入循环。

import "相对目录/包主文件名"

相对目录是指从 <workspace style="box-sizing: border-box; padding: 0px; margin: 0px;">/pkg/<os_arch style="box-sizing: border-box; padding: 0px; margin: 0px;"> 开始的子目录,以标准库为例:</os_arch></workspace>

import "fmt" -> /usr/local/go/pkg/darwin_amd64/fmt.a
import "os/exec" -> /usr/local/go/pkg/darwin_amd64/os/exec.a

在导入时,可指定包成员访问方式。比如对包重命名,以避免同名冲突。

import "yuhen/test" // 默认模式: test.A
import M "yuhen/test" // 包重命名: M.A
import . "yuhen/test" // 简便模式: A
import _ "yuhen/test" // 非导入模式: 仅让该包执行初始化函数。

未使用的导入包,会被编译器视为错误 (不包括 "import _")。

./main.go:4: imported and not used: "fmt"

对于当前目录下的子包,除使用默认完整导入路径外,还可使用 local 方式。

image

main.go

import "learn/test" // 正常模式
import "./test" // 本地模式,仅对 go run main.go 有效。

8.3.2 自定义路径

可通过 meta 设置为代码库设置自定义路径。

server.go

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, `<meta name="go-import"
        content="test.com/qyuhen/test git https://github.com/qyuhen/test">`)
}

func main() {
    http.HandleFunc("/qyuhen/test", handler)
    http.ListenAndServe(":80", nil)
}

该示例使用自定义域名 test.com 重定向到 github。

$ go get -v test.com/qyuhen/test

Fetching https://test.com/qyuhen/testgo-get=1
https fetch failed.
Fetching http://test.com/qyuhen/testgo-get=1
Parsing meta tags from http://test.com/qyuhen/testgo-get=1 (status code 200)
get "test.com/qyuhen/test": found meta tag http://test.com/qyuhen/testgo-get=1
test.com/qyuhen/test (download)
test.com/qyuhen/test

如此,该库就有两个有效导入路径,可能会导致存储两个本地副本。为此,可以给库添加专门的 "import comment"。当 go get 下载完成后,会检查本地存储路径和该注释是否一致。

github.com/qyuhen/test/abc.go

package test // import "test.com/qyuhen/test"

func Hello() {
    println("Hello, Custom import path!")
}

如继续用 github 路径,会导致 go build 失败。

$ go get -v github.com/qyuhen/test

github.com/qyuhen/test (download)
package github.com/qyuhen/test
    " imports github.com/qyuhen/test
    " imports github.com/qyuhen/test: expects import "test.com/qyuhen/test"

这就强制包用户使用唯一路径,也便于日后将包迁移到其他位置。

8.3.3 初始化

初始化函数:

  • 每个源文件都可以定义一个或多个初始化函数。
  • 编译器不保证多个初始化函数执行次序。
  • 初始化函数在单一线程被调用,仅执行一次。
  • 初始化函数在包所有全局变量初始化后执行。
  • 在所有初始化函数结束后才执行 main.main。
  • 无法调用初始化函数。
  • 因为无法保证初始化函数执行顺序,因此全局变量应该直接用 var 初始化。
var now = time.Now()

func init() {
    fmt.Printf("now: %v\n", now)
}

func init() {
    fmt.Printf("since: %v\n", time.Now().Sub(now))
}

可在初始化函数中使用 goroutine,可等待其结束。

var now = time.Now()

func main() {
    fmt.Println("main:", int(time.Now().Sub(now).Seconds()))
}

func init() {
    fmt.Println("init:", int(time.Now().Sub(now).Seconds()))
    w := make(chan bool)

    go func() {
        time.Sleep(time.Second * 3)
        w <- true
    }()

    <-w
}

输出:

init: 0
main: 3

不应该滥用初始化函数,仅适合完成当前文件中的相关环境设置。

8.4 文档

扩展工具 godoc 能自动提取注释生成帮助文档。

  • 仅和成员相邻 (中间没有空行) 的注释被当做帮助信息。
  • 相邻行会合并成同一段落,用空行分隔段落。
  • 缩进表示格式化文本,比如示例代码。
  • 自动转换 URL 为链接。
  • 自动合并多个源码文件中的 package 文档。
  • 无法显式 package main 中的成员文档。

8.4.1 Package

  • 建议用专门的 doc.go 保存 package 帮助信息。
  • 包文档第一整句 (中英文句号结束) 被当做 packages 列表说明。

8.4.2 Example

只要 Example 测试函数名称符合以下规范即可。

image
说明:使用 suffix 作为示例名称,其首字母必须小写。如果文件中仅有一个 Example 函数,且调用了该文件中的其他成员,那么示例会显示整个文件内容,而不仅仅是测试函数自己。

8.4.3 Bug

非测试源码文件中以 BUG(author) 开始的注释,会在帮助文档 Bugs 节点中显示。

// BUG(yuhen): memory leak.

第 9 章 进阶

9.1 内存布局

了解对象内存布局,有助于理解值传递、引用传递等概念。

image

string

image

struct

image

slice

image

interface

image

new

image

make

image

9.2 指针陷阱

对象内存分配会受编译参数影响。举个例子,当函数返回对象指针时,必然在堆上分配。

可如果该函数被内联,那么这个指针就不会跨栈帧使用,就有可能直接在栈上分配,以实现代码优化目的。因此,是否阻止内联对指针输出结果有很大影响。

允许指针指向对象成员,并确保该对象是可达状态。

除正常指针外,指针还有 unsafe.Pointer 和 uintptr 两种形态。其中 uintptr 被 GC 当做普通整数对象,它不能阻止所 "引用" 对象被回收。

type data struct {
    x [1024 * 100]byte
}

func test() uintptr {
    p := &data{}
    return uintptr(unsafe.Pointer(p))
}

func main() {
    const N = 10000
    cache := new([N]uintptr)

    for i := 0; i < N; i++ {
        cache[i] = test()
        time.Sleep(time.Millisecond)
    }
}

输出:

$ go build -o test && GODEBUG="gctrace=1" ./test

gc607(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3070-3025) objects
gc611(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3090-3045) objects
gc613(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3100-3055) objects

合法的 unsafe.Pointer 被当做普通指针对待。

func test() unsafe.Pointer {
    p := &data{}
    return unsafe.Pointer(p)
}

func main() {
    const N = 10000
    cache := new([N]unsafe.Pointer)

    for i := 0; i < N; i++ {
        cache[i] = test()
        time.Sleep(time.Millisecond)
    }
}

输出:

$ go build -o test && GODEBUG="gctrace=1" ./test
gc12(1): 0+0+0 ms, 199 -> 199 MB 2088 -> 2088 (2095-7) objects
gc13(1): 0+0+0 ms, 399 -> 399 MB 4136 -> 4136 (4143-7) objects
gc14(1): 0+0+0 ms, 799 -> 799 MB 8232 -> 8232 (8239-7) objects

指向对象成员的 unsafe.Pointer,同样能确保对象不被回收。

type data struct {
    x [1024 * 100]byte
    y int
}

func test() unsafe.Pointer {
    d := data{}
    return unsafe.Pointer(&d.y)
}

func main() {
    const N = 10000
    cache := new([N]unsafe.Pointer)

    for i := 0; i < N; i++ {
        cache[i] = test()
        time.Sleep(time.Millisecond)
    }
}

输出:

$ go build -o test && GODEBUG="gctrace=1" ./test

gc12(1): 0+0+0 ms, 207 -> 207 MB 2088 -> 2088 (2095-7) objects
gc13(1): 1+0+0 ms, 415 -> 415 MB 4136 -> 4136 (4143-7) objects
gc14(1): 3+1+0 ms, 831 -> 831 MB 8232 -> 8232 (8239-7) objects

由于可以用 unsafe.Pointer、uintptr 创建 "dangling pointer" 等非法指针,所以在使用时需要特别小心。另外,cgo C.malloc 等函数所返回指针,与 GC 无关。

指针构成的 "循环引用" 加上 runtime.SetFinalizer 会导致内存泄露。

type Data struct {
    d [1024 * 100]byte
    o *Data
}

func test() {
    var a, b Data
    a.o = &b
    b.o = &a

    runtime.SetFinalizer(&a, func(d *Data) { fmt.Printf("a %p final.\n", d) })
    runtime.SetFinalizer(&b, func(d *Data) { fmt.Printf("b %p final.\n", d) })
}

func main() {
    for {
        test()
        time.Sleep(time.Millisecond)
    }
}

输出:

$ go build -gcflags "-N -l" && GODEBUG="gctrace=1" ./test

gc11(1): 2+0+0 ms, 104 -> 104 MB 1127 -> 1127 (1180-53) objects
gc12(1): 4+0+0 ms, 208 -> 208 MB 2151 -> 2151 (2226-75) objects
gc13(1): 8+0+1 ms, 416 -> 416 MB 4198 -> 4198 (4307-109) objects

垃圾回收器能正确处理 "指针循环引用",但无法确定 Finalizer 依赖次序,也就无法调用Finalizer 函数,这会导致目标对象无法变成不可达状态,其所占用内存无法被回收。

9.3 cgo

通过 cgo,可在 Go 和 C/C++ 代码间相互调用。受 CGO_ENABLED 参数限制。

package main

/*
    #include <stdio.h>
    #include <stdlib.h>

    void hello() {
        printf("Hello, World!\n");
    }
*/
import "C"

func main() {
C.hello()
}

调试 cgo 代码是件很麻烦的事,建议单独保存到 .c 文件中。这样可以将其当做独立的 C 程序进行调试。

test.h

#ifndef __TEST_H__
#define __TEST_H__

void hello();

#endif

test.c

#include <stdio.h>
#include "test.h"

void hello() {
    printf("Hello, World!\n");
}

#ifdef __TEST__ // 避免和 Go bootstrap main 冲突。

int main(int argc, char *argv[]) {
    hello();
    return 0;
}

#endif

main.go

package main

/*
    #include "test.h"
*/
import "C"

func main() {
    C.hello()
}

编译和调试 C,只需在命令行提供宏定义即可。

$ gcc -g -D__TEST__ -o test test.c

由于 cgo 仅扫描当前目录,如果需要包含其他 C 项目,可在当前目录新建一个 C 文件,然后用 #include 指令将所需的 .h、.c 都包含进来,记得在 CFLAGS 中使用 "-I" 参数指定原路径。某些时候,可能还需指定 "-std" 参数。

9.3.1 Flags

可使用 #cgo 命令定义 CFLAGS、LDFLAGS 等参数,自动合并多个设置。

/*
    #cgo CFLAGS: -g
    #cgo CFLAGS: -I./lib -D__VER__=1
    #cgo LDFLAGS: -lpthread

    #include "test.h"
*/
import "C"

可设置 GOOS、GOARCH 编译条件,空格表示 OR,逗号 AND,感叹号 NOT。

#cgo windows,386 CFLAGS: -I./lib -D__VER__=1

9.3.2 DataType

数据类型对应关系。

image
image

可将 cgo 类型转换为标准 Go 类型。

/*
    int add(int x, int y) {
        return x + y;
    }
*/
import "C"

func main() {
    var x C.int = C.add(1, 2)
    var y int = int(x)
    fmt.Println(x, y)
}

9.3.3 String

字符串转换函数。

/*
    #include <stdio.h>
    #include <stdlib.h>

    void test(char *s) {
        printf("%s\n", s);
    }

    char* cstr() {
        return "abcde";
    }
*/
import "C"

func main() {
    s := "Hello, World!"

    cs := C.CString(s) // 该函数在 C heap 分配内存,需要调用 free 释放。
    defer C.free(unsafe.Pointer(cs)) // #include <stdlib.h>

    C.test(cs)
    cs = C.cstr()

    fmt.Println(C.GoString(cs))
    fmt.Println(C.GoStringN(cs, 2))
    fmt.Println(C.GoBytes(unsafe.Pointer(cs), 2))
}

输出:

Hello, World!
abcde
ab
[97 98]

用 C.malloc/free 分配 C heap 内存。

/*
    #include <stdlib.h>
*/
import "C"

func main() {
    m := unsafe.Pointer(C.malloc(4 * 8))
    defer C.free(m) // 注释释放内存。

    p := (*[4]int)(m) // 转换为数组指针。
    for i := 0; i < 4; i++ {
        p[i] = i + 100
    }

    fmt.Println(p)
}

输出:

&[100 101 102 103]

9.3.4 Struct/Enum/Union

struct、enum 支持良好,union 会被转换成字节数组。如果没使用 typedef定义,那么必须添加 struct*、enum*、union_ 前缀。

struct

/*
    #include <stdlib.h>

    struct Data {
        int x;
    };

    typedef struct {
        int x;
    } DataType;

    struct Data* testData() {
        return malloc(sizeof(struct Data));
    }

    DataType* testDataType() {
        return malloc(sizeof(DataType));
    }
*/
import "C"

func main() {
    var d *C.struct_Data = C.testData()
    defer C.free(unsafe.Pointer(d))

    var dt *C.DataType = C.testDataType()
    defer C.free(unsafe.Pointer(dt))

    d.x = 100
    dt.x = 200

    fmt.Printf("%#v\n", d)
    fmt.Printf("%#v\n", dt)
}

输出:

&main._Ctype_struct_Data{x:100}
&main._Ctype_DataType{x:200}

enum

/*
    enum Color { BLACK = 10, RED, BLUE };
    typedef enum { INSERT = 3, DELETE } Mode;
*/
import "C"

func main() {
    var c C.enum_Color = C.RED
    var x uint32 = c
    fmt.Println(c, x)

    var m C.Mode = C.INSERT
    fmt.Println(m)
}

union

/*
    #include <stdlib.h>

    union Data {
        char x;
        int y;
    };

    union Data* test() {
        union Data* p = malloc(sizeof(union Data));
        p->x = 100;
        return p;
    }
*/
import "C"

func main() {
    var d *C.union_Data = C.test()
    defer C.free(unsafe.Pointer(d))

    fmt.Println(d)
}

输出:

&[100 0 0 0]

9.3.5 Export

导出 Go 函数给 C 调用,须使用 //export 标记。建议在独立头文件中声明函数原型,避免 duplicate symbol 错误。

main.go

package main

import "fmt"

/*
    #include "test.h"
*/
import "C"

//export hello

func hello() {
    fmt.Println("Hello, World!\n")
}

func main() {
    C.test()
}

test.h

#ifndef __TEST_H__
#define __TEST_H__

extern void hello();
void test();

#endif

test.c

#include <stdio.h>
#include "test.h"

void test() {
    hello();
}

9.3.6 Shared Library

在 cgo 中使用 C 共享库。

test.h

#ifndef __TEST_HEAD__
#define __TEST_HEAD__

int sum(int x, int y);

#endif

test.c

#include <stdio.h>
#include <stdlib.h>
#include "test.h"

int sum(int x, int y)
{
    return x + y + 100;
}

编译成 .so 或 .dylib。

$ gcc -c -fPIC -o test.o test.c
$ gcc -dynamiclib -o libtest.dylib test.o

将共享库和头文件拷贝到 Go 项目目录。

main.go

package main

/*
    #cgo CFLAGS: -I.
    #cgo LDFLAGS: -L. -ltest
    #include "test.h"
*/
import "C"

func main() {
    println(C.sum(10, 20))
}

输出:

$ go build -o test && ./test
130

编译成功后可用 ldd 或 otool 查看动态库使用状态。 静态库使用方法类似。

9.4 Reflect

没有运行期类型对象,实例也没有附加字段用来表明身份。只有转换成接口时,才会在其 itab 内部存储与该类型有关的信息,Reflect 所有操作都依赖于此。

9.4.1 Type

以 struct 为例,可获取其全部成员字段信息,包括非导出和匿名字段。

type User struct {
    Username string
}

type Admin struct {
    User
    title string
}

func main() {
    var u Admin
    t := reflect.TypeOf(u)

    for i, n := 0, t.NumField(); i < n; i++ {
        f := t.Field(i)
        fmt.Println(f.Name, f.Type)
    }
}

输出:

User main.User // 可进一步递归。
title string

如果是指针,应该先使用 Elem 方法获取目标类型,指针本身是没有字段成员的。

func main() {
    u := new(Admin)

    t := reflect.TypeOf(u)
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
    }

    for i, n := 0, t.NumField(); i < n; i++ {
        f := t.Field(i)
        fmt.Println(f.Name, f.Type)
    }
}

同样,value-interfacepointer-interface 也会导致方法集存在差异。

type User struct {
}

type Admin struct {
    User
}

func (*User) ToString() {}
func (Admin) test() {}

func main() {

    var u Admin

    methods := func(t reflect.Type) {
        for i, n := 0, t.NumMethod(); i < n; i++ {
            m := t.Method(i)
            fmt.Println(m.Name)
        }
    }

    fmt.Println("--- value interface ---")
    methods(reflect.TypeOf(u))

    fmt.Println("--- pointer interface ---")
    methods(reflect.TypeOf(&u))
}

输出:

--- value interface ---
test
--- pointer interface ---
ToString
test

可直接用名称或序号访问字段,包括用多级序号访问嵌入字段成员。

type User struct {
    Username string
    age int
}

type Admin struct {
    User
    title string
}

func main() {
    var u Admin
    t := reflect.TypeOf(u)

    f, _ := t.FieldByName("title")
    fmt.Println(f.Name)

    f, _ = t.FieldByName("User") // 访问嵌入字段。
    fmt.Println(f.Name)
    f, _ = t.FieldByName("Username") // 直接访问嵌入字段成员,会自动深度查找。
    fmt.Println(f.Name)

    f = t.FieldByIndex([]int{0, 1}) // Admin[0] -> User[1] -> age
    fmt.Println(f.Name)
}

输出:

title
User
Username
age

字段标签可实现简单元数据编程,比如标记 ORM Model 属性。

type User struct {
    Name string `field:"username" type:"nvarchar(20)"`
    Age int `field:"age" type:"tinyint"`
}

func main() {
    var u User

    t := reflect.TypeOf(u)
    f, _ := t.FieldByName("Name")

    fmt.Println(f.Tag)
    fmt.Println(f.Tag.Get("field"))
    fmt.Println(f.Tag.Get("type"))
}

输出:

field:"username" type:"nvarchar(20)"
username
nvarchar(20)

可从基本类型获取所对应复合类型。

var (
    Int = reflect.TypeOf(0)
    String = reflect.TypeOf("")
)

func main() {
    c := reflect.ChanOf(reflect.SendDir, String)
    fmt.Println(c)

    m := reflect.MapOf(String, Int)
    fmt.Println(m)

    s := reflect.SliceOf(Int)
    fmt.Println(s)

    t := struct{ Name string }{}
    p := reflect.PtrTo(reflect.TypeOf(t))
    fmt.Println(p)
}

输出:

chan<- string
map[string]int
[]int
*struct { Name string }

与之对应,方法 Elem 可返回复合类型的基类型。

func main() {
    t := reflect.TypeOf(make(chan int)).Elem()
    fmt.Println(t)
}

方法 Implements 判断是否实现了某个具体接口,AssignableTo、ConvertibleTo 用于赋值和转换判断。

type Data struct {
}

func (*Data) String() string {
    return ""
}

func main() {
    var d *Data
    t := reflect.TypeOf(d)

    // 没法直接获取接口类型,好在接口本身是个 struct,创建
    // 一个空指针对象,这样传递给 TypeOf 转换成 interface{}
    // 时就有类型信息了。。
    it := reflect.TypeOf((*fmt.Stringer)(nil)).Elem()

    // 为啥不是 t.Implements(fmt.Stringer),完全可以由编译器生成。
    fmt.Println(t.Implements(it))
}

某些时候,获取对齐信息对于内存自动分析是很有用的。

type Data struct {
    b byte
    x int32
}

func main() {
    var d Data

    t := reflect.TypeOf(d)
    fmt.Println(t.Size(), t.Align()) // sizeof,以及最宽字段的对齐模数。

    f, _ := t.FieldByName("b")
    fmt.Println(f.Type.FieldAlign()) // 字段对齐。
}

输出:

8 4
1

9.4.2 ValueValue 和 Type 使用方法类似,包括使用 Elem 获取指针目标对象。

type User struct {
    Username string
    age int
}

type Admin struct {
    User
    title string
}

func main() {
    u := &Admin{User{"Jack", 23}, "NT"}
    v := reflect.ValueOf(u).Elem()

    fmt.Println(v.FieldByName("title").String()) // 用转换方法获取字段值
    fmt.Println(v.FieldByName("age").Int()) // 直接访问嵌入字段成员
    fmt.Println(v.FieldByIndex([]int{0, 1}).Int()) // 用多级序号访问嵌入字段成员
}

输出:

NT

23
23

除具体的 Int、String 等转换方法,还可返回 interface{}。只是非导出字段无法使用,需用 CanInterface 判断一下。

type User struct {
    Username string
    age int
}

func main() {
    u := User{"Jack", 23}
    v := reflect.ValueOf(u)

    fmt.Println(v.FieldByName("Username").Interface())
    fmt.Println(v.FieldByName("age").Interface())
}

输出:

Jack

panic: reflect.Value.Interface: cannot return value obtained from unexported field or
method

当然,转换成具体类型不会引发 panic。

func main() {
    u := User{"Jack", 23}
    v := reflect.ValueOf(u)

    f := v.FieldByName("age")

    if f.CanInterface() {
        fmt.Println(f.Interface())
    } else {
        fmt.Println(f.Int())
    }
}

struct,其他复合类型 array、slice、map 取值示例。

func main() {
    v := reflect.ValueOf([]int{1, 2, 3})
    for i, n := 0, v.Len(); i < n; i++ {
        fmt.Println(v.Index(i).Int())
    }

    fmt.Println("---------------------------")

    v = reflect.ValueOf(map[string]int{"a": 1, "b": 2})
    for _, k := range v.MapKeys() {
        fmt.Println(k.String(), v.MapIndex(k).Int())
    }
}

输出:

1
2
3
---------------------------
a 1
b 2

需要注意,Value 某些方法没有遵循 "comma ok" 模式,而是返回 ZeroValue,因此需要用 IsValid 判断一下是否可用。

func (v Value) FieldByName(name string) Value {
    v.mustBe(Struct)
    if f, ok := v.typ.FieldByName(name); ok {
        return v.FieldByIndex(f.Index)
    }
    return Value{}
}
type User struct {
    Username string
    age int
}

func main() {
    u := User{}
    v := reflect.ValueOf(u)

    f := v.FieldByName("a")
    fmt.Println(f.Kind(), f.IsValid())
}

输出:

invalid false

另外,接口是否为 nil,需要 tab 和 data 都为空。可使用 IsNil 方法判断 data 值。

func main() {
    var p *int

    var x interface{} = p
    fmt.Println(x == nil)

    v := reflect.ValueOf(p)
    fmt.Println(v.Kind(), v.IsNil())
}

输出:

false
ptr true

将对象转换为接口,会发生复制行为。该复制品只读,无法被修改。所以要通过接口改变目标对象状态,必须是 pointer-interface。

就算是指针,我们依然没法将这个存储在 data 的指针指向其他对象,只能透过它修改目标对象。因为目标对象并没有被复制,被复制的只是指针。

type User struct {
    Username string
    age int
}

func main() {
    u := User{"Jack", 23}

    v := reflect.ValueOf(u)
    p := reflect.ValueOf(&u)

    fmt.Println(v.CanSet(), v.FieldByName("Username").CanSet())
    fmt.Println(p.CanSet(), p.Elem().FieldByName("Username").CanSet())
}

输出:

false false
false true

非导出字段无法直接修改,可改用指针操作。

type User struct {
    Username string
    age int
}

func main() {
    u := User{"Jack", 23}
    p := reflect.ValueOf(&u).Elem()

    p.FieldByName("Username").SetString("Tom")

    f := p.FieldByName("age")
    fmt.Println(f.CanSet())

    // 判断是否能获取地址。
    if f.CanAddr() {
        age := (*int)(unsafe.Pointer(f.UnsafeAddr()))
        // age := (*int)(unsafe.Pointer(f.Addr().Pointer())) // 等同
        *age = 88
    }

    // 注意 p 是 Value 类型,需要还原成接口才能转型。
    fmt.Println(u, p.Interface().(User))
}

输出:

false
{Tom 88} {Tom 88}

复合类型修改示例。

func main() {
    s := make([]int, 0, 10)
    v := reflect.ValueOf(&s).Elem()

    v.SetLen(2)
    v.Index(0).SetInt(100)
    v.Index(1).SetInt(200)

    fmt.Println(v.Interface(), s)

    v2 := reflect.Append(v, reflect.ValueOf(300))
    v2 = reflect.AppendSlice(v2, reflect.ValueOf([]int{400, 500}))

    fmt.Println(v2.Interface())

    fmt.Println("----------------------")

    m := map[string]int{"a": 1}
    v = reflect.ValueOf(&m).Elem()

    v.SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(100)) // update
    v.SetMapIndex(reflect.ValueOf("b"), reflect.ValueOf(200)) // add
    fmt.Println(v.Interface(), m)
}

输出:

[100 200] [100 200]
[100 200 300 400 500]
----------------------
map[a:100 b:200] map[a:100 b:200]

9.4.3 Method
可获取方法参数、返回值类型等信息。

type Data struct {
}

func (*Data) Test(x, y int) (int, int) {
    return x + 100, y + 100
}

func (*Data) Sum(s string, x ...int) string {
    c := 0
    for _, n := range x {
        c += n
    }

    return fmt.Sprintf(s, c)
}

func info(m reflect.Method) {
    t := m.Type

    fmt.Println(m.Name)

    for i, n := 0, t.NumIn(); i < n; i++ {
        fmt.Printf(" in[%d] %v\n", i, t.In(i))
    }

    for i, n := 0, t.NumOut(); i < n; i++ {
        fmt.Printf(" out[%d] %v\n", i, t.Out(i))
    }
}

func main() {
    d := new(Data)
    t := reflect.TypeOf(d)
    test, _ := t.MethodByName("Test")
    info(test)

    sum, _ := t.MethodByName("Sum")
    info(sum)
}

输出:

Test
    in[0] *main.Data // receiver
    in[1] int
    in[2] int
    out[0] int
    out[1] int
Sum
    in[0] *main.Data
    in[1] string
    in[2] []int
    out[0] string

动态调用方法很简单,按 In 列表准备好所需参数即可 (不包括 receiver)。

func main() {
    d := new(Data)
    v := reflect.ValueOf(d)

    exec := func(name string, in []reflect.Value) {
        m := v.MethodByName(name)
        out := m.Call(in)

        for _, v := range out {
            fmt.Println(v.Interface())
        }
    }

    exec("Test", []reflect.Value{
    reflect.ValueOf(1),
    reflect.ValueOf(2),
    })

    fmt.Println("-----------------------")

    exec("Sum", []reflect.Value{
        reflect.ValueOf("result = %d"),
        reflect.ValueOf(1),
        reflect.ValueOf(2),
    })
}

输出:

101
102
-----------------------
result = 3

如改用 CallSlice,只需将变参打包成 slice 即可。

func main() {
    d := new(Data)
    v := reflect.ValueOf(d)

    m := v.MethodByName("Sum")

    in := []reflect.Value{
        reflect.ValueOf("result = %d"),
        reflect.ValueOf([]int{1, 2}), // 将变参打包成 slice。
    }

    out := m.CallSlice(in)

    for _, v := range out {
        fmt.Println(v.Interface())
    }
}

非导出方法无法调用,甚至无法透过指针操作,因为接口类型信息中没有该方法地址。

9.4.4 Make
利用 Make、New 等函数,可实现近似泛型操作。

var (
    Int = reflect.TypeOf(0)
    String = reflect.TypeOf("")
)

func Make(T reflect.Type, fptr interface{}) {

    // 实际创建 slice 的包装函数。
    swap := func(in []reflect.Value) []reflect.Value {

        // --- 省略算法内容 --- //
        // 返回和类型匹配的 slice 对象。
        return []reflect.Value{
            reflect.MakeSlice(
                reflect.SliceOf(T), // slice type
                int(in[0].Int()), // len
                int(in[1].Int()) // cap
            ),
        }
    }

    // 传入的是函数变量指针,因为我们要将变量指向 swap 函数。
    fn := reflect.ValueOf(fptr).Elem()

    // 获取函数指针类型,生成所需 swap function value。
    v := reflect.MakeFunc(fn.Type(), swap)

    // 修改函数指针实际指向,也就是 swap。
    fn.Set(v)
}

func main() {
    var makeints func(int, int) []int
    var makestrings func(int, int) []string

    // 用相同算法,生成不同类型创建函数。
    Make(Int, &makeints)
    Make(String, &makestrings)

    // 按实际类型使用。
    x := makeints(5, 10)
    fmt.Printf("%#v\n", x)

    s := makestrings(3, 10)
    fmt.Printf("%#v\n", s)
}

输出:

[]int{0, 0, 0, 0, 0}
[]string{"", "", ""}

原理并不复杂。

核心是提供一个 swap 函数,其中利用 reflect.MakeSlice 生成最终 slice 对象, 因此需要传入 element type、len、cap 参数。
接下来,利用 MakeFunc 函数生成 swap value,并修改函数变量指向,以达到调 用 swap 的目的。
当调用具体类型的函数变量时,实际内部调用的是 swap,相关代码会自动转换参 数列表,并将返回结果还原成具体类型返回值。
如此,在共享算法的前提下,无须用 interface{},无须做类型转换,颇有泛型的效果。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:哈哥_

查看原文:golang基础语法-2

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

568 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传