Go语言精进之路-白明

MrSnake
2022-02-21 / 2 评论 / 335 阅读 / 正在检测是否收录...

代码块与作用域

直接上题,一个简单的测验。

func main() {
  if a := 1; false {
  } else if b := 2; false {
  } else if c := 3; false {
  } else {
    println(a, b, c)
  }
}

两个选项:123或无法通过编译

这道题只有深入了解了Go代码块与作用域规则,才能理解这道题的原理。

Go代码块与作用域简介

代码块是代码执行流流转的基本单元,代码执行流总是从一个代码块跳到另一个代码块。
Go语言中分为两类代码块,一类为由代码中直观可见通过大括号包括的显示代码块,另一类则是没有大括号包裹的隐式代码块隐式代码块又分成:

  • 宇宙(Universe)代码块
    相当于所有Go代码最外层都存在一对大括号
  • 包代码块
    每个包都有一个代码块,其中放置着该包的所有Go
    源码
  • 文件代码块
    每个文件都有一个文件代码块,其中包含着文件中所有Go源码

If语句的代码块

1.单if型

 if statment; expression {
     ...
 }

if语句自身在一个隐式代码块中,上述情况中,有隐式代码块和显示代码块。
上面的代码等价于:

{ // 隐式代码块开始
  statment
  if expression { // 显示代码块开始
      ...
  } // 显示代码块结束

} // 隐式代码块结束

可以看出if expression的显示代码块嵌套在statment所在的隐式代码块内部,这也是为何statment声明的短变量,可以在if显示代码块中使用。
示例:

func Foo() {
  if a := 1; true {
      fmt.Println(a)
  }
}
// 等价于
func Foo() {
  {
    a := 1
    if true {
      fmt.Println(a)
    }
  }
}

变量a的作用域是可延伸到if内的显示代码块中,在if中使用a是合法的。
2.ifelse型

if statment; expression{
  ...
} else {
  ...
}
// 等价于
{ // 隐式代码块开始
  statment
  if expression { // 显示代码块1开始
    ...
    // 显示代码块1结束
  } else { // 显示代码块2开始
    ...
  } // 显示代码块2结束

} // 隐式代码块结束

示例:

func Foo() {
  if a,b := 1, 2; false {
    fmt.Println(a)
  } else {
    fmt.Println(b)
  }
}
// 等价于
func Foo() {
  {
    a, b := 1, 2
    if false {
      fmt.Println(a)
    } else {
      fmt.Println(b)
    }
  }
}

可以看出,statement中声明的变量,其作用域范围可以延伸到else后面的显示代码块中。
3.ifelseifelse型
下面就是上面测试的类型:

if statement; expression1 {
  ...
} else if statement2; expresson2 {
  ...
} else {
  ...
}
// 等价于
{ // 隐式代码块1开始
  statment1
  if expression { // 显示代码块1开始
    ...
  } else { // 显示代码块1结束;显示代码块2开始
    { // 隐式代码块2开始
      statement2
      if expression2 { // 显示代码块3开始
        ....
      } else { // 显示代码块3结束;显示代码块4开始
        ...
      } // 显示代码块4结束
    } //隐式代码块2结束
  } // 显示代码块2结束
} // 隐式代码块1结束

结合上述规则,分析上面的小测验:

func main() {
  if a := 1; false {
  } else if b := 2; false {
  } else if c := 3; false {
  } else {
    println(a, b, c)
  }
}
// 等价于
func main() {
  {
    a := 1
    if false {
    
    } else {
      {
        b := 2
        if false {
        
        } else {
          c := 3
          if false {
            
          } else {
            println(a, b, c)
          }
        }
      }
    }
  }
}

展开后,可以很明显的看到,a、b、c三个变量都位于不同的隐私和代码块中,根据他们的作用域范围,最后一层的else使用这三个变量都和合法的。
所以最后的结果为123

函数与方法

让自己习惯于函数是“一等公民”
作为现代编程语言的基本语法元素,函数存在于支持各种范式的主流编程语言当中。无论是命令式语言C、多范式通用编程语言C++,还是面向对象编程语言Java、Ruby,抑或函数式语言Haskell及动态脚本语言Python、PHP、JavaScript,函数这一语法元素都是当仁不让的核心。
Go语言中没有那些典型的面向对象语言的语言,比如类、继承、对象等。Go语言中的方法(method)本质上是函数的一个变种。

本质上,我们可以说Go程序就是一组函数的集合。

Go语言的函数具有如下特点:
以func关键字开头;

  • 支持多返回值;
  • 支持具名返回值;
  • 支持递归调用;
  • 支持同类型的可变参数;
  • 支持defer,实现函数优雅返回。

函数在Go语言中属于“ 一等公民 ”。那么“一等公民”函数有哪些特质可以帮助我们写出优雅、简洁的Go代码。

什么是“一等公民”

引用一下wiki发明人、C2站点作者Ward Cunningham对“一等公民”的诠释:
如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。在动态类型语言中,语言运行时还支持对“一等公民”类型的检查。
基于上面关于“一等公民”的诠释,来看一下Go是如何满足上述条件的。
(1) 正常创建

// $GOROOT/src/fmt/print.go
func newPrinter() *pp {
  p := ppFree.Get().(*pp)
  p.panicking = false
  p.erroring = false
  p.wrapErrs = false
  p.fmt.init(&p.buf)
  return p
}

(2) 在函数内创建
可以在函数内部定义匿名函数,(被复制给变量p1)。在C/C++中无法实现这一点,这也是C/C++语言中函数不是“一等公民”的例证。

func example() {
  p := func(x, y int) {
    fmt.Println(x + y)
  }
}

(3) 作为类型
可以使用函数来自定义类型,如:

type HandlerFunc func(http.ResponseWriter, *http.Request)

(4) 存储到变量中

func example2() {
  p := func(x, y int) {
    fmt.Println(x * y)
  }
}

(5) 作为参数传入函数

// $GOROOT/src/time/sleep.go
func AfterFunc(d Duration, f func()) *Timer {
  t := &Timer{
    r: runtimeTimer{
      when: when(d),
      f: goFunc,
      arg: f,
    },
  }
  startTimer(&t.r)
  return t
}

(6) 作为返回值从函数返回

func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
  return func (db *gorm.DB) *gorm.DB {
    return db.Where("status IN (?)", status)
  }
}

Go中的函数可以像普通整型值那样被创建和使用 ,函数还可以被放入数组、切片或map等结构中,可以像变量一样被赋值给interface{},还可以建立元素为函数的channel。如:

type calFunc func(int, int) int

func main() {
  var i interface{} = calFunc(func(x, y int) int { return x + y})
  c := make(chan func(int, int) int, 10)
  fns := []calFunc{
    func(x, y int) int { return x + y},
    func(x, y int) int { return x - y},
  }
  c <- func(x, y int) int {
    return x * y
  }

  fmt.Println(fns[0](1, 2))
  f := <-c
  fmt.Println(f(1, 2))
  v, _ := i.(calFunc)
  fmt.Println(v(1, 2))
}

如何发挥“一等公民”的最大效用

1.对函数进行显示类型转换
最为典型的使用,就是http.HandlerFunc这个类型

func greeting(w http.ReponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, World!")
}

func main() {
  http.ListenAndServe(":8080", http.HandlerFunc(greet))
}

上述代码是用Go构建的Web Server的例子。当用户访问web server的8080端口时,会收到"Hello, World!",这里用到了“一等公民”的特性。http.HandlerFunc(greeting)将函数greeting显示转换为HandlerFunc类型,而其实现了Handler接口,这样greeting也就实现了Handler接口,满足了ListenAndServe函数第二个参数的要求。
2.函数式编程
对函数式编程的支持,而这就得益于函数是“一等公民”的特质。虽然Go不推崇函数式编程,但有些时候局部应用函数式编程风格可以写出更优雅、更简洁、更易维护的代码。
(1) 柯里化函数
 柯里化把函数接收多个参数转化为接收一个单一的参数,返回能接下余下参数的函数。
例如:

func add(x, y int) int {
  return a + b
}

// 接受第一个参数,返回一个函数接受第二个参数,效果上等价于add方法
func partialAdd(x int) func(int) int {
  return func(y int) int {
      return add(x, y)
  }
}

func main() {
  two := partialAdd(2) // 展开 two := func(y int) int {return add(2, y)}
  fmt.Println(two(3))
}

上述例子是Go函数支持的另一个特性闭包。闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。本质上,闭包是将函数内部和函数外部连接起来的桥梁。
(2) 函子
 函子需要满足两个条件:函子本身是一个容器类型,以Go语言为例,这个容器可以是切片、map甚至channel;该容器类型需要实现一个方法,该方法接受一个函数类型参数,并在容器的每个元素上应用那个函数,得到一个新函子,原函子容器内部的元素值不受影响。
例如:

type IntSlicer interface {
  Fmap(func(int) int) IntSlicer
}

type intSlice struct {
  ints []int
}

func NewIntSlice(x []int) IntSlicer {
  return intSlice{ints: x}
}

func (i intSlice) Fmap(fn func(int) int) IntSlicer {
  newInts := make([]int, len(i.ints))
  for i, v := range i.ints {
    r := fn(v)
    newInts[i] = r
  }
  return intSlice{ints: newInts}
}

func main() {
  s := NewIntSlice([]int{1, 2, 3, 4}) // 原函子并且不会变化
  s.Fmap(fun(x int) int {return x + 10}) // 每个数加10
  // 支持链式调用
  s.Fmap(fun(x int) int {return x + 10}).Fmap(fun(x int) int {return x * 10})
}

(3) 延续传递式
 延续传递式(Continuation-passing Style,CPS)函数是不允许有返回值的。一个函数A应该将其想返回的值显式传给一个continuation函数(一般接受一个参数),而这个continuation函数自身是函数A的一个参数
例如:

func Max(a, b int) int {
 if a < b {
  return b
 } else {
  return a
 }
}

// 转化后cps风格
func MaxCps(a, b int, f func(int)) {
 if a < b {
   f(b)
 } else {
   f(a)
 }
}

// main
func main() {
 fmt.Printf("%d/n", Max(5,6))
 MaxCps(5, 6, func(y int){fmt.Printf("%d/n", y)})
}
1

评论 (2)

取消
  1. 头像
    1
    Windows 10 · Google Chrome

    555

    回复
  2. 头像
    1
    Windows 10 · Google Chrome

    555

    回复