- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
首先我们从一个 Go 变量遮蔽( Variable Shadowing )的问题说起.
什么是变量遮蔽呢?
变量遮蔽(Variable Shadowing) 是指在程序中一个作用域内的变量名(或标识符)隐藏(遮蔽)了外部作用域中相同名称的变量。这会导致在遮蔽内部作用域内,无法直接访问外部作用域的变量,因为编译器或解释器将优先选择内部作用域的变量,而不是外部的.
我们来看下面这段示例代码:
package main
import "fmt"
var x = 10 // 包级作用域的变量
func main() {
x := 5 // 函数内的局部变量,遮蔽了包级作用域的 x
fmt.Println(x) // 输出:5
}
func anotherFunction() {
fmt.Println(x) // 在这个函数中,外部包级作用域的 x 是可见的,输出:10
}
你可以看到,在这段代码中,函数 main 内部有一个局部变量 x ,它遮蔽了包级作用域的 x 。因此,在 main 函数内部,通过变量 x 访问的是局部变量,而不是外部包级作用域的变量。然而,在 anotherFunction 中,没有局部变量 x ,因此外部包级作用域的 x 是可见的.
在Go语言中,代码块是包裹在一对大括号 {} 包围的声明和语句序列.
这些代码块是你在代码中明确可见的,由一对大括号 {} 包围。比如函数的函数体、for循环的循环体、以及其他控制结构内部的代码块。这些代码块明确定义了它们的作用域,包括变量的可见性:
func Foo() {
// 这里是显式代码块,包裹在函数的函数体内
// ...
for {
// 这里是显式代码块,包裹在for循环体内
// 该代码块也是嵌套在函数体显式代码块内部的代码块
// ...
}
if true {
// 这里是显式代码块,包裹在if语句的true分支内
// 该代码块也是嵌套在函数体显式代码块内部的代码块
// ...
}
}
隐式代码块没有显式代码块那样的肉眼可见的配对大括号包裹,我们无法通过大括号来识别隐式代码块.
虽然隐式代码块身着“隐身衣”,但我们也不是没有方法来识别它,因为 Go 语言规范对现存的几类隐式代码块做了明确的定义,我们可以看下这张图:
我们按代码块范围从大到小,逐一说明
Universe
)代码块:它囊括的范围最大,所有 Go 源码都在这个隐式代码块中,你也可以将该隐式代码块想象为在所有 Go 代码的最外层加一对大括号,就像图中最外层的那对大括号那样。 if
、 for
与 switch
。我们可以把每个控制语句都视为在它自己的隐式代码块里。不过你要注意,这里的控制语句隐式代码块与控制语句使用大括号包裹的显式代码块并不是一个代码块。你再看一下前面的图, switch
控制语句的隐式代码块的位置是在它显式代码块的外面的。 switch
或 select
语句的每个 case/default
子句中,虽然没有大括号包裹,但实质上,每个子句都自成一个代码块。 如果一对大括号内部没有任何声明或其他语句,我们就把它叫做 空代码块 .
空代码块在Go语言中是有效的,并且在某些情况下可以有一定的用途,尤其是在控制结构中,如if语句、for循环或switch语句的特定分支。它们 充当了占位符 ,允许你将来添加代码而不需要改变代码的结构.
以下是一个示例,演示了空代码块的使用:
func main() {
x := 10
if x > 5 {
// 非空代码块
fmt.Println("x 大于 5")
} else {
// 空代码块,什么都不做
}
for i := 0; i < 5; i++ {
// 空代码块,什么都不做
}
}
Go 代码块支持嵌套,我们可以在一个代码块中嵌入多个层次的代码块,如下面示例代码所示:
func foo() { //代码块1
{ // 代码块2
{ // 代码块3
{ // 代码块4
}
}
}
}
作用域的概念是 针对标识符 的,不局限于变量。 每个标识符都有自己的作用域,而一个标识符的作用域就是指这个标识符在被声明后可以被有效使用的源码区域.
显然,作用域是一个编译期的概念,也就是说,编译器在编译过程中会对每个标识符的作用域进行检查,对于在标识符作用域外使用该标识符的行为会给出编译错误的报错.
我们可以使用代码块的概念来划定每个标识符的作用域。一般划定 原则就是声明于外层代码块中的标识符,其作用域包括所有内层代码块。 而且,这一原则同时适于显式代码块与隐式代码块.
首先,我们来看看位于最外层的宇宙隐式代码块的标识符。 这一区域是 Go 语言预定义标识符的自留地 。你可以看看下面这张表是Go 语言当前版本定义里的所有预定义标识符:
由于这些预定义标识符位于包代码块的外层,所以它们的作用域是范围最大的 ,对于开发者而言,它们的作用域就是源代码中的任何位置。不过,这些预定义标识符不是关键字,我们同样可以在内层代码块中声明同名的标识符.
包顶层声明中的常量、类型、变量或函数(不包括方法)对应的标识符的作用域是包代码块.
不过,对于作用域为包代码块的标识符,我需要你知道一个特殊情况。那就是当一个包 A 导入另外一个包 B 后,包 A 仅可以使用被导入包包 B 中的导出标识符(Exported Identifier).
按照 Go 语言定义,一个标识符要成为导出标识符需同时具备两个条件:一是这个标识符声明在包代码块中,或者它是一个字段名或方法名;二是它名字第一个字符是一个大写的 Unicode 字符。这两个条件缺一不可.
// 包 A
package A
import "B"
func SomeFunction() {
// 可以访问包 B 中的导出标识符
B.ExportFunction()
}
// 这里无法访问包 B 中的非导出标识符
在Go语言中,除了大多数在包顶层声明的标识符具有包代码块范围的作用域外,还有一个特殊情况,即 导入的包名 。导入的包名的作用域是 文件代码块范围 ,这意味着它在包含它的源代码文件中可见,但对其他源文件不可见.
考虑以下示例,其中一个包A有两个源文件,它们都依赖包B中的标识符:
// 文件1:source1.go
package A
import "B"
func FunctionInSource1() {
B.SomeFunctionFromB() // 可以使用导入的包名 B
}
// 文件2:source2.go
package A
import "B"
func FunctionInSource2() {
B.AnotherFunctionFromB() // 可以使用导入的包名 B
}
在这个示例中,两个源文件都导入了包B,但每个文件内的包名 B 在 文件级别 可见。这意味着 FunctionInSource1 和 FunctionInSource2 函数都可以访问 B 包中的导出标识符(以大写字母开头的标识符),但对于其他包和源文件而言,它们不可见.
函数体内的标识符的作用域被限制在函数的开始和结束之间。这意味着函数体内的局部变量只能在函数体内部访问.
func exampleFunction() {
var localVar = 42
fmt.Println(localVar) // 可以访问局部变量 localVar
}
fmt.Println(localVar) // 这里无法访问局部变量 localVar
流程控制结构,如if语句、for循环和switch语句,也会引入新的作用域。在这些结构中声明的局部变量的作用域限制在结构内部,不会泄漏到外部.
if x := 10; x > 5 {
// x 只能在 if 语句块内访问
fmt.Println(x)
}
fmt.Println(x) // 这里无法访问 x
在上面的示例中,变量 x 在if语句内部有一个新的局部作用域,因此它只在if语句块内可见.
变量是标识符的一种,通过以上我们知道,一个变量的作用域起始于其声明所在的代码块,并且可以一直扩展到嵌入到该代码块中的所有内层代码块,而正是这样的作用域规则,成为了滋生“变量遮蔽问题”的土壤.
变量遮蔽问题的根本原因,就是内层代码块中声明了一个与外层代码块同名且同类型的变量,这样,内层代码块中的同名变量就会替代那个外层变量,参与此层代码块内的相关计算,我们也就说内层变量遮蔽了外层同名变量。现在,我们先来看一下这个示例代码,它就存在着多种变量遮蔽的问题:
... ...
var a int = 2020
func checkYear() error {
err := errors.New("wrong year")
switch a, err := getYear(); a {
case 2020:
fmt.Println("it is", a, err)
case 2021:
fmt.Println("it is", a)
err = nil
}
fmt.Println("after check, it is", a)
return err
}
type new int
func getYear() (new, error) {
var b int16 = 2021
return new(b), nil
}
func main() {
err := checkYear()
if err != nil {
fmt.Println("call checkYear error:", err)
return
}
fmt.Println("call checkYear ok")
}
这个变量遮蔽的例子还是有点复杂的,我们首先运行一下这个例子:
$go run complex.go
it is 2021
after check, it is 2020
call checkYear error: wrong year
我们可以看到,第 20 行定义的 getYear 函数返回了正确的年份 (2021),但是 checkYear 在结尾却输出“after check, it is 2020”,并且返回的 err 并非为 nil,这显然是变量遮蔽的“锅”! 。
根据我们前面给出的变量遮蔽的根本原因,看看上面这段代码究竟有几处变量遮蔽问题(包括标识符遮蔽问题).
面对上面代码,我们一眼就看到了位于第 18 行的 new,这本是 Go 语言的一个预定义标识符,但上面示例代码呢,却用 new 这个名字定义了一个新类型,于是 new 这个标识符就被遮蔽了。如果这个时候你在 main 函数下方放上下面代码:
p := new(int)
*p = 11
你就会收到 Go 编译器的错误提示:“ type int is not an expression ”,如果没有意识到 new 被遮蔽掉,这个提示就会让你不知所措。不过,在上面示例代码中,遮蔽 new 并不是示例未按预期输出结果的真实原因,我们还得继续往下看.
你看,位于第 7 行的 switch 语句在它自身的隐式代码块中,通过短变量声明形式重新声明了一个变量 a,这个变量 a 就遮蔽了外层包代码块中的包级变量 a,这就是打印“ after check, it is 2020 ”的原因。包级变量 a 没有如预期那样被 getYear 的返回值赋值为正确的年份 2021,2021 被赋值给了遮蔽它的 switch 语句隐式代码块中的那个新声明的 a.
同样还是第 7 行的 switch 语句,除了声明一个新的变量 a 之外,它还声明了一个名为 err 的变量,这个变量就遮蔽了第 4 行 checkYear 函数在显式代码块中声明的 err 变量,这导致第 12 行的 nil 赋值动作作用到了 switch 隐式代码块中的 err 变量上,而不是外层 checkYear 声明的本地变量 err 变量上,后者并非 nil,这样 checkYear 虽然从 getYear 得到了正确的年份值,但却返回了一个错误给 main 函数,这直接导致了 main 函数打印了错误:“ call checkYear error: wrong year ”.
通过这个示例,我们也可以看到,短变量声明与控制语句的结合十分容易导致变量遮蔽问题,并且很不容易识别,因此在日常 go 代码开发中你要尤其注意两者结合使用的地方.
依靠肉眼识别变量遮蔽问题终归不是长久之计,所以Go 官方提供了 go vet 工具可以用于对 Go 源码做一系列静态检查,在 Go 1.14 版以前默认支持变量遮蔽检查,Go 1.14 版之后,变量遮蔽检查的插件就需要我们单独安装了,安装方法如下:
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
安装成功后,我们就可以通过 go vet 扫描代码并检查这里面有没有变量遮蔽的问题了。我们检查一下前面的示例代码,看看效果怎么样。执行检查的命令如下:
$go vet -vettool=$(which shadow) -strict complex.go
./complex.go:13:12: declaration of "err" shadows declaration at line 11
最后此篇关于Go代码块与作用域,变量遮蔽问题详解的文章就讲到这里了,如果你想了解更多关于Go代码块与作用域,变量遮蔽问题详解的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
这是我的本地域名 http://10.10.1.101/uxsurvey/profile/dashboard 在 Controller 中,我为用户列表设置了一个操作 redirect(control
要处理 Canonical URL,最佳做法是执行 301 重定向还是更好地为 www 和非 www 域使用相同的 IP 地址? 例如: 想要的规范 URL/域是 http://example.com
1 内网基础 内网/局域网(Local Area Network,LAN),是指在某一区域内有多台计算机互联而成的计算机组,组网范围通常在数千米以内。在局域网中,可以实现文件管理、应用软件共享、打印机
1 内网基础 内网/局域网(Local Area Network,LAN),是指在某一区域内有多台计算机互联而成的计算机组,组网范围通常在数千米以内。在局域网中,可以实现文件管理、应用软件共享、打印机
我想创建一个 weblogic 集群,其中有两个托管服务器,每个服务器在物理上独立的远程计算机上运行 根据weblogic文档 All Managed Servers in a cluster mus
我正在运行 grails 3.1.4,但在创建允许我将多个域对象绑定(bind)到其他几个域对象的模式时遇到了问题。作为我正在尝试做的一个例子: 我有三个类(class)。书籍、作者和阅读列表。 作者
我试图使用@count函数来根据它获取数据,但是在没有崩溃报告的情况下它以某种方式崩溃了。 这是代码 class PSMedia: Object { @objc dynamic var id
有谁知道是否有办法只输入字母字符而不输入数字?我想过这样的事情 CREATE DOMAIN countryDomain AS VARCHAR(100) CHECK( VALUE ??? );
我的代码: const checkoutUrl = 'https://example.com/checkout/*' window.onload = startup() function st
一些不是我编写的应用程序,也不是用 PHP 编写的,它为域 www.example.com 创建了一个 cookie。 我正在尝试替换该 cookie。所以在 PHP 中我做到了: setcookie
什么是 oauth 域?是否有任何免费的 oauth 服务?我可以将它用于 StackApps registration 吗? ?我在谷歌上搜索了很多,但找不到答案。 最佳答案 这是redirect_
自从 In October 2009, the Internet Corporation for Assigned Names and Numbers (ICANN) approved the cre
我使用 apache 作为我的应用程序 Web 服务器的代理,并希望即时更改与 sessionid cookie 关联的域名。 该cookie有一个与之关联的.company.com域,我想使用apa
我只想托管一个子域到cloudflare。我不想将主域名的域名服务器更改为他们的域名服务器。真的有可能吗? 最佳答案 是的,这是可能的,但是需要通过CloudFlare合作伙伴进行设置,或者您需要采用
When using socket in the UNIX domain, it is advisable to use path name for the directory directory m
想象两个共享一个域类的 Grails 应用程序。也许是 Book 域类。 一个应用程序被标识为数据的所有者,一个应用程序必须访问域数据。类似于亚马逊和亚马逊网络服务。 我想拥有的应用程序将使用普通的域
我有一个包含字段“URL”的表单。第一部分需要用户在文本框中填写。第二部分是预定义的,显示在文本框的右侧。 例如,用户在文本框中输入“test”。第二部分预定义为“.example.com”。因此,总
如果我要关闭并取消分配 azure 中的域 Controller ,从而生成新的 vm Generationid,我需要采取哪些步骤来恢复它? 最佳答案 what steps do I need to
我想尝试使用 Azure 作为托管提供商(我有一个域)。我读过那篇文章https://learn.microsoft.com/en-us/azure/app-service-web/web-sites
所以.... 我想知道是否有人可以在这方面协助我? 基本上,我已经创建了一个自托管的Docker容器,用作构建代理(Azure DevOps) 现在,我已经开始测试代理,并且由于我们的放置文件夹位于W
我是一名优秀的程序员,十分优秀!