说说 Go 中的变量(附粗制滥造面试题)

和其他语言没有区别,Go 中的数据也是两种表示方式,常量和变量,本文先说说变量吧。

为了增加文章的趣味性(多掉些头发),文末搜集了一些常见的面试题。部分是自己瞎编的,顺便为自己明年可能到来的面试做些准备。

什么是变量

变量是可以理解为使用一个名称绑定一个用来存放数据的内存块。

变量,首先是量,即为数据,而后是变,即表示内存块中的数据是可变的。与变量相对的是常量,常即恒常,表示数据不可变。常量的值是在编译期就确定了。

变量的定义

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 上的 “烫烫烫” 的梗,就是和变量未初始化有关。

如果定义时,指定初始值,则可以省略类型,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 中每个语法块,如 funcifforselectswitch 等,都有一个隐式的作用域。基于它,出现了很多坑死人不偿命的面试题。

一个简单的例子。

func get() int {
    return 1
}

func main() {
    if x := get();  x == 0 {
        fmt.Println(x)
    } else {
        fmt.Println(x)
    }
}

else 中也可以使用 x 变量, if 之上有个隐式作用域。

作用域这块还有很多坑,比如与 defer 结合就会产生更多叹为观止(惨绝人寰)的现象。

面试题(无解答)

1.1 如下的代码,哪些能正常编译?如果不能正常编译,如何修改?

A.

package main

import (
    "fmt"
)

func get() {
    return 1, 2
}

func main() {
    x, y := get()
    fmt.Println(x)
}

B.

package main

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)
}

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)
}
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章