作用域
作用域并非函数的子知识点,之所以在此处介绍,是因为在学习完函数基本使用后,我们才可以成体系介绍它
一 作用域介绍
-
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() { // 1、此处是函数最外层一层代码块=>显式的 var i int = 666 for i := 0; i < 3; i++ { // 2、本行代码是for语句声明部分的代码块=>隐式的 // 3、此处是for循环的子代码块=>显式的 fmt.Println(i) } { // 4、此时是单纯用{}声明的一级代码块=>显式的 var i int = 777 fmt.Println("===>", i) } fmt.Println("--->", i) }
-
总结:代码块之间有嵌套关系,也有并列关系,这些关系到了名字的访问顺序,即作用域。
再来看你一个例子强化一下理解
// 可以依次注释var声明语句查看效果
import "fmt"
var len int = 666
func main() {
var len int = 777
{
var len int = 888
{
var len int = 999
fmt.Println(len) // 999
}
fmt.Println(len) // 888
}
fmt.Println(len) // 777
}
三 关于作用域强调6点
3.1 词法域的核心
再次强调静态作用域/词法域的核心是:嵌套关系是以定义/声明时为准的
package main
import "fmt"
var x int = 111
func foo() {
fmt.Println(x)
}
func main() {
var x int = 222
foo() // 调用函数,打印结果为111,而不是222
fmt.Println(x) // 打印结果为222
}
3.2 关于声明同名变量的问题
-
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") // 无法执行打印功能,因为内部的fmt屏蔽了外部 }
3.3 函数相关
-
1 函数最外层是什么?
在函数体中,函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。
package main import "fmt" func foo(x, y int) (z int) { // 最外层相当于声明了 // var x int = 111 // 接收实参111 // var y int = 222 // 接收实参222 // var z int // 初始化零值 fmt.Println(x, y, z) // 输出:111 222 0 z = x + y // 赋值操作,z被改为333 return z } func main() { res:=foo(111, 222) fmt.Println(res) // 333 }
-
2 函数体修改外部传入的值
实参通过值的方式传递,因此函数的形参是实参的拷贝。在函数内对值的修改不会影响原值。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,在函数内的修改会影响原值
3.4 包级作用域与声明顺序
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以为我们带来一些额外的效果
-
1、有名函数无需先定义后调用
有名函数必须声明在包级,包级声明在使用时无所谓顺序,所以有名函数使用,跟定义位置无关,只要定义了,在任何位置都能用,无需先定义后使用import "fmt" func main() { sayHi() } // 后定义的函数 func sayHi() { fmt.Println("hello") }
-
2、包级声明的变量,在后面定义,也可以引用
import "fmt" func main() { fmt.Println(name) } // 后定义的变量 var name = "hello" // 但是如果一个变量或常量递归引用了自身,则会产生编译错误。 // var x = x // 编译错误
强调:如果没有在包级,而是在函数内部,变量肯定是要先定义再使用
3.5 注意短变量声明语句的作用范围
简短声明与赋值符号很像对不对,很容易搞混,所以要特别注意短变量声明语句的作用域范围。
例1:局部声明的重名变量会覆盖包级的
import "fmt"
var x int
func main() {
x, y := 1, 2 // 此处是声明语句,在局部声明了一个变量x,覆盖了包
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
考虑下面的程序:
// 获取当前的工作目录然后保存到一个包级的变量中。这可以本来可以在包级直接调用os.Getwd完成,但是将这个从主逻辑中分离出来放到一个单独的函数内可能会更好(特别是在需要处理错误的时候),于是我们编写了下述代码
var cwd string // 包级变量
func init() {
cwd, err := os.Getwd() // 此处是声明语句,编译错误:cwd declared but not used
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() // 此时是声明语句,compile error: unused: cwd
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() // 此时就完全成了赋值语句,就是更新了包级的cwd
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
3.6 if的子分支
和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" { // 首先可以访问外层的name,然后此处的x声明属于if最外层
fmt.Println("===>",x)
} else if y := "smallegonn"; x == y { // 可以访问外层的x
fmt.Println("--->", y,x)
} else if z := 1; z > 100 {//上述条件都不成立会执行该分支,虽然该分支条件不成立,但肯定是声明了z
fmt.Printf("+++>")
} else {
fmt.Println("...>",x, y, z) // 可以访问x,y,z
}
//fmt.Println(x, y, z) // 无法访问x,y,z
}
懂了上例之后,我们再来看一个例子
if f, err := os.Open(fname); err != nil { // compile error: unused: f
return err
}
f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f
// 变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误
通常需要在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 and err are visible here too
f.ReadByte()
f.Close()
}
但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。
3.7 switch的子分支
switch的子分支与if类似的是,每个子块都有自己的作用域:条件部分为一个隐式词法域,然后每个是每个分支的词法域。不同的是,子块彼此独立
package main
import "fmt"
func main() {
m := 10
switch m := 20; { // 隐式的词法域,末尾必须加分号
case m > 19:
var xxx = 111
fmt.Println(xxx)
//fallthrough
case m > 29:
var yyy = 222
fmt.Println(yyy)
//fallthrough
case m > 39:
var zzz = 333
fmt.Println(zzz)
//fallthrough
default:
fmt.Println("===>")
//fmt.Println(xxx, yyy, zzz) // 打开所有的fallthrough,也无法访问xxx、yyy
}
fmt.Println(m) // 输出10
}
四 练习
练习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" // 1、函数体词法域=>显式的
for _, x := range x { // 2、本行代码是初始化词法域=>隐式的
// 3、此处是循环体词法域=>显式的
x := x + ('A' - 'a') // 'A' - 'a'为大写字母与小写字母的差值,'B' - 'b'也一样,该行代码的目的是把字母加上一定的偏移量,变为对应的大写字母
fmt.Printf("%c", x) // "HELLO"
}
}
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)
}
五 匿名函数和闭包
5.1 匿名函数
匿名函数就是没有名字的函数,用于临时使用一次的场景
func(参数)(返回值){
函数体
}
匿名函数因为没有函数名,所以没办法像普通函数那样调用,匿名函数需要保存到某个变量或者作为立即执行函数:
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
匿名函数多用于实现回调函数和闭包。
5.2 闭包
闭包函数=闭函数+包函数
闭:指的是定义在函数内部的函数,注意,在Go语言中,定义在函数内部的函数必须是匿名函数
包:闭函数包含了对外层函数名字的引用
func counter(start int) func(int) int{
f:= func(n int) int{
start += n // 修改外层函数的变量start
return start
}
return f
}
func main() {
c:=counter(0)
fmt.Println(c(1)) // 1
fmt.Println(c(1)) // 2
fmt.Println(c(1)) // 3
}
闭包应用示例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")) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}
闭包应用示例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() // 周长是: 20.734511513692635
f2() // 面积是: 34.21194399759284
}
练习:
v := "outer"
fmt.Println(v)
{
v := "inner"
fmt.Println(v)
{
fmt.Println(v)
}
}
{
fmt.Println(v)
}
fmt.Println(v)
输出:
outer
inner
inner
outer
outer