go面试基础知识点大全-2需要注意的坑
# go面试基础知识点大全 2
# 关于分号(Semicolons)
官方关于分号的说明 golang的正式语法是以分号作为每一条语句的终止符,但是一般情况下,程序员不需要自己输入分号,golang会自动插入分号,这就很好的解释了为什么在初始化一个结构体,为每个字段赋值时,在最后一个字段后面还要加一个逗号。
s := student{
age: 25,
name: "ben",
}
上面的例子中,如果没在"ben"后加一个逗号,那么golang会自动在"ben"后插入一个分号,从而将一个完整的语句强行拆分开了,编译就会报错。 同理,在初始化一个map时,最后一个key/value后面也要加一个逗号,例如:
m := map[string]int{
"k1": 1,
"k2": 2,
}
或者直接把最后的"}"直接放在最后一个key/value后面,如下:
m := map[string]int{
"k1": 1,
"k2": 2}
为什么"{"必须要放在一条语句的结尾,而不能另起一行,否则编译就会报错。例如,下面的语法是错误的,因为golang会自动在"i++"后面插入一个分号,把原本是一个完整的逻辑单元强行拆分开了。
for i := 0; i < 10; i++
{
fmt.Println(i)
}
正确的写法应该是:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
注意,不是所有的"{"都必须放在一行的结尾。例如下面switch的例子,"{"在另起一行的开头,语法也是正确的。根据golang spec的说明,golang只会在break、continue、fallthrough、return这四个关键字后插入分号,显然switch不在其中,所以golang不会在switch后自动插入分号,所以下面这种写法语法正确。
switch
{
case i < 10:
fmt.Println("i < 10")
case i < 20:
fmt.Println("i < 20")
default:
fmt.Println("i >= 20")
}
当然,为了风格的一贯性,还是建议将“{"写在前一行的结尾处,如下:
switch {
case i < 10:
fmt.Println("i < 10")
case i < 20:
fmt.Println("i<20")
default:
fmt.Println("i >= 20")
}
golang spec中关于分号的最后一条规则,估计很多人看不明白。这里举一个例子说明。下面的例子中,一条复杂的语句完全放在了一行,{}里面的两条语句之间必须加一个逗号,但最后一条语句后面的逗号可以省略。
for i := 0; i < 3; i++ {a:=i+1; fmt.Println(a)}
当然,最后加上一个逗号,语法也是正确的
for i := 0; i < 3; i++ {a:=i+1; fmt.Println(a);}
# 变量定义
golang的变量,既可以通过var定义,也可以直接通过":="直接赋初值的方式定义,例如下面几种方式都是合法的
var a int; a = 1
var b int = 2
var c = 3
d := 4
不管是上面哪种方式定义变量,要注意一点,变量定义之后,一定要使用,否则编译会出错。golang不允许声明未使用的变量 但是要注意,对于常量,则***允许定义未使用的常量*** ,例如,下面定义了一个常量PI,但程序中没有任何地方使用它,也是合法的。
const PI = 3.14
对于用":="这种简短模式定义变量,一定要注意几点:
- 1、:=只能用在函数内部,也就是说不能用来定义全局变量;
- 2、不用提供数据类型,编译器会自动推导,如果要提供数据类型,那就要使用"=",而不是":="。
定义变量而不提供数据类型时,golang会自动推导类型,例如,下面的例子中,golang自动推导i, j的数据类型分别为int、string。
i := 4
var j = "hello"
但是要注意,在不提供数据类型时,"="右边不能是nil,因为golang无法推导变量的类型 例如下面的定义就是非法的
i := nil
var j = nil
如果要给一个变量赋nil,那必须要指定类型,下面几种写法都正确:
type Interface interface {
Do()
}
a := Interface(nil)
var b = Interface(nil)
var c Interface = nil
var d Interface; d = nil
要注意,nil只能赋给指针、channel、函数变量(func)、interface、map、slice这几种类型的变量 所以诸如var s string = nil这种赋值语句是错误的。
在golang中,可以基于一个现有的数据类型定义一个新的类型,例如,下面的例子定义了一个新的数据类型MyInt,其底层的类型是int。
type MyInt int
在上面的例子中,虽然MyInt的底层类型是int,但是MyInt和int是两种不同的数据类型,因为golang是强类型的语言,所以MyInt类型的变量和int类型的变量之间不能相互赋值。例如,下面的例子中,第4行是非法的,编译通不过。
type MyInt int
var a MyInt
var b int = 2
a = b
但是并不意味着,所有新定义的类型和原类型的变量之间不能相互赋值,例如,下面的程序就是合法的,
type MySlice []int
var ms MySlice
var s []int = []int{1, 2, 3}
ms = s
其实golang spec对于两个不同的变量之间在什么情况下可以相互赋值,有明确的说明,如下:
其中关键是第二条规则:两个变量必须具有完全相同的底层类型,并且至少有一个不是defined type defined type有两种:
1、golang的内置类型,例如int, string, bool, float64等;
2、使用关键字type定义的类型。
回头再来看上面的例子,因为MySlice与[]int具有完全相同的底层类型,而且[]int不是defined type,所以相互赋值是合法的。
这里要区分开类型定义(defined type)和别名(alias)两个概念。为一个类型定义一个别名(alias)需要使用=。alias类型的变量和原类型的变量之间可以相互赋值 例如,下面的例子中MyInt只是int的一个别名,所以a, b之间可以相互赋值。
type MyInt = int // MyInt is a alias of int
var a MyInt
var b int = 3
a = b
每个变量都有自己的作用域,这里要注意,定义在不同作用域的两个同名变量,互不影响 例如,下面例子中变量a定义了两次,但在不同的作用域,所以互不影响,在if里面的a的值为5,而if外的a的值为3。
package main
import "fmt"
func main() {
a := 3
if a > 0 {
a, b := 5, 6
fmt.Println(a, b)
}
fmt.Println(a)
}
下面这个例子,将a+1的结果赋给b,但一定要注意,这里是用if语句之外的a参与运算,因为此时if内的变量a尚未初始化完成,所以b的值是3+1=4。
package main
import "fmt"
func main() {
a := 3
if a > 0 {
a, b := 5, a+1
fmt.Println(a, b)
}
fmt.Println(a)
}
上面的例子中出现了简单的多重赋值的场景(第8行)。下面这个例子中的多重赋值则容易让人迷惑。
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
i := 1
i, s[i+2] = 2, 10
fmt.Println(i, s)
}
对于这种多重赋值情况下的计算顺序,golang spec有明确的说明:
The assignment proceeds in two phases. First, the operands of index expressions and pointer indirections on the left and the expressions on the right are all evaluated in the usual order. Second, the assignments are carried out in left-to-right order.
简单翻译一下,分三步:
- 1、首先是计算等号左侧的"索引表达式"与"指针取址";
- 2、计算等号右边的所有表达式的值;
- 3、赋值,也即是将等号右边的值按“从左到右”的顺序赋给左边的变量; 回到前面的例子,首先计算索引表达式"i+2",结果为3;然后计算右边的表达式,因为右边都是常数,没什么计算的;最后将2赋给i,将10赋给s[3]。所以最后输出结果为:
2 [1 2 3 10 5]
一定要记住,***++和--只能放在变量的后面,而不能在变量前面*** golang中可以将一个变量的地址赋给一个指针,例如:
i := 1
p := &i
*p = 4
但是指针不能指向一个常量的地址,例如下面的写法就是错误的,编译通不过,
const A = 4
q := &A
对整型变量进行运算时要注意溢出问题。这里分两种情况,分别为无符号整数计算和有符号整数计算。先来看一个无符号整数计算的例子,
package main
import "fmt"
func main() {
var a uint8 = 100
fmt.Println(a * a)
}
上面的例子中,因为a是uint8,只有一个字节(8 bit),a*a的结果为10000,转换成二进制就是:
10 0111 0001 0000
因为***结果超过了8位,所以直接对结果进行截取,取最低的8个bit,所以结果为10000,也就是16*** 再来看一个有符号整数计算的例子,
package main
import "fmt"
func main() {
var a int8 = 127
fmt.Println(a + 1)
}
上面的例子中,a+1的结果为128,转换成二进制就是:
1000 0000
因为a是有符号整数,而且只有8个bit,计算结果最高位为1,表示负数,所以最后输出为-128。 最后再看一个有趣的计算,最后输出是什么?
package main
import "fmt"
func main() {
a := 1
a += 010
fmt.Println(a)
}
这里要注意,计算结果不是11,而是9,因为010是八进制,换成十进制就是8,所以结果是1+8=9
# 关键字和预定义标识符
golang中一共有25个关键字(keyword),分别如下:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
此外,Go语言中还有37个保留字。
Constants: true false iota nil
Types: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error
Functions: make len cap new append copy close delete complex real imag panic recover
这里只记住一点,关键字不能用来定义标识符(包括变量和类型),而预定义的标识符则可以(当然不建议这么干) 例如,下面的例子语法是正确的,因为make和nil都只是预定义标识符,可以用来定义类型和变量,
package main
import "fmt"
type make int
func main() {
var a make = 9
nil := 8
fmt.Println(nil, a)
}
而下面的例子则是非法的,因为func和break都是关键字,不能用来定义类型和变量,
package main
import "fmt"
type func int
func main() {
var a func = 9
break := 8
fmt.Println(break, a)
}
# 内置函数
golang提供了下面这些内置函数,它们都属于预定义的标识符(见上一节"预定义标识符"),
append len cap close copy delete complex real imag make new panic recover print println
由于这些内置函数都没有标准的类型,所以他们不能赋给其它的函数变量。例如,下面的例子就是非法的,因为不能将内置函数make赋给变量f,
package main
import "fmt"
func main() {
f := make
s := f([]int, 3)
fmt.Println(s)
}
正确的写法应该是:
package main
import "fmt"
func main() {
s := make([]int, 3)
fmt.Println(s)
}
于这些内置函数,每一个都有一些值得注意的点。例如len的参数可以是string、array、slice、map、channel;而cap的参数则只能是array、slice、channel。所以,下面的例子是非法的,因为cap的参数不能是map类型,
package main
import "fmt"
func main() {
m := map[string]int{}
fmt.Println(cap(m)) //invalid
}
再例如,copy函数既可以用来拷贝slice,也可以将一个string拷贝到[]byte。调用格式如下:
copy(dst, src []T) int
copy(dst []byte, src string) int
这里要注意,拷贝的长度是len(src)和len(dst)的最小值,例如,下面的例子中,s1和s2的长度分别为3和2,所以只会拷贝2个元素,返回值是实际拷贝的元素数量,也就是2。
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3}
s2 := []int{8, 9}
l := copy(s1, s2)
fmt.Println(l, s1)
}
要求用一行代码来比较两个正整数的大小,返回其中较小的值。这个题就可以利用copy巧妙的实现,示例如下:
package main
import "fmt"
func min(a, b int) int {
return copy(make([]int, a), make([]int, b))
}
func main() {
fmt.Println(min(5, 9))
}