代码块与作用域
直接上题,一个简单的测验。
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语言的函数具有如下特点:
以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)})
}
评论