和其他语言没有区别,Go 中的数据也是两种表示方式,常量和变量,本文先说说变量吧。
为了增加文章的趣味性(多掉些头发),搜集了一些常见的面试题。部分是自己瞎编的,顺便为自己明年可能到来的面试做些准备。
先答题,题目中附有提示,但无解答。带着问题看文章效果或许更好。
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)
}
复制代码
考点: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 上的 "烫烫烫" 的梗,就是和变量未初始化有关。
如果定义时,指定初始值,则可以省略类型,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 结合就会产生更多叹为观止(惨绝人寰)的现象。