Golang常用语法糖
1、名字由来
语法糖(Syntactic sugar)的概念是由英国计算机科学家彼得·兰丁提出的,用于表示编程语言中的某种类型的语法,这些语法不会影响功能,但使用起来却很方便。
语法糖,也称糖语法,这些语法不仅不会影响功能,编译后的结果跟不使用语法糖也一样。
语法糖,有可能让代码编写变得简单,也有可能让代码可读性更高,但有时也会给你一个意外让您的代码出问题。Golang中的语法糖语法有很多,本文将讲解Golang中常用的语法糖。
2、Golang常用语法糖
2.1 简短变量声明 :=
规则:简短变量声明符这个语法糖使用起来很方便,导致你可能随手就会使用它定义一个变量,往往程序的bug就是随手写出来的,在这里说一下简短变量声明的原理和规则。
(1)多变量赋值可能会重新声明
使用 :=
一次可以声明多个变量,例如:
i, j := 0, 0 j, k := 1, 1
(1)使用举例-不传值
调用可变参函数时,可变参部分是可以不传值的,例如:
func ExampleGreetingWithoutParameter() { sugar.Greeting("nobody") // OutPut: // Nobody to say hi. }
这里没有传递第二个参数。可变参数不传递的话,默认为nil。
(2)使用举例-传递多个参数
调用可变参函数时,可变参数部分可以传递多个值,例如:
func ExampleGreetingWithParameter() { sugar.Greeting("hello:", "Joe", "Anna", "Eileen") // OutPut: // hello: Joe // hello: Anna // hello: Eileen }
可变参数可以有多个。多个参数将会生成一个切片传入,函数内部按照切片来处理。
(3)使用举例-传递切片
调用可变参函数时,可变参数部分可以直接传递一个切片。参数部分需要使用slice...
来表示切片。例如:
func ExampleGreetingWithSlice() { guest := []string{"Joe", "Anna", "Eileen"} sugar.Greeting("hello:", guest...) // OutPut: // hello: Joe // hello: Anna // hello: Eileen }
此时需要注意的一点是,切片传入时不会生成新的切片,也就是说函数内部使用的切片与传入的切片共享相同的存储空间。说得再直白一点就是,如果函数内部修改了切片,可能会影响外部调用的函数。
2.3 new函数
在 Go 语言中,new 函数用于动态地分配内存,返回一个指向新分配的零值的指针。它的语法如下:
func new(Type) *Type
其中,Type 表示要分配的内存的类型,new 函数返回一个指向 Type 类型的新分配的零值的指针。但是需要注意的是,new 函数只分配内存,并返回指向新分配的零值的指针,而不会初始化该内存。
所谓零值,是指 Go 语言中变量在声明时自动赋予的默认值。对于基本类型来说,它们的零值如下:
- 布尔型:false
- 整型:0
- 浮点型:0.0
- 复数型:0 + 0i
- 字符串:""(空字符串)
- 指针:nil
- 接口:nil
- 切片、映射和通道:nil
因此,new 函数返回的指针指向新分配的零值,但不会将其初始化为非零值。如果需要将内存初始化为非零值,可以使用结构体字面量或者显式地为其赋值。例如:
package main import "fmt" type Person struct { name string age int sex int } func main() { // 使用 new 函数分配内存,但不会将其初始化为非零值 p := new(Person) fmt.Println(p) // 输出:&{ 0 0} // 使用结构体字面量初始化 p2 := &Person{name: "Tom", age: 18, sex: 1} fmt.Println(p2) // 输出:&{Tom 18 1} // 显式为字段赋值 p3 := new(Person) p3.name = "Jerry" p3.age = 20 p3.sex = 0 fmt.Println(p3) // 输出:&{Jerry 20 0} }
上面的代码中,使用 new 函数分配了一个新的 Person 结构体,但不会将其初始化为非零值,因此输出结果是"空字符串 0 0"。接下来,使用结构体字面量或者显式为其赋值,将其初始化为非零值。
注意 1:p3 := new(Person) 返回是指向新分配的Person类型对象零值的指针,按照我们对指针语法的了解,基于p3显示赋值的话需要使用如下语法进行赋值:
(*p3).name = "Jerry" (*p3).age = 20 (*p3).sex = 0而我们在对指针类型结构体对象赋值的时候一般都很少会带着*,这是Go结构体自带隐式解引用,详情请参见《Golang指针隐式间接引用》这篇博文。
注意 2:new函数更多细节介绍请参见《Go语言new( )函数》这篇博文。
很明显,new函数的设计同样是为了方便程序员的使用。
2.4 声明不定长数组
我么都知道数组长度是固定的,所以在声明数组的时候都要指定长度,Go里提供了一种偷懒的声明方式,即使用...
操作符声明数组时,我们只管填充元素值,其他的由Go编译器来处理。
// Go的实现:数组长度是4,等同于 a := [4]{1, 2, 3, 4} a := [...]int{1, 2, 3, 4}
有时我们想声明一个大数组,但是某些index想设置特别的值也可以使用...操作符搞定:
a := [...]int{1: 20, 999: 10} // 数组长度是1000, 下标1的元素值是20,下标999的元素值是10,其他元素值都是0
2.5 init函数
Go语言提供了先于main函数执行的init函数,初始化每个包后会自动执行init函数,每个包中可以有多个init函数,每个包中的源文件中也可以有多个init函数,加载顺序如下:
从当前包开始,如果当前包包含多个依赖包,则先初始化依赖包,层层递归初始化各个包,在每一个包中,按照源文件的字典序从前往后执行,每一个源文件中,优先初始化常量、变量,最后初始化init函数,当出现多个init函数时,则按照顺序从前往后依次执行,每一个包完成加载后,递归返回,最后在初始化当前包!
init函数实现了sync.Once,无论包被导入多少次,init函数只会被执行一次,所以使用init可以应用在服务注册、中间件初始化、实现单例模式等等,比如我们经常使用的pprof工具(Go性能分析工具),它就使用到了init函数,在init函数里面进行路由注册:
//go/1.15.7/libexec/src/cmd/trace/pprof.go func init() { http.HandleFunc("/io", serveSVGProfile(pprofByGoroutine(computePprofIO))) http.HandleFunc("/block", serveSVGProfile(pprofByGoroutine(computePprofBlock))) http.HandleFunc("/syscall", serveSVGProfile(pprofByGoroutine(computePprofSyscall))) http.HandleFunc("/sched", serveSVGProfile(pprofByGoroutine(computePprofSched))) http.HandleFunc("/regionio", serveSVGProfile(pprofByRegion(computePprofIO))) http.HandleFunc("/regionblock", serveSVGProfile(pprofByRegion(computePprofBlock))) http.HandleFunc("/regionsyscall", serveSVGProfile(pprofByRegion(computePprofSyscall))) http.HandleFunc("/regionsched", serveSVGProfile(pprofByRegion(computePprofSched))) }
注意 1:Go中main函数和init函数的调用链关系图可以参见《(转)Go中的main函数和init函数》这篇博文。
2.6 忽略导包
Go语言在设计时有代码洁癖,在设计上尽可能避免代码滥用,所以Go语言的导包必须要使用,如果导包了但是没有使用的话就会产生编译错误,但有些场景我们会遇到只想导包,但是不使用的情况,比如上文提到的init函数,我们只想初始化包里的init函数,但是不会使用包内的任何方法,这时就可以使用 _ 操作符号重命名导入一个不使用的包:
import _ "net/http/pprof" import _ "github.com/go-sql-driver/mysql"
注意 1:忽略导包的详细使用可以参见《Golang中下划线的使用》这篇博文。
2.7 忽略字段
这个应该是最简单的用途,比如某个函数返回三个参数,但是我们只需要其中的两个,另外一个参数可以忽略,这样的话代码可以这样写:
v1, v2, _ := function(...)
2.8 类型断言
我们通常都会使用interface,一种是带方法的interface,一种是空的interface,Go1.18之前是没有泛型的,所以我们可以用空的interface{}来作为一种伪泛型使用,当我们使用到空的interface{}作为入参或返回值时,就会使用到类型断言,来获取我们所需要的类型,在Go语言中类型断言的语法格式如下:
value, ok := x.(T) or value := x.(T)
x是interface类型,T是具体的类型,方式一是安全的断言,方式二断言失败会触发panic,例如:
var a interface{} =10 t2:= a.(float64) // panic: interface conversion: interface {} is int, not float64
注意 1:类型断言的详细使用可以参见《Golang类型转换》这篇博文。
2.9 for range循环
切片/数组是我们经常使用的操作,在Go语言中提供了for range语法来快速迭代对象,数组、切片、字符串、map、channel等等都可以进行遍历,总结起来总共有三种方式:
// 方式一:只遍历不关心数据,适用于切片、数组、字符串、map、channel for range T {} // 方式二:遍历获取索引或数组,切片,数组、字符串就是索引,map就是key,channel就是数据 for key := range T{} // 方式三:遍历获取索引和数据,适用于切片、数组、字符串,第一个参数就是索引,第二个参数就是对应的元素值,map 第一个参数就是key,第二个参数就是对应的值; for key, value := range T{}
注意 1:for range更详细使用可以参见《Golang for循环遍历小坑》这篇博文。
2.10 判断map的key是否存在
import "fmt" func main() { dict := map[string]int{"asong": 1} if value, ok := dict["asong"]; ok { fmt.Printf(value) } else { fmt.Println("key:asong不存在") } }
注意 1:go map详细使用可以参见《Go语言Map》这篇博文。
2.11 select控制结构
Go语言提供了select关键字,select配合channel能够让Goroutine同时等待多个channel读或者写,在channel状态未改变之前,select会一直阻塞当前Goroutine。先看一个例子:
func fibonacci(ch chan int, done chan struct{}) { x, y := 0, 1 for { select { case ch <- x: x, y = y, x+y case <-done: fmt.Println("over") return } } } func main() { ch := make(chan int) done := make(chan struct{}) go func() { for i := 0; i < 10; i++ { fmt.Println(<-ch) } done <- struct{}{} }() fibonacci(ch, done) }
select与switch具有相似的控制结构,与switch不同的是,select中的case中的表达式必须是channel的收发操作,当select中的两个case同时被触发时,会随机执行其中的一个。为什么是随机执行的呢?随机的引入就是为了避免饥饿问题的发生,如果我们每次都是按照顺序依次执行的,若两个case一直都是满足条件的,那么后面的case永远都不会执行。
上面例子中的select用法是阻塞式的收发操作,直到有一个channel发生状态改变。我们也可以在select中使用default语句,那么select语句在执行时会遇到这两种情况:
- 当存在可以收发的Channel时,直接处理该Channel 对应的case;
- 当不存在可以收发的Channel 时,执行default 中的语句;
注意:nil channel上的操作会一直被阻塞,如果没有default case,只有nil channel的select会一直被阻塞。
2.12 强制的代码风格
Go语言中,左括号必须紧接着语句不换行。其他样式的括号将被视为代码编译错误。这个特性刚开始会使开发者有一些不习惯,但随着对Go语言的不断熟悉,开发者就会发现风格统一让大家在阅读代码时把注意力集中到了解决问题上,而不是代码风格上。同时Go语言也提供了一套格式化工具。一些Go语言的开发环境或者编辑器在保存时,都会使用格式化工具对代码进行格式化,让代码提交时已经是统一格式的代码。
3、总结
语法糖能让程序员使用更简练的言语表达较复杂的含义,它的本质是编译器做了额外的处理逻辑。