详解Go语言中的作用域和变量隐藏

目录
  • 前言
  • 包隐藏
  • 全局变量
    • 类型强制
    • 闭包
  • := 的情况
  • 总结

前言

变量隐藏在 Go 中可能会令人困惑,让我们尝试弄清楚。

package main

import (
	"fmt"
	"io/ioutil"
	"log"
)

func main() {

	f, err := ioutil.TempFile("", "")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	if _, err := f.Write([]byte("Hello World\n")); err != nil {
		log.Fatal(err)
	} else {
		fmt.Println("All success")
	}
}

请注意,我们首先从 TempFile 函数创建了两个变量: f 和 err 。然后我们调用 Write 丢弃写入的字节数。我们让函数在 if 语句中调用它。让我们编译,它工作正常。

$ go run main.go
All success

现在,与 if 之外的 Write 调用相同的代码:

package main

import (
	"fmt"
	"io/ioutil"
	"log"
)

func main() {

	f, err := ioutil.TempFile("", "")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	// if _, err := f.Write([]byte("Hello World\n")); err != nil {
	// 	log.Fatal(err)
	// } else {
	// 	fmt.Println("All success")
	// }

	_, err := f.Write([]byte("Hello World\n"))
	if err != nil {
		log.Fatal(err)
	} else {
		fmt.Println("All success")
	}
}

运行该代码:

$ go run main.go
# command-line-arguments
./main.go:23:9: no new variables on left side of :=

所以发生了什么事?

请注意,我们使用 := 调用 Write ,这意味着我们创建了一个新变量 err 。在第二个例子中,很明显, err 已经存在,所以我们不能重新声明它。

但是为什么它第一次起作用呢?因为在 Go 中,变量是其作用域的本地变量。在第一个示例中,我们实际上在 if 范围内隐藏了 err 。

例如:

package main

func main() {
	var err error
	_ = err
	var err error
	_ = err
}

这显然会失败,但是,如果我们限定第二个 err ,它会起作用!

package main

func main() {
	var err error
	_ = err
	{
		var err error
		_ = err
	}
}

包隐藏

考虑以下代码:

package main

import "fmt"

func Debugf(fmt string, args ...interface{}) {
	fmt.Printf(fmt, args...)
}

起初,它看起来不错。我们从 fmt 包中调用 Printf 并将 fmt 变量传递给它。

函数声明中的 fmt 字符串实际上隐藏了包,现在“只是”一个变量。编译器会抱怨:我们需要使用不同的变量名来保存对 fmt 包的访问。

全局变量

需要考虑的是,一个函数已经是一个“子作用域”,它是全局作用域内的一个作用域。这意味着您在函数中声明的任何变量都可以在全局范围内隐藏某些内容。

正如我们之前看到的,变量可以映射包,全局变量和函数的概念是相同的。

类型强制

就像我们可以用变量或函数来映射一个包一样,我们也可以用任何类型的新变量来映射一个变量。阴影变量不需要来自同一类型。这个例子编译得很好:

package main

func main() {
	var a string
	_ = a
	{
		var a int
		_ = a
	}
}

闭包

使用嵌入式函数时,作用域非常重要。函数中使用且未声明的任何变量都是对上层范围的引用。众所周知的使用 goroutine 的例子:

package main

import (
	"fmt"
	"time"
)

func main() {
	for _, elem := range []byte{'a', 'b', 'c'} {
		go func() {
			fmt.Printf("%c\n", elem)
		}()
	}
	time.Sleep(1e9) // Sleeping to give time to the goroutines to be executed.
}

运行该代码:

$ go run main.go
c
c
c

这不是我们真正想要的。这是因为范围改变了 goroutine 中引用的 elem ,因此在短列表中,它将始终显示最后一个元素。

为了避免这种情况,有两种解决方案:

1.将变量传递给函数

package main

import (
	"fmt"
	"time"
)

func main() {
	for _, elem := range []byte{'a', 'b', 'c'} {
		go func(char byte) {
			fmt.Printf("%c\n", char)
		}(elem)
	}
	time.Sleep(1e9)
}

运行结果:

$ go run main.go
a
c
b

2.在本地范围内创建变量的副本

package main

import (
	"fmt"
	"time"
)

func main() {
	for _, elem := range []byte{'a', 'b', 'c'} {
		char := elem
		go func() {
			fmt.Printf("%c\n", char)
		}()
	}
	time.Sleep(1e9)
}

运行该代码,可以得到我们想要的结果:

当我们将变量传递给函数时,我们实际上将变量的副本发送给以字符形式接收它的函数。因为每个 goroutine 都有自己的副本,所以没有问题。

当我们复制变量时,我们创建一个新变量并将 elem 的值分配给它。我们在每次迭代中都这样做,这意味着对于每个步骤,我们都会创建一个新变量, goroutine 会引用该变量。每个 goroutine 都有一个对不同变量的引用,并且它也可以正常工作。

现在,我们知道我们可以隐藏变量,为什么还要更改名称呢?我们可以简单地使用相同的名称,因为它会影响上层范围:

package main

import (
	"fmt"
	"time"
)

func main() {
	for _, elem := range []byte{'a', 'b', 'c'} {
		go func(elem byte) {
			fmt.Printf("%c\n", elem)
		}(elem)
	}
	time.Sleep(1e9)
}
package main

import (
	"fmt"
	"time"
)

func main() {
	for _, elem := range []byte{'a', 'b', 'c'} {
		elem := elem
		go func() {
			fmt.Printf("%c\n", elem)
		}()
	}
	time.Sleep(1e9)
}

当我们将变量传递给函数时,会发生同样的事情,我们将变量的副本传递给函数,该函数以名称 elem 和正确的值获取它。

在这个范围内,由于变量被遮蔽,我们无法从上层范围影响元素,所做的任何更改都将仅在此范围内应用。

当我们复制变量时,和以前一样:我们创建一个新变量并将 elem 的值分配给它。在这种情况下,新变量恰好与另一个变量具有相同的名称,但想法保持不变:新变量 + 赋值。当我们在范围内创建一个具有相同名称的新变量时,我们有效地隐藏了该变量,同时保持它的值。

:= 的情况

当 := 与多个返回函数(或类型断言、通道接收和映射访问)一起使用时,我们可以在 2 个语句中得到 3 个变量:

package main

func main() {
	var iface interface{}

	str, ok := iface.(string)
	if ok {
		println(str)
	}
	buf, ok := iface.([]byte)
	if ok {
		println(string(buf))
	}
}

在这种情况下, ok 不会被遮蔽,它只是被覆盖。这就是为什么 ok 不能改变类型。但是,在范围内这样做会隐藏变量并允许使用不同的类型:

package main

func main() {
	var m = map[string]interface{}{}

	elem, ok := m["test"]
	if ok {
		str, ok := elem.(string)
		if ok {
			println(str)
		}
	}
}

总结

隐藏可能非常有用,但需要牢记以避免意外行为。它当然是基于案例的,它通常有助于提高可读性和安全性,但也可以减少它。

在 goroutines 的例子中,因为它是一个简单的例子,它的影子更具可读性,但在更复杂的情况下,最好使用不同的名称来确定你正在修改什么。然而,另一方面,尤其是对于错误,它是一个非常强大的工具。回到我的第一个例子:

package main

import (
	"io/ioutil"
	"log"
)

func main() {
	f, err := ioutil.TempFile("", "")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	if _, err := f.Write([]byte("hello world\n")); err != nil {
		err = nil
	}
	// err is still the one form TempFile
}

在这种情况下,在 if 中隐藏 err 可以保证以前的错误不会受到影响,而如果使用相同的代码,我们在 if 中使用 = 而不是 := ,它不会隐藏变量而是覆盖错误的值。参考链接

到此这篇关于详解Go语言中的作用域和变量隐藏的文章就介绍到这了,更多相关Go语言作用域 变量隐藏内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解Go语言变量作用域

    作用域为已声明标识符所表示的常量.类型.变量.函数或包在源代码中的作用范围. Go 语言中变量可以在三个地方声明: 函数内定义的变量称为局部变量 函数外定义的变量称为全局变量 函数定义中的变量称为形式参数 接下来让我们具体了解局部变量.全局变量和形式参数. 局部变量 在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量. 以下实例中 main() 函数使用了局部变量 a, b, c: package main   import "fmt"   fu

  • Go中变量命名规则与实例

    目录 前言 命名习惯很重要 良好命名喜欢的特质 经验法则 大小写混用的变量名 变量名避免冗余 函数参数的命名 返回值的命名 方法 Receiver 的命名 导出 package 级别变量命名 接口类型 error 命名 Packages 名 结论 总结 前言 来自 Google 的 Andrew Gerrand 曾经关于 Go 中的参数命名规范进行了分享,slides 为 https://talks.golang.org/2014/names.slide 命名习惯很重要 良好的可读性是高质量代码

  • Go语言的变量定义详情

    目录 一.变量 声明变量 二.短声明 指针 三.new函数 四.变量的生命期 五.变量的作用域 一.变量 声明变量 go定义变量的方式和c,c++,java语法不一样,如下: var 变量名 类型, 比如 : var a int var在前,变量名在中间,类型在后面 我们以代码举例,如下: var i int = 0 var i = 0 var i int 以上三个表达式均是合法的,第三个表达式会将i初始化为int类型的零值,0:如果i是bool类型,则为false:i是float64类型,则为

  • Go语言变量创建的五种方法

    对于只有 Python 语言经验的朋友,也许会不太理解声明这个词,在 Python 中直接拿来就用,也不用声明类型啥的. Go 语言是静态类型语言,由于编译时,编译器会检查变量的类型,所以要求所有的变量都要有明确的类型. 变量在使用前,需要先声明.声明类型,就约定了你这个变量只能赋该类型的值. 声明一般有以下四种方法,其中前面两种同样也可用于定义常量,只需把关键字 var 变成 const 即可. 第一种:一行声明一个变量 var <name> <type> 其中 var 是关键字

  • Golang中的变量学习小结

    Golang里面变量总的来说分四大类型 1. bool,string bool:指布尔类型,也就是true, false string: 字符串类型 2. (u)int, (u)int8, (u)int16, (u)int32, (u)int64, uintptr int 和 uint, 其中有u和没有u指的是unsigned指的是有无符号,也就是有无正负号,int类型会根据你操作系统的字数来判断是32位还是64位,如果你的操作系统是64位的,那么在定义int的时候就是64位,也就是你定义int

  • 详解Go语言中的作用域和变量隐藏

    目录 前言 包隐藏 全局变量 类型强制 闭包 := 的情况 总结 前言 变量隐藏在 Go 中可能会令人困惑,让我们尝试弄清楚. package main import ( "fmt" "io/ioutil" "log" ) func main() { f, err := ioutil.TempFile("", "") if err != nil { log.Fatal(err) } defer f.Clos

  • 详解C语言中的符号常量、变量与算术表达式

    C语言中的符号常量 在结束讨论温度转换程序前,我们再来看一下符号常量.在程序中使用 300.20 等类似的"幻数"并不是一个好习惯,它们几乎无法向以后阅读该程序的人提供什么信息,而且使程序的修改变得更加困难.处理这种幻数的一种方法是赋予它们有意义的名字.#define 指令可以把符号名(或称为符号常量)定义为一个特定的字符串: #define 名字 替换文本 在该定义之后,程序中出现的所有在 #define 中定义的名字(既没有用引号引起来,也不是其它名字的一部分)都将用相应的替换文本

  • 详解Go语言中关于包导入必学的 8 个知识点

    1. 单行导入与多行导入 在 Go 语言中,一个包可包含多个 .go 文件(这些文件必须得在同一级文件夹中),只要这些 .go 文件的头部都使用 package 关键字声明了同一个包. 导入包主要可分为两种方式: 单行导入 import "fmt" import "sync" 多行导入 import( "fmt" "sync" ) 如你所见,Go 语言中 导入的包,必须得用双引号包含,在这里吐槽一下. 2. 使用别名 在一些场

  • 详解R语言中生存分析模型与时间依赖性ROC曲线可视化

    R语言简介 R是用于统计分析.绘图的语言和操作环境.R是属于GNU系统的一个自由.免费.源代码开放的软件,它是一个用于统计计算和统计制图的优秀工具. 人们通常使用接收者操作特征曲线(ROC)进行二元结果逻辑回归.但是,流行病学研究中感兴趣的结果通常是事件发生时间.使用随时间变化的时间依赖性ROC可以更全面地描述这种情况下的预测模型. 时间依赖性ROC定义 令 Mi为用于死亡率预测的基线(时间0)标量标记. 当随时间推移观察到结果时,其预测性能取决于评估时间 t.直观地说,在零时间测量的标记值应该

  • 详解R语言中的多项式回归、局部回归、核平滑和平滑样条回归模型

    在标准线性模型中,我们假设 .当线性假设无法满足时,可以考虑使用其他方法. 多项式回归 扩展可能是假设某些多项式函数, 同样,在标准线性模型方法(使用GLM的条件正态分布)中,参数  可以使用最小二乘法获得,其中  在  . 即使此多项式模型不是真正的多项式模型,也可能仍然是一个很好的近似值 .实际上,根据 Stone-Weierstrass定理,如果  在某个区间上是连续的,则有一个统一的近似值  ,通过多项式函数. 仅作说明,请考虑以下数据集 db = data.frame(x=xr,y=y

  • 详解R语言中的表达式、数学公式、特殊符号

      在R语言的绘图函数中,如果文本参数是合法的R语言表达式,那么这个表达式就被用Tex类似的规则进行文本格式化. y <- function(x) (exp(-(x^2)/2))/sqrt(2*pi) plot(y, -5, 5, main = expression(f(x) == frac(1,sqrt(2*pi))*e^(-frac(x^2,2))), lwd = 3, col = "blue") library(ggplot2) x <- seq(0, 2*pi, b

  • 详解C语言中不同类型的数据转换规则

    不同类型数据间的混合运算与类型转换 1.自动类型转换 在C语言中,自动类型转换遵循以下规则: ①若参与运算量的类型不同,则先转换成同一类型,然后进行运算 ②转换按数据长度增加的方向进行,以保证精度不降低.如int型和long型运算时,先把int量转成long型后再进行运算 a.若两种类型的字节数不同,转换成字节数高的类型 b.若两种类型的字节数相同,且一种有符号,一种无符号,则转换成无符号类型 ③所有的浮点运算都是以双精度进行的,即使是两个float单精度量运算的表达式,也要先转换成double

  • 详解C语言中二分查找的运用技巧

    目录 基础的二分查 查找左侧边界 查找右侧边界 二分查找问题分析 实例1: 爱吃香蕉的珂珂 实例2:运送包裹 前篇文章聊到了二分查找的基础以及细节的处理问题,主要介绍了 查找和目标值相等的元素.查找第一个和目标值相等的元素.查找最后一个和目标值相等的元素 三种情况. 这些情况都适用于有序数组中查找指定元素 这个基本的场景,但实际应用中可能不会这么直接,甚至看了题目之后,都不会想到可以用二分查找算法来解决 . 本文就来分析下二分查找在实际中的应用,通过分析几个应用二分查找的实例,总结下能使用二分查

  • 详解Go语言中的数据类型及类型转换

    目录 1.基本数据类型 2.基础数据类型转换 3.基本数据类型转为字符串 4.strconv的使用 5.字符串转为基础类型 1.基本数据类型 数据类型有很多,先研究一下基础的,例如:布尔型.数字类型.字符串类型. 数字类型有uint8.uint16.uint32.uint64.int8.int16.int32.int64(uint和int区别在于uint为无符号整数,即只支持正数,不支持负数形式) 数字浮点型有fload32.float64.complex64.complex126(后面两个均为

  • 详解Go语言中泛型的实现原理与使用

    目录 前言 问题 解决方法 类型约束 重获类型安全 泛型使用场景 性能 虚拟方法表 单态化 Go 的实现 结论 前言 原文:A gentle introduction to generics in Go byDominik Braun 万俊峰Kevin:我看了觉得文章非常简单易懂,就征求了作者同意,翻译出来给大家分享一下. 本文是对泛型的基本思想及其在 Go 中的实现的一个比较容易理解的介绍,同时也是对围绕泛型的各种性能讨论的简单总结.首先,我们来看看泛型所解决的核心问题. 问题 假设我们想实现

随机推荐