作用域并非函数的子知识点,之所以在此处介绍,是因为在学习完函数基本使用后,我们才可以成体系介绍它
-
1、什么是作用域?
域指的是范围,作用域名指的是名字(变量名、函数名等)可以被有效访问的范围
-
2、为何要学习作用域?
声明语句会将程序中的实体和一个名字关联,比如函数名、变量名等名字,而声明的意义在于以后引用,若想引用声明实体就需要用到名字,但我们在程序中声明了那么多名字,当引用一个名字时应该去哪里找?名字若出现冲突了,访问的优先级是什么?这就是我们学习作用域的意义。
-
3、词法域
作用域分为静态作用域(又称之为词法域)与动态作用域,go与python都是静态作用域,详解如下
| 编译器的第一个工作阶段叫做词法化,也叫单词化,所以说,词法作用域是在词法阶段生成的作用域,是一个编译时的概念。与词法域相对应的是动态作用域,二者对比如下 |
| 1、词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。而动态作用域是在运行时确定的。 |
| 3、词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用,即动态作用域链是基于运行时的调用栈的 |
总结:词法域是声明语句的域,而非引用语句的域,即声明语句的地方决定了作用域。
详细地说就是:
层层嵌套的关系是以定义/声明阶段为准生成的,定义/声明位置决定了作用域的嵌套关系而与引用位置无关,即无论在何处引用/查找名字,都需要老老实实地回到该名字当初定义的地方去找作用域关系,然后以此为准,由内向外一层层查找
所以,搞清楚作用域的关键在于了解声明语句都可以声明在哪些地方,请看下一小节
毫无疑问,声明语句肯定是处于某一级别的代码块里,那什么是代码块呢,又是如何区分层级的呢?
代码块指的是一组的代码(声明语句肯定也是代码)集合
| 补充说明: |
| 1、同一组代码属于一个级别,同级代码会按照自上而下的顺序依次执行。 |
| 2、语法块/语句块指的就是一组声明语句的集合,与代码块其实是一个东西,不必纠结此概念 |
go语言中有用{}的显式声明的代码块,当然也有没有用{}包含的隐式代码块,主要有以下几种,
-
1、全局语法块
我们可以把整个源代码看成一个大的代码块,包含它外面的,也就是最外层的,称之为全局语法块,可以在整个程序中直接使用,例如内置的类型、内置函数、内置的常量等。
-
2、包级语法块
函数外部只能有声明语句,这一系列的声明语句构成包级语法块,可以在同一个包的任何源文件中访问
注意:首先导入语句也是声明语句,只能在包级声明,然后对于导入的包,例如在当前文件中导入的fmt包,则是对应源文件级的作用域,只能在当前的文件中访问导入的fmt包,即便是当前包的其它源文件也无法访问在当前源文件导入的包,若也想使用包fmt,需要重新导入才行
-
3、函数内的语法块
-
3.1 用{}包含的函数体代码,是函数内最外一层的代码块
-
3.2 函数内的语句如每个for、if、switch语句声明部分
-
3.3 每个for、if、switch语句{}包含的子代码
-
3.4 单纯用{}声明的块
一个例子说明
| package main |
| |
| import "fmt" |
| |
| func main() { |
| |
| var i int = 666 |
| |
| for i := 0; i < 3; i++ { |
| |
| fmt.Println(i) |
| } |
| { |
| |
| var i int = 777 |
| fmt.Println("===>", i) |
| } |
| fmt.Println("--->", i) |
| } |
总结:代码块之间有嵌套关系,也有并列关系,这些关系到了名字的访问顺序,即作用域。
再来看你一个例子强化一下理解
| |
| import "fmt" |
| |
| var len int = 666 |
| |
| func main() { |
| var len int = 777 |
| { |
| var len int = 888 |
| { |
| var len int = 999 |
| fmt.Println(len) |
| } |
| fmt.Println(len) |
| } |
| fmt.Println(len) |
| } |
再次强调静态作用域/词法域的核心是:嵌套关系是以定义/声明时为准的
| package main |
| |
| import "fmt" |
| |
| var x int = 111 |
| |
| func foo() { |
| fmt.Println(x) |
| } |
| |
| func main() { |
| var x int = 222 |
| foo() |
| fmt.Println(x) |
| } |
-
1、同一个作用域名,名字不能重复,这点写惯了python的人很容易犯错
| import "fmt" |
| |
| func main() { |
| var x int = 111 |
| var x int = 222 |
| fmt.Println(x) |
| } |
-
2、在不同的作用域确实可以声明相同的名字,但是go语言不建议这么做 (2.1中的例子只是为演示方便),因为如果名字在内部和外部的块分别声明过,则内部块的声明首先被找到,此时,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。
| package main |
| |
| import "fmt" |
| |
| func main() { |
| var fmt int = 1111 |
| fmt.Println("hello") |
| } |
-
1 函数最外层是什么?
在函数体中,函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。
| package main |
| |
| import "fmt" |
| |
| func foo(x, y int) (z int) { |
| |
| |
| |
| |
| fmt.Println(x, y, z) |
| z = x + y |
| return z |
| } |
| |
| func main() { |
| res:=foo(111, 222) |
| fmt.Println(res) |
| } |
-
2 函数体修改外部传入的值
实参通过值的方式传递,因此函数的形参是实参的拷贝。在函数内对值的修改不会影响原值。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,在函数内的修改会影响原值
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以为我们带来一些额外的效果
-
1、有名函数无需先定义后调用
有名函数必须声明在包级,包级声明在使用时无所谓顺序,所以有名函数使用,跟定义位置无关,只要定义了,在任何位置都能用,无需先定义后使用
| import "fmt" |
| |
| func main() { |
| sayHi() |
| } |
| |
| |
| func sayHi() { |
| fmt.Println("hello") |
| } |
-
2、包级声明的变量,在后面定义,也可以引用
| import "fmt" |
| |
| func main() { |
| fmt.Println(name) |
| } |
| |
| |
| var name = "hello" |
| |
| |
| |
强调:如果没有在包级,而是在函数内部,变量肯定是要先定义再使用
简短声明与赋值符号很像对不对,很容易搞混,所以要特别注意短变量声明语句的作用域范围。
例1:局部声明的重名变量会覆盖包级的
| import "fmt" |
| |
| var x int |
| |
| func main() { |
| x, y := 1, 2 |
| fmt.Println(x, y) |
| } |
例2:若想修改包级变量值,需要这么做
| package main |
| |
| import "fmt" |
| |
| var x int |
| |
| func main() { |
| var y int |
| x, y = 1, 2 |
| fmt.Println(x, y) |
| } |
上述两个例子看似简单,却是在教你解决一个非常隐晦的bug
考虑下面的程序:
| |
| var cwd string |
| |
| func init() { |
| cwd, err := os.Getwd() |
| if err != nil { |
| log.Fatalf("os.Getwd failed: %v", err) |
| } |
| } |
解析:
虽然cwd在外部的包级已经声明过,但是:=
语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。
由于当前的编译器会检测到局部声明的cwd并没有本使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。
| var cwd string |
| |
| func init() { |
| cwd, err := os.Getwd() |
| if err != nil { |
| log.Fatalf("os.Getwd failed: %v", err) |
| } |
| log.Printf("Working directory = %s", cwd) |
| } |
但此时全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。
有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用:=
的简短声明方式:
| var cwd string |
| |
| func init() { |
| var err error |
| cwd, err = os.Getwd() |
| if err != nil { |
| log.Fatalf("os.Getwd failed: %v", err) |
| } |
| } |
和for循环类似,if语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。
但是与for循环不同的是,if可以有多个子分支,而且多个子分支相当于层层嵌套关系:最开始的if声明语句属于顶级包含在整个判断语句的最外层,分支语句都属于其子级,即if里套了else if,else if里套了else if。。。套在最里层的是else
阅读下述代码,判断结果
| package main |
| |
| import "fmt" |
| |
| func main() { |
| var name string |
| fmt.Print("请输入名字:") |
| fmt.Scanln(&name) |
| |
| if x := name; x == "bigegon" { |
| fmt.Println("===>",x) |
| } else if y := "smallegonn"; x == y { |
| fmt.Println("--->", y,x) |
| } else if z := 1; z > 100 { |
| fmt.Printf("+++>") |
| } else { |
| fmt.Println("...>",x, y, z) |
| } |
| |
| } |
懂了上例之后,我们再来看一个例子
| if f, err := os.Open(fname); err != nil { |
| return err |
| } |
| f.ReadByte() |
| f.Close() |
| |
| |
通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:
| f, err := os.Open(fname) |
| if err != nil { |
| return err |
| } |
| f.ReadByte() |
| f.Close() |
你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题:
| if f, err := os.Open(fname); err != nil { |
| return err |
| } else { |
| |
| f.ReadByte() |
| f.Close() |
| } |
但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。
switch的子分支与if类似的是,每个子块都有自己的作用域:条件部分为一个隐式词法域,然后每个是每个分支的词法域。不同的是,子块彼此独立
| package main |
| |
| import "fmt" |
| |
| func main() { |
| m := 10 |
| switch m := 20; { |
| case m > 19: |
| var xxx = 111 |
| fmt.Println(xxx) |
| |
| case m > 29: |
| var yyy = 222 |
| fmt.Println(yyy) |
| |
| case m > 39: |
| var zzz = 333 |
| fmt.Println(zzz) |
| |
| default: |
| fmt.Println("===>") |
| |
| } |
| |
| fmt.Println(m) |
| } |
练习1:下述代码结果为?
| var x int = 111 |
| |
| func main() { |
| x=3333 |
| say() |
| } |
| |
| func say() { |
| fmt.Println(x) |
| } |
练习2:下述代码结果为?
| var x int = 111 |
| |
| func main() { |
| x:=222 |
| { |
| x:=333 |
| { |
| x:=444 |
| fmt.Println(x) |
| } |
| fmt.Println(x) |
| } |
| fmt.Println(x) |
| |
| say() |
| } |
| |
| func say() { |
| fmt.Println(x) |
| } |
3、下述三个x是否会冲突,请说明原因
| func main() { |
| x := "hello" |
| for _, x := range x { |
| x := x + ('A' - 'a') |
| fmt.Printf("%c", x) |
| } |
| } |
| |
| 解析: |
| 例子有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域 |
| func main() { |
| x := "hello" |
| for _, x := range x { |
| |
| x := x + ('A' - 'a') |
| fmt.Printf("%c", x) |
| } |
| } |
4、下述函数打印结果是什么?
| package main |
| |
| import "fmt" |
| |
| var x int = 111 |
| |
| func f1() { |
| fmt.Println("在f1内访问x:",x) |
| var x int = 222 |
| func() { fmt.Println("在f1的子函数内访问x:",x) }() |
| } |
| |
| func main() { |
| x := 333 |
| f1() |
| |
| fmt.Println("在main中访问x:",x) |
| } |
匿名函数就是没有名字的函数,用于临时使用一次的场景
匿名函数因为没有函数名,所以没办法像普通函数那样调用,匿名函数需要保存到某个变量或者作为立即执行函数:
| func main() { |
| |
| add := func(x, y int) { |
| fmt.Println(x + y) |
| } |
| add(10, 20) |
| |
| |
| func(x, y int) { |
| fmt.Println(x + y) |
| }(10, 20) |
| } |
匿名函数多用于实现回调函数和闭包。
闭包函数=闭函数+包函数
闭:指的是定义在函数内部的函数,注意,在Go语言中,定义在函数内部的函数必须是匿名函数
包:闭函数包含了对外层函数名字的引用
| func counter(start int) func(int) int{ |
| f:= func(n int) int{ |
| start += n |
| return start |
| } |
| return f |
| } |
| |
| func main() { |
| c:=counter(0) |
| fmt.Println(c(1)) |
| fmt.Println(c(1)) |
| fmt.Println(c(1)) |
| } |
闭包应用示例1
| func makeSuffixFunc(suffix string) func(string) string { |
| return func(name string) string { |
| if !strings.HasSuffix(name, suffix) { |
| return name + suffix |
| } |
| return name |
| } |
| } |
| |
| func main() { |
| jpgFunc := makeSuffixFunc(".jpg") |
| txtFunc := makeSuffixFunc(".txt") |
| fmt.Println(jpgFunc("test")) |
| fmt.Println(txtFunc("test")) |
| } |
闭包应用示例2
| func circle(radius float64) (func(), func()) { |
| perimeter := func() { |
| fmt.Println("周长是:",2 * math.Pi * radius) |
| } |
| area := func() { |
| fmt.Println("面积是:",math.Pi * (radius * radius)) |
| } |
| return perimeter, area |
| } |
| |
| func main() { |
| f1,f2 := circle(3.3) |
| f1() |
| f2() |
| } |
练习:
| v := "outer" |
| fmt.Println(v) |
| { |
| v := "inner" |
| fmt.Println(v) |
| { |
| fmt.Println(v) |
| } |
| } |
| { |
| fmt.Println(v) |
| } |
| fmt.Println(v) |
| |
| 输出: |
| outer |
| inner |
| inner |
| outer |
| outer |