更多优质内容
请关注公众号

Go入门系列(二) 变量、指针、数据类型简介和作用域-张柏沛IT博客

正文内容

Go入门系列(二) 变量、指针、数据类型简介和作用域

栏目:Go语言 系列:Go入门系列 发布时间:2020-12-06 23:54 浏览量:2734

一、声明

Go语言主要有四种类型的声明语句:var、const、type和func在包一级声明语句声明的名字可在整个包(目录)对应的每个源文件中访问,而不是仅仅在其声明语句所在的源文件中访问。相比之下,局部声明的名字就只能在函数内部很小的范围被访问。

这句话怎么理解?

 

例子1

# prepare.go

package main
import "fmt"

var name = "zbp"
const ID = "430724xxxx5018"

func main() {
    var age = 24
    fmt.Println(age)

}

这个例子中 nameID就是在包一级的地方声明的变量和常量,此时如果相同的包(本例中是main包)下不同的go文件引用这个prepare.go的时候,就可以直接使用prepare.go中的ID(变量name无法使用,因为他是小写的变量,小写变量无法导出,因此同一包下的其他文件无法引用)

 

 

二、变量

从根本上说,变量相当于是对一块数据存储 空间的命名,程序可以通过定义一个变量来申请一块数据存储空间,之后可以通过引用变量名来使用这块存储空间。

 

变量的声明

格式

var 变量名 类型 = 表达式

其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么就是声明 + 初始化,将根据初始化表达式来推导变量的类型。

如果初始化表达式被省略,那么就是单纯的声明但未初始化,将用零值初始化该变量。

数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。

例如

var v1 int
var v2 string
var v3 [10] int // 元素类型为整型的数组
var v4 [] int // 元素类型为整型的切片
var v5 struct { // 结构体
    f int
}
var v6 * int // 整型指针
var v7 map [ string ] int //key为string类型,value为int类型的哈希表
var v8 func (a int ) int  // 参数和返回值都是整型的函数

// var关键字的另一种用法是可以将若干个需要声明的变量放置在一起,免得程序员需要重复写var关键字
var (
	v1 int
	v2 string
)

//或者
var i, j, k int // 3个都是int
var b, f, s = true, 2.3, "four"  // 会自动推到类型

 

变量初始化
初始化就是声明 + 赋值。对于声明变量时需要进行初始化的场景,var关键字可以保留,但不再是必要的元素

var v1 int = 10 // 正确的使用方式1

var v2 = 10 // 正确的使用方式2,编译器可以自动推导出v2的类型

v3 := 10 // 正确的使用方式3,相当于var v3 = 10

Go语言也引入了另一个C和C++中没有的符号(冒号和等号的组合:=),作用是同时进行变量声明和初始化赋值(:=相当于是 var 声明 + 赋值操作)。

声明时指定类型已不再是必需的,Go编译器可以从初始化表达式的右值推导出该变量是哪种类型,这让Go语言看起来有点像动态类型语言,尽管Go语言实际上是不折不扣的强类型语言(静态类型语言)。

 

出现在:=左侧的变量不应该是已经被声明过的(因为:=就是声明 + 赋值,包含声明动作,go里面不能重复声明变量),否则会导致编译错误,比如:

var i int

i := 2

会导致类似如下的编译错误:

no new variables on left side of :=

 

PS::=进行初始化变量不能也不必注明类型(str string := "hello" 这样是错的),它会根据:=右边的值自动推导类型。

 

变量赋值
在Go语法中,变量初始化和变量赋值是两个不同的概念,:=是一个声明语句,而=是一个赋值语句。

v1 := 10 // 这是初始化(声明 + 赋值)

v1 = 20 // 这是赋值

如果没有进行变量声明的情况下进行变量赋值会报错

v2 = 10 // 报错

 

多重赋值

a,b = 1,2

多重初始化(声明)

a,b := 1,2

 

如果使用:=进行重复声明会报错

v1, v2 := 1, 2

v1, v2 := 3, 4

 

此时应该改为

v1, v2 := 1, 2

v1, v2 = 3, 4

 

但是如果在多重初始化的时候有1个已经声明了,一个还没声明,此时不会报错,例如

v1 := 1

v1, v2 := 3, 4

这是没有错的,此时v1就是赋值操作,v2就是声明操作。

 

三、指针 point

一个变量对应一个存储了相应值的内存空间(x := 2,x就对应存了2的内存空间)。变量不一定有名字,像x := 2,就是一个有名字的变量,但是像数组的某个元素 arr[5],这个元素其实也是一个变量,但是它就没有名字。

 

指针也算是Go中的一种数据类型

 

个指针就是一个变量对应的这个空间的存储位置(地址),所以指针就是一个内存地址。(一句话概括:变量就是一个空间,指针就是空间的地址)

并不是每一个值都会有对应的内存地址,但是每一个变量肯定都会有一个内存地址。

通过指针,我们可以在不需要知道变量名字的情况下读取或者更新对应变量的值。

 

如何表示一个指针?

var x int 声明了一个变量x,那么&x表达式会生成一个指向x的指针(是一个地址),指针对应的类型是 *int(是的,指针也有不同类型的指针,比如 *int是整型指针,*string是字符串指针,*[]string是个字符串类型的切片的指针),表示这是一个整型变量的指针。

如果我们指定指针的名字为p(即 p := &x),那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。*p表达式是p指针指向的变量的值。

 

再说一遍,变量p是内存地址,*p是内存地址所在空间的值内容,而*int是一种指针类型(是个整型指针)。

 

如果我们不想通过修改x来修改空间内的值,我们也可以使用修改 *p来改变x的值。

例子1:

x := 2
p := &x // 创建一个x的指针赋值给变量p
fmt.Println(p) // 打印出一串16进制的内容:0xc00009e058,是变量所在空间的内存地址
fmt.Println(*p) // 打印出 2
*p = 10 // 修改p指针指向的空间的值内容,会影响到x
fmt.Println(x, *p) // 10  10

 

 

对于聚合类型每个成员(比如结构体的每个成员)、或者是数组的每个元素也都是对应一个变量,因此可以被取地址。

例子2:

arr := [5]int{1,2,3,4,5}
p3 := &arr[2] // 创建一个指向arr第3个元素的指针
*p3 = 100 // 通过指针修改arr第3个元素的值
fmt.Println(arr) // [1,2,100,4,5]
slice := arr[:] // 创建一个切片
p4 := &slice[3] // 允许对切片的某个元素取址,其本质还是对数组的元素取址
fmt.Println(p4, *p4)
map1 := map[string]int{"a" : 1, "b" : 2}
pb := &map1["b"] // 报错,因为go禁止对map的元素取址,原因后面介绍到map的时候会再说

 

 

任何类型的指针的零值都是nil。如果p指向某个有效变量,那么p肯定不为nil。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。

例子3:

func f() *int{ // 定义了一个函数,返回的是一个int类型的指针
	a := 2
    return &a
}
func main() {
	fmt.Println(f() == f()) // 结果为false,原因是每次调用函数,函数内部产生的局部变量都不是同一个变量,所以这两个地址也不同。
}

 

 

如果将指针作为函数参数,那将可以在函数中通过该指针来更新变量的值。

例子4:

func add(p *int) int {
	*p++
	return *p
}
func main() {
	x := 1
	fmt.Println(add(&x)) // 返回2
	fmt.Println(x) // x也受影响,变成2,原因是p指针指向x的底层内存空间,而函数中已经通过指针p对空间的值做出了修改,所以x引用这个空间的值的时候,引用的也是被修改的值
}

 

有没有办法不通过&变量名的方式生成一个指针呢,可以用new函数,new函数接收一个参数,参数是指定的类型。new函数做的事情是在底层创建一个匿名变量并且为其初始化一个零值,最后返回这个匿名变量的指针。

p1 := new(int)  // 在底层创建一个值为0的无变量名的整型变量,并且返回它的指针
p2 := new(int)
fmt.Println(p1, *p1, p2, *p2, p1 == p2) // p1不等于p2

每次调用new函数都是返回一个新的变量的地址,因为每次调用new时都会在底层创建一个新的空间存变量值,因此上面两个地址是不同的,因为p1/p2指向的是两个空间。

new函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活。

 

在这里,要强调一点大家可能会忽略的地方:在go中,无法进行不同类型的值的比较,如果硬是要比较就会报错。

例如

fmt.Println( 1 == "2") // 报错
a := 10
b := "10"
fmt.Println(a==b) // 报错
p3 := new([5]int)
p4 := new([10]int)
fmt.Println(p3 == p4) // 报错,虽然p3和p4都是指针,但是指针内部也是有不同的类型,p3的指针类型是 *[5]int,p4的指针类型是*[10]int

//同理,不同长度的整型数组也属于不同的类型,也就是说[5]int这个数组类型和[10]int这个数组类型不能比较。
var (
	arr1 [5]int
	arr2 [10]int
)
fmt.Println(arr1 == arr2) // 报错

 

那么不同长度的整型切片呢?他们当然是同一类型(不过无法用 == 进行比较),因为不同长度的切片他们的声明表达式中的类型都是[]int

var (

    s1 []int

    s2 []int

)

但很遗憾的是切片无法用 == 进行比较,只能用循环进行比较

 

四、变量生命周期

对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建和声明。

这里稍微介绍一下堆和栈的概念,一个程序中的所有变量会被存放到内部才能的栈区或者堆区中。

包级的变量是分配在堆上面的,会在程序结束是才被系统回收。而函数内定义的局部变量则可能分配在堆上或者栈上。

简单的说,堆中的变量会生存的更久,生命周期更长;栈内的变量生命周期较短。

下面我们看一个例子

var global *int // 在包级的全局地区声明一个指针
func f() {
    var x int
    x = 1
    global = &x
}
func g() {
    y := new(int)
    *y = 1
}

f函数里的x变量必须在堆上分配,因为它在函数退出后依然要求可以通过包一级的global变量找到,虽然它是在函数内部定义的(也就是说,虽然函数已经调用结束,但是由于global变量的存在,程序可能要通过global这个指针变量找到x的内容,所以x不能在函数调用后被回收,所以要存在堆中);用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量*y不会再在其他地方再被引用到,也就是说可以马上被回收的。因此,*y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间。

 

五、 匿名变量

匿名变量一般用于接收函数的多个返回值的时候,忽略掉一些不需要的返回值。比如:

func GetName() (firstName, lastName, nickName string ) {
    return "May", "Chan", "Chibi Maruko"
}

// 我只想获得nickName,则函数调用语句可以用如下方式编写:
_, _, nickName := GetName()

_就是匿名变量

 

六、 常量

在Go语言中,常量是指编译期间就已知且不可改变的值。常量可以是数值类型(包括整型、浮点型和复数类型)、布尔类型、字符串类型等

通过const关键字定义常量,Go的常量可以是有类型的也可以是无类型的。

const Pi float64 = 3.14159265358979323846 // 类型为float64的常量

const zero = 0.0 // 无类型浮点常量

const (

size int64 = 1024

eof = -1 // 无类型整型常量

)

const u, v float32 = 0, 3  // u = 0.0, v = 3.0,常量的多重赋值

const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", 无类型整型和字符串常量

Go的常量定义可以限定类型,但不是必需的。如果定义常量时没有指定类型,那么它就是无类型常量。

常量定义的右值也可以是一个在编译期运算的常量表达式,比如

const mask = 1 << 3

由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行才能得出结果的表达式,比如试图以如下方式定义常量就会导致编译错误:

const Home = os.GetEnv("HOME")

原因很简单,os.GetEnv()只有在运行期才能知道返回结果,在编译期并不能确定,所以无法作为常量定义的右值。

Go语言预定义了这些常量:true、false和iota。 iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个 const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1。

从以下的例子可以基本理解iota的用法:

const (

//iota被重设为0

c0 = iota // c0 == 0

c1 = iota // c1 == 1

c2 = iota // c2 == 2

)

const (

a = 1 << iota // a == 1 (iota在每个const开头被重设为0)

b = 1 << iota // b == 2

c = 1 << iota // c == 4

)

const (

u = iota * 42  // u == 0

v float64 = iota * 42 // v == 42.0

w = iota * 42 // w == 84

)

const x = iota // x == 0 (因为iota又被重设为0了)

const y = iota // y == 0 (同上)

如果两个const的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式。因此,上

面的前两个const语句可简写为:

const (

// iota被重设为0

c0 = iota // c0 == 0

c1 // c1 == 1

c2 // c2 == 2

)

const (

a = 1 << iota // a == 1 (iota在每个const开头被重设为0)

b // b == 2

c // c == 4

)

以大写字母开头的常量在包外可见,小写字母的常量只能在包内使用。

 

七、数据类型(简介)
Go语言内置以下这些基础类型:

 布尔类型:bool。

 整型:int8、byte、int16、int、uint、uintptr等。

 浮点类型:float32、float64。

 复数类型:complex64、complex128。

 字符串:string。

 字符类型:rune。

 错误类型:error。

 

复合类型:

 指针(pointer)

 数组(array)

 切片(slice)

 字典(map)

 通道(chan)

 结构体(struct)

 接口(interface)

 

除了上面的go提供的原有的类型之外,go还允许我们用type关键字自定义自己的类型。

格式:

type 类型名字 底层类型

type关键字可以将一个go的原有类型包装为一个新的类型,使得新类型和原有类型不再兼容,例如下面的例子就将一个int类型封装为一个Myint类型:

package main
import "fmt"

type Myint int  // 创建一个Myint新类型,其底层类型是int

func main() {
    var x Myint = 10
    y := 10
    fmt.Println(x)
    fmt.Println(x == y) // 报错,因为Myint和int不是相同类型,不能比较
    y = x // 也会报错,因为不能把给一个变量赋值一个不同类型的变量
}

 

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

虽然int和Myint不兼容,但是我们可以将int强制转为Myint类型。例如:

var x Myint = 10
y := 100
fmt.Println(x + Myint(y))

 

每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。

数值类型之间的转型也是允许的,并且在字符串和一些特定类型的slice之间也是可以转换的。例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte类型的slice(将拷贝一个字符串数据的副本)。

我们还可以为自定义的类型定义一些该类型特有的行为,这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。

格式为:

func (变量名 自定义的类型) 方法名(其他参数 类型) (返回值 返回类型) {...}

相比于普通的函数,它多了  (变量名 自定义的类型)  这一部分

例如:

package main
import "fmt"
type Myint int
func main() {
    var x Myint = 1
    fmt.Println(x.Add(5)) // 6
}
// 为Myint这种类型定义Add这个行为(有点类似于js中的property原型方法或者说类方法)
func (x Myint) Add(n Myint) Myint {
    x += n
    return x
}

在这个例子中,调用这个Add方法的时候不会影响x本身(因为func (x Myint) ...... 的x是函数Add声明的局部变量,是main中的x的值拷贝),Add方法不是main中x变量独有的方法,而是Myint这个类型的变量公有的方法。

 

八、包和文件
对于Go语言的项目,编译工具对源码目录有严格要求,每个工作空间或者说项目目录 (workspace) 必须由bin、pkg、src三个目录组成。

src ---- 项目源码目录,里面每一个子目录,就是一个包,包内是Go语言的源码文件。

pkg ---- Go语言编译的.a 中间文件存放目录,可自动生成。

bin ---- Go语言编译可执行文件存放目录,可自动生成。

一个包可以由许多以.go为扩展名的源文件组成。包内的源文件必须进行包声明,包声明 "package + 包名" ,必须在源文件中非注释的第一行指明这个文件属于哪个包。

 

导出一个包:

在go中,我们无需可以的使用某个关键字去导出一个包。

在Go语言中根据首字母的大小写来确定可以访问的权限。如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用。该规则适用于在包一级定义的全局变量、全局常量、类型type、结构成员、函数、方法等。

可以简单的理解成,首字母大写是公有的,首字母小写是私有的。在导入包之后,你只能访问包所导出的名字(即大写的变量或函数或类型),任何未导出的名字(小写的)是不能被包外的代码访问的。

通过在命令行执行"go get + 完整包名"下载第三方库。

在执行go get 命令之前,确保你的电脑配置了环境变量GOPATH,并且安装git。

导入一个包:

包有3种导入方式

 

第一种,导入系统包。

import "fmt"

 

第二种,相对路径导入包,导入同一目录下 test 包中的内容

import "./test"

 

第三种,绝对路径导入包,导入 gopath/src/oldboy/python 包中的内容。

import "oldboy/python"

 

我们可以在导入包的时候给其指定别名,例如

导入fmt,并给他启别名f

import f "fmt"

 

将fmt启用别名".",这样就可以直接使用其内容,而不用再添加fmt,fmt.Println可以直接写成Println。

import . "fmt"

 

如果使用_作为别名

import  _ "fmt"  

表示不使用该包,而是只是调用该包的init函数。

当import时会执行fmt包中的init函数,但是导入方不能够使用被导入包的其他函数,以及被导入包内的包一级变量。

被导入的包一般不能是main包,被导入包里面的文件不能声明是main包的文件,而且不能有main()函数,例如下面的例子中的pg1.go就不能有main方法。没有main方法的go文件只能被导入到main包的文件中进行执行,而不能单独执行。

 

例子1:

在我的测试目录的src目录下有一个testPg.go和一个自定义的pg包下有一个pg1.go

// pg1.go
package pg

type Myint int // 在全局声明了一个自定义Myint类
var X Myint = 0 // 在全局声明了可导出的变量X
var z Myint = 1 // 在全局声明了不可导出的变量y

func init(){ // 定义包的初始化函数
    
}
func (x Myint) Add(n Myint) Myint { // 在包中定义一个可导出的类方法Add
    x += n
    return x
}
func (x Myint) sub(n Myint) Myint { // 在包中定义一个不可导出的类方法sub
    x -= n
    return x
}
func Calculate(number Myint) Myint { // 在包中定义一个可导出的函数Calculate
    return number.Add(5).sub(10)
}

 

// testPg.go
package main
import (
    "./pg" // 要通过相对路径的方式引入,否则会被Go认为是引入系统包,引入的时候,pg.go的init方法会自动执行
    "fmt"
)

func main() {
    var y pg.Myint = 100 // 使用pg包的Myint类型声明必须写为 pg.Myint
    fmt.Println(y, pg.X) // 想要使用pg包的X全局变量也必须写为 pg.X
    fmt.Println(y.Add(10))
    fmt.Println(pg.Calculate(y))
    //fmt.Println(y.sub(10)) // 报错:由于sub是首字母小写的,因此不可导出,所以在本文件中找不到sub方法
    //fmt.Println(pg.z) // 报错:理由同上
}

如果写为 import “pg” 而不是 import “./pg”也不会报错,前者的话,go会先到系统包找pg,找不到才会在本级目录去找。后者会直接去本级目录去找这个包。

还有,import的是一个包是一个包目录下所有的源文件而不能是一个文件。

未使用的导入包(导入了却不使用),会被编译器视为错误(除非是 import _)

 

 

九、作用域

一个词法块就是一个作用域,什么是词法块呢,对于有花括号{}包着的区域就是一个词法块,除此之外,一些没有用{}包住的地方比如函数外的全局区域也是一个词法块。

当访问一个变量的时候,默认先从当前词法块中获取,如果当前词法块中不存在这个变量就会在上一级词法块中获取,直到最顶级的作用域都找不到就会报错。

如果在本级和上级都定义了一个相同的变量,本级会覆盖上级的。

package main

import "fmt"

var x int = 10

func main() {
	fmt.Println(x)	// 10
}

 

package main

import "fmt"

var x int = 10

func main() {
	x := 100
	fmt.Println(x)	// 100
}

 

这里要注意一点:

不能在函数体外的包级作用域进行自动识别类型的赋值(:=),否则会报non-declaration statement outside function body的错误

import "fmt"

x := 10	// 报错
func main() {
	fmt.Println(x)
}

 

在一个文件的包级作用域定义的变量,函数或者常量可以在相同的包下的其他go文件中直接使用,例如:

Src目录下有pg目录和main.go文件

pg目录下有 pg1.gopg2.go

 

# pg1.go

package pg

type Myint int		// 在全局声明了一个自定义Myint类
var X Myint = 0  	// 在全局声明了可导出的变量X

func init(){	// 定义包的初始化函数
	X++
}

func (x Myint) Add(n Myint) Myint {		// 在包中定义一个可导出的类方法Add
	x += n
	return x
}

 

# pg2.go

package pg

func PrintX() {
	X = X.Add(10)		// pg2直接使用了pg1的变量X和函数Add
	print(X)	// 11
}

 

# main.go

package main

import (
	"./pg"
)

func main() {
	pg.PrintX()
}

 

这里有几个注意点:
1.    pg2.go可以直接使用pg1.go中的X变量和Add函数,因为X和Add是在pg1.go的包级作用域定义的而且pg1.go和pg2.go是在同一个包下。但是如果是非首字母大写的变量或者函数,就无法被pg2.go调用,因为小写变量和函数是非导出的变量和函数。
2.    pg2.go不能直接运行,必须在main.go中运行才行,当main文件引用pg包的时候就同时将pg1和pg2都引入了,可以通过pg.xx的方式使用pg1和pg2文件下的所有全局变量和函数。而且引入的顺序是按照文件名的顺序来引入的(先引入pg1.go再引入pg2.go)。但其实引入的顺序并不影响pg1使用pg2的变量和函数,比如pg2定义了一个A,pg1一样可以直接使用A,即使在运行main的时候是pg1先引入pg2后引入。
如何证明pg1比pg2先引入,只需在pg1和pg2都定义一个init初始化方法,打印一些标志性的文字即可验证。
3.     在运行main引入pg包(执行import的时候)的时候,pg1的init函数就执行了,所以X变为了1。然后才执行的X = PrintX(),X变为11


上面我们说了一个{}就是一个词法域,同级的不同词法域之间的变量不能相互访问,子级词法域的变量不能被父级词法域访问,父级词法域可以被子级词法域访问。
例如:

package main

import "fmt"

func main() {
	x := 100		// main词法域下的x
	if true {
		x := 10	// if词法域下的x
		fmt.Println(x)		// 10
	}
	fmt.Println(x)		// 100
}

这个代码中有以下几个词法域:包级词法域A,main函数下的词法域B,if条件处的词法域C,if代码块中的词法域D。

其中,A是B的父级词法域,B是C和D的父辈级词法域,C是D的父级词法域。

由于if {}内的词法域D(或者说作用域)单独声明了一个x,所以D词法域的x会覆盖main词法域的x,而且这个x只在if{}中有效。

之后Main词法域中又打印了一次x,这里打印的x是main词法域下的x。由于外层词法域不能访问到内层词法域if{}的变量x,所以main下打印的x是10而不是100。

画个图表示就是这样:

 

我们稍微变一下:

package main

import "fmt"

func main() {
    x := 100
    if true {
        x = 10
        fmt.Println(x)    // 10
    }
    fmt.Println(x)        // 10
}

If内的对x赋值,由于if作用域内没有声明过x就直接对x赋值,所以在if作用域找不到x,会去上一级作用域找到main的x。所以红色的地方是对main作用域下的x进行赋值,改变的是main作用域的x。所以两个地方都打印10

这个例子也充分体现了 := 和 = 的不同。

:=相当于在一个本作用域中声明一个变量(不管外层作用域是否有过这个变量)(:=只能在函数作用域中使用,不能在包级作用域下使用);而=是为已声明的变量赋值,相当于是使用一个已有变量,如果本作用域下没有会尝试往上一层作用域找。

 

我们再看一个例子:

package main

import "fmt"

func main() {
	x := "hello"
	for _, x := range x {
		x := x + 'A' - 'a'
		fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
	}
	fmt.Printf(x)		// hello
}

 

这个例子中分别有3个x在3个不同的词法域中,1号x在main词法域中,2号x在for的隐式初始化词法域,3在for循环体词法域中。画个图就是这样:

箭头明确的标注了代码中出现的7x分别是哪个作用域下的x

其中_, x := range x所在的词法域是x := x + 'A' - 'a'所在词法域的外层(父级)词法域,所以for{}内可以引用到for ...=...中的x。

 

再例如:

if f, err := os.Open(fname); err != nil {	 // compile error: unused: f
    // 要在这个代码块中用到f才行

    return err
}
f.ReadByte()
f.Close()

上面这段代码会报错,原因是if中定义的f被定义但没有被使用过。而且就算if中的f不报错,下面的f.ReadByte()也会报错,原因是外层的f未被声明。

此时的3f分别属于两个词法域下的f变量

 

我们可以做出这样的改进:

f, err := os.Open(fname)
if err != nil {
    return err
}
f.ReadByte()
f.Close()

 

请不要这样:

if f, err := os.Open(fname); err != nil {
    return err
} else {
    // f and err are visible here too
    f.ReadByte()
    f.Close()
}

这样虽然不会报错,也没有逻辑问题,但是根据代码规范,我们一般都会把错误放在if内处理,而正常运行放在主流程中执行

 

我们再看一个例子:

var cwd string

func init() {
    cwd, err := os.Getwd()   // compile error: unused: cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

还是和之前的例子一样,在两个作用域下都有一个cwd变量,这两个作用域下的cwd变量相互独立。

Init作用域下的cwd声明后没有被使用所以报错。

包级作用域下的cwd可以不在本文件内被使用,所以它不会报错。

 

解决方法是,在init中使用外层的cwd变量(因为我们本来就是要将目录下的文件信息传给全局的cwd变量的),而不是在init中又重新声明一个cwd变量

var cwd string
func init() {
	var err error
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

 

在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Go入门系列(二) 变量、指针、数据类型简介和作用域

热门推荐
推荐新闻