• Go基础语法:变量和作用域
  • 发布于 2个月前
  • 165 热度
    0 评论
  • Cactus
  • 20 粉丝 36 篇博客
  •   
和其他语言没有区别,Go 中的数据也是两种表示方式,常量和变量,本文先说说变量吧。为了增加文章的趣味性(多掉些头发),搜集了一些常见的面试题。部分是自己瞎编的,顺便为自己明年可能到来的面试做些准备。先答题,题目中附有提示,但无解答。带着问题看文章效果或许更好。

面试题
1.1 如下的代码,哪些能正常编译?如果不能正常编译,如何修改?
A.
package main
import (
    "fmt"
)
func get() {
    return 1, 2
}
func main() {
    x, y := get()
    fmt.Println(x)
}
考点:定义未使用的局部变量和使用匿名变量。

B.
package main
// 堆代码 duidaima.com
import (
    "fmt"
)
var (
    x = 1
    y := 10
)
func main() {
    fmt.Println(x)
}
考点:简短模式只能定义局部变量

C.
package main
import (
    "fmt"
)
var i int, s string = 1, "3"
func main() {
    fmt.Println(i, s)
}
考点:var 定义多个变量

1.2 下面这段代码逻辑是否正确?
package main
import (
    "fmt"
)
var p *int
func foo() (*int, error) {
    var i int = 5
    return &i, nil
}
func bar() {
    // 使用 p
    fmt.Println(*p)
}
func main() {
    p, err := foo()
    if err != nil {
        fmt.Println(err)
        return
    }
    bar()
    fmt.Println(*p)
}
考点:变量的作用域问题
注:取自 tonybai 老师的博客,原文地址。

1.3 下面哪一行变量简短定义存在已定义变量的赋值行为?
package main
import (
    "fmt"
)
func main() {
    x := 1
    fmt.Println(&x)
    if x, y := 3, 4; true {
    x = x + y
    fmt.Println(&x)
    }

    x , y := 5, 6
    x = x + y
    fmt.Println(&x)
}
考点:局部变量的作用域与简单模式的退化赋值。
题目是不是都非常简单呢?如有问题,继续看正文吧。

什么是变量
变量是可以理解为使用一个名称绑定一个用来存放数据的内存块。变量,首先是量,即为数据,而后是变,即表示内存块中的数据是可变的。与变量相对的是常量,常即恒常,表示数据不可变。常量的值是在编译期就确定了。

变量的定义
Go 中变量的定义有多种方式,先看一个变量完整的定义组成。如下:
变量名称  变量类型     变量值
var varName typeName [= Value]
var 是 Go 提供的用于定义变量的关键词,变量的定义语句可出现在函数和包级别中。语句中核心是三个部分,分别是变量的名称、类型和值。与 C/C++ 不同,Go 的变量类型是在变量名称之后。

定义一个变量:
var i int
var 除了定义单个变量,还可以一次定义多个变量。
// 相同类型简写
var i, j int
// 定义不同类型变量
var (
    i int
    s string
)

初始化
变量定义时可以指定初始值。
var i int = 1
var f float64 = 1.1
var s string = "string"
变量值的可选范围由变量类型决定。Go 是静态语言,变量类型是不可修改的。
var i int
var s string
如果变量定义时,没有指定初始值,将自动初始化为相应的零值(不同类型,零值不同),避免类似 C/C++ 中不可预测的行为。在 windows 上的 “烫烫烫” 的梗,就是和变量未初始化有关。

不同类型变量的零值如下:
数值类型:0;
布尔类型:false;
字符串类型:空字符串;
接口或引用类型(slice、指针、map、chan 和 函数): nil;

聚合类型(数组、结构体):每个元素或字段都为对应类型的零值;


如果定义时,指定初始值,则可以省略类型,Go 编译器会自动推导变量类型。
var i = 1
// 同时定义初始化两个不同类型变量
var f, s = 1.1, "string"

简短定义
在函数中,变量的定义有一种简短写法,:=。在初始化值类型明确的情况下,代替 var,实现类似动态语言的效果,懒人神器。
i := 1
变量类型由编译器根据初始化值自动推导。要注意的是,函数外的每个语句都必须以关键字开始(var, func 等),简短模式不能在函数外使用。简短模式下,如果语句左边有多个变量,其中包含已定义变量,且必须是位于当前的作用域,则已定义变量会转化为赋值行为。
var x = 1
fmt.Println(&x, x)

x, y := 10, 20
fmt.Println(&x, x)
运行代码将会发现,x 的值修改了,但地址并未改变。

多变量赋值
定义变量时,已经演示了如何同时为多个变量赋初始值。动态语言通常支持这种写法,比如 Pyhon。
x, y := 10, 20
x, y = x+10, y+20
这种语法在简化写法的同时,还有一个比较有用的点,变量交换。

通常,交换变量的写法:
t := x
x = y
y = t
引入一个临时变量实现交换。除此之外,还有两种比较常见的交换算法,不引入临时变量。
x = x + y 
y = x - y
x = x - y
或者
a = a^b
b = b^a
a = a^b
有了多变量同时赋值的特性之后,如下的写法即可完成交换。
x, y = y, x

匿名变量
Go 语言中会将定义但未使用的变量当成错误。但是有一种情况,如果 Go 的函数允许返回多个值,就要定义多个变量接收。
假设,有函数定义如下:
func row() (string, int) {
    return "poloxue", 18
}
现在 main 函数将打印第一个返回值,第二个返回值不会使用。
func main() {
    name, age := row()
    fmt.Println(name)
}
编译无法通过,提示存在未使用的变量。这时,可以使用 Go 中提供的匿名变量 _ 接收无用的返回值。
name, _ := row()
匿名变量有什么特点?匿名变量可以多次使用,不占用内存空间,解决未使用变量的报错问题。

变量作用域
变量作用域和生命周期不同,生命周期表示变量执行期间的存活时间,而作用域表示变量能有效使用的范围。除了变量有作用范围,还有诸如常量、函数、类型等都是有作用域的。Go 的作用域可分为全局和局部,变量也就有全局变量和局部变量。但细究起来,全局和局部变量的说法也不对,Go 中内置的常量、函数、类型才能算是全局。变量的全局只能算包级别,包级别变量支持访问控制,变量名首字母大写,才能在全局可用。

局部会覆盖全局,不同的作用范围可以重新定义同名变量覆盖上一级作用域的变量。
package main
// 全局变量
var i = 1
func printI() {
    // 局部变量覆盖全局变量
    i := 10
    fmt.Println(i)
}
func main() {
    fmt.Println(i)
    printI()
}
局部变量有几种情况,分别是函数的参数与返回值,函数体内部定义变量,函数内部语法块等。

函数体内部作用域的例子。
func main() {
s := "局部变量"
{
s := "语法块内部变量"
fmt.Println(s)
}
fmt.Println(s)
}
变量作用域是一个很坑的话题,Go 中每个语法块,如 func、if、for、select、switch 等,都有一个隐式的作用域。基于它,出现了很多坑死人不偿命的面试题。

一个简单的例子。
func get() int {
    return 1
}

func main() {
    if x := get();  x == 0 {
        fmt.Println(x)
    } else {
        fmt.Println(x)
    }
}
else 中也可以使用 x 变量,if 之上有个隐式作用域。作用域这块还有很多坑,比如与 defer 结合就会产生更多叹为观止(惨绝人寰)的现象。
用户评论