函数和指针

func

使用Go语言进行模块化编程,必须了解的知识主要包括几个重要的数据类型以及一些模块化编程的技巧。

在Go语言中函数是一等公民,函数类型也是一等的数据类型。这是函数式编程的重要特征,Go语言在语言层面支持函数式编程。函数的用途:

  1. 封装代码、分割功能、解耦逻辑

  2. 作为普通函数值,在其他函数之间传递、赋值变量、做类型判断和转换(就像切片或字典)

函数值可以成为被随意传播的独立逻辑组件(功能模块)

对于函数类型来说,它是一种对一组输入、输出进行模板化的重要工具,它比接口类型更加轻巧、灵活,它的值也借此变成了可被热替换的逻辑组件。

package main

import "fmt"

// 声明函数类型
type Printer func(contents string) (n int, err error)

// 实现了函数类型Printer
func printToStd(contents string) (bytesNum int, err error) {
    return fmt.Println(contents)
}

func main() {
    var p Printer
    p = printToStd
    p("something")
}

函数的签名包括:函数的参数列表和函数的结果列表,它定义了用来鉴别不同函数的特征,也定义了与函数的交互方式。

函数签名中元素顺序及类型一致,那么就是同一个函数,或是实现了同一个函数类型的函数

编写高阶函数

高阶函数满足的条件:

  1. 接收其他函数作为参数传入

  2. 把其他的函数作为结果返回

只要满足其中一个条件就是高阶函数,高阶函数也是函数式编程中重要的概念和特征

高阶函数可用于实现闭包。

需求:编写calculate函数来实现两个整数间的加减乘除运算,希望两个整数和具体的操作都由该函数的调用方给出。

接收其他的函数作为参数传入。

package main

import (
    "errors"
    "fmt"
)

// 第一步:声明叫operator的函数类型
type operator func(x, y int) int

// 第二步:编写calculate函数
func calculate(x int, y int, op operator) (int, error) {
    if op == nil { // 卫述语句,判断操作是否合法
        return 0, errors.New("invalid operation")
    }
    return op(x, y), nil
}

// 实现operator,只要函数的签名与operator类型一致就是它的实现

func main() {
    op := func(x, y int) int {
        return x + y
    }

    x, y := 12, 23
    result, err := calculate(x, y, op)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(result)
}

卫述语句:被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块的执行的语句。Go语言中if语句常被作为卫述语句。

把其他的函数作为结果返回。

package main

import (
    "errors"
    "fmt"
)

// 第一步:声明叫operator和calculateFunc的函数类型
type operator func(x, y int) int

type calculateFunc func(x int, y int) (int, error)

// 第二步:编写genCalculator函数
func genCalculator(op operator) calculateFunc {
    return func(x int, y int) (int, error) {
        if op == nil { // 卫述语句
            return 0, errors.New("invalid operation")
        }
        return op(x, y), nil
    }
}

// 实现operator

func main() {
    op := func(x, y int) int {
        return x + y
    }

    add := genCalculator(op)

    x, y := 56, 78
    result, err := add(x, y)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(result)
}

闭包

在一个函数中存在对外来标识符(既不代表当前函数的任何参数或结果,也不是函数内部声明的,而是直接从外边拿来的)的引用,也称为自由变量。这里的函数称为闭包函数,就是因为引用了自由变量,而呈现出了一种“不确定”的状态(开放状态)。

闭包体现的是由“不确定”变为“确定”的一个过程。对于Go这样的静态类型语言,在定义闭包函数的时候,最多只能知道自由变量的类型。

func genCalculator(op operate) calculateFunc {
    return func(x int, y int) (int, error) {
        if op == nil {
            return 0, errors.New("invalid operation")
        }
        return op(x, y), nil
    }
}

genCalculator只做了一件事,定义一个匿名的calculateFunc类型的函数,并把它作为结果返回。这个匿名函数就是一个闭包函数。它使用的变量op既不代表它的任何参数或结果,也不是它自己声明的,而是定义它的genCalculator函数的参数,所以是一个自由变量(这个自由变量不是在定义闭包函数的时候确定的,而是在genCalculator函数被调用的时候确定的)。

只有给定了该函数的参数op,才知道闭包函数可用于声明运算

编译到if op == nill语句时,Go编译器寻找op所代表的东西,发现op代表的是genCalculator函数的参数,然后把这两者联系起来,此时自由变量op被捕获,这个闭包函数的状态由”不确定“变为”确定“。

闭包的用途:表面上是延迟实现了一部分程序逻辑或功能,实际上是在动态地生成那部分程序逻辑。

函数的参数值

package main

import "fmt"

func main() {
    array1 := [3]string{"a", "b", "c"}
    fmt.Printf("The array: %v\n", array1)
    array2 := modifyArray(array1)
    fmt.Printf("The modified array: %v\n", array2)  // The modified array: [a x c]
    fmt.Printf("The original array: %v\n", array1)  // The original array: [a b c]
}

func modifyArray(a [3]string) [3]string {
    a[1] = "x"
    return a
}

原数组没有因为modifyArray()函数而改变,因为所有传给函数的参数值都会被复制,函数在其内部使用的并不是参数值的原值,而是它的副本。

数组是值类型,所以每次复制都会拷贝它,以及它的所有元素值。

对于引用类型(切片、字典、通道),复制值的时候,只会拷贝它本身,并不会拷贝它引用的底层数据。以切片为例,只是拷贝了它指向底层数组中某一个元素的指针,以及它的长度值和容量值,底层数组并不会被拷贝。

pointer

type Dog struct {    // 结构体类型,基本类型
    name string
}

func (dog *Dog) SetName(name string) {    // *Dog 指针类型
    dog.name = name
}

SetName方法的接收值就是Dog类型的指针值,通过指针值无缝访问基本值包含的任何字段、以及调用与之关联的任何方法。

指针是一个指向某个确切的内存地址的值,这个内存地址可以是任何数据或代码的起始地址,如,某个变量、字段、函数。

Go语言内建的数据类型:uintptr类型是一个数值类型,最贴近传统意义上的指针。根据计算机架构的不同,它可以存储32或64为的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。

标准库unsafe包中的Pointer类型,代表了指针。unsafe.Pointer可以表示任何指向可寻址的值的指针,它也是指针值和uintptr值之间的桥梁。通过它可以在这两者之间转换。

不可寻址的值

package main

type Named interface {
    // Name 用于获取名字
    Name() string
}

type Dog struct {
    name string
}

func (dog *Dog) SetName(name string) {
    dog.name = name
}

func (dog Dog) Name() string {
    return dog.name
}

func main() {
    const num = 123
    //_ = &num 常量不可寻址
    //_ = &(123) 基本类型值的字面量不可寻址

    var str = "abc"
    _ = str
    //_ = &(str[0]) 对字符串变量的索引结果值不可寻址
    //_ = &(str[0:2]) 对字符串变量的切片结果值不可寻址
    str2 := str[0]
    _ = &str2 // 但这样的寻址就是合法的

    //_ = &(123 + 456) 算术操作的结果值不可寻址
    num2 := 456
    _ = num2
    //_ = &(num + num2) 算术操作的结果值不可寻址

    //_ = &([3]int{1, 2, 3}[0]) 对数组字面量的索引结果值不可寻址
    //_ = &([3]int{1, 2, 3}[0:2]) 对数组字面量的切片结果值不可寻址
    _ = &([]int{1, 2, 3}[0]) // 对切片字面量的索引结果值却是可寻址的
    //_ = &([]int{1, 2, 3}[0:2]) 对切片字面量的切片结果值不可寻址
    //_ = &(map[int]string{1: "a"}[0]) 对字典字面量的索引结果值不可寻址

    var map1 = map[int]string{1: "a", 2: "b", 3: "c"}
    _ = map1
    //_ = &(map1[2]) 对字典变量的索引结果值不可寻址

    //_ = &(func(x, y int) int {
    //    return x + y
    //}) 字面量代表的函数不可寻址
    //_ = &(fmt.Sprintf) 标识符代表的函数不可寻址
    //_ = &(fmt.Sprintln("abc")) 对函数的调用结果值不可寻址

    dog := Dog{"little pig"}
    _ = dog
    //_ = &(dog.Name) 标识符代表的函数不可寻址
    //_ = &(dog.Name()) 对方法的调用结果值不可寻址

    //_ = &(Dog{"little pig"}.name) 结构体字面量的字段不可寻址

    //_ = &(interface{}(dog)) 类型转换表达式的结果值不可寻址
    dogI := interface{}(dog)
    _ = dogI
    //_ = &(dogI.(Named)) 类型断言表达式的结果值不可寻址
    named := dogI.(Named)
    _ = named
    //_ = &(named.(Dog)) 类型断言表达式的结果值不可寻址

    var chan1 = make(chan int, 1)
    chan1 <- 1
    //_ = &(<-chan1) 接收表达式的结果值不可寻址
}

不可寻址的总结:

  • 不可变的:常量、基本类型字面量、字符串、函数以及方法的字面量

  • 临时结果:算术操作的结果,对数组值、切片值、字典值字面量施加的表达式的求值结果;如果把一个临时结果赋给一个变量,那它就是可寻址的

  • 不安全: 字典类型的变量施加索引值得到的结果不属于临时结果,但是字典中键值对的地址是会变化的,获取指针不安全

对函数或方法的调用属于临时结果、拿到一段代码的指针是不安全的。

表达式整理:

  • 索引表达式

  • 切片表示式

  • 选择表达式

  • 调用表达式

  • 类型转换表达式

  • 类型断言表达式

  • 向通道发送或者从通道接收的接收表达式

对数组或切片类型的变量的索引或切片的结果值不是临时结果

无法使用取地址操作符(&)获取指针;对不可寻址值取地址,编译器会报错

在上述例子的基础上编写新函数:

func New(name string) Dog {
    return Dog{name}
}

func main() {
    New("little pig").SetName("monster")    // 编译报错,不能取得New("little pig")的地址
}

例外情况:

  1. ++--的左边是一个表达式(表达式的结果值必须是可寻址的),就可以组成一个自增或自减语句,所以针对值字面量的表达式几乎都无法在这里使用。字典字面量和索引表达式可以用

  2. 赋值语句中,赋值操作左边的表达式的结果必须是可寻址的,字典的索引结果可用

  3. 带range子句的for循环中,range关键字左边的表达式的结果值必须是可寻址的,字典的索引结果可用

最后更新于

这有帮助吗?