gpt4 book ai didi

Golang常用语法糖

转载 作者:我是一只小鸟 更新时间:2023-05-12 14:32:06 34 4
gpt4 key购买 nike

1、名字由来

语法糖(Syntactic sugar)的概念是由英国计算机科学家彼得·兰丁提出的,用于表示编程语言中的某种类型的语法,这些语法不会影响功能,但使用起来却很方便。 语法糖,也称糖语法,这些语法不仅不会影响功能, 编译后的结果跟不使用语法糖也一样。 语法糖,有可能让代码编写变得简单,也有可能让代码可读性更高,但有时也会给你一个意外让您的代码出问题。Golang中的语法糖语法有很多,本文将讲解Golang中常用的语法糖.

2、Golang常用语法糖

2.1 简短变量声明 :=

规则:简短变量声明符这个语法糖使用起来很方便,导致你可能随手就会使用它定义一个变量,往往程序的bug就是随手写出来的,在这里说一下简短变量声明的原理和规则.

(1)多变量赋值可能会重新声明

使用  :=  一次可以声明多个变量,例如:

                          i, j := 0, 0
j, k := 1, 1
                        
  • 当 := 左侧存在新的变量时(如 k),那么已经声明的变量(如 j)会被重新声明。 这并没有引入新的变量,只是把变量的值改变了。
  • 当 := 左侧没有新变量编译报错。如下示例由于左侧没有新变量编译会提示" No new variables on the left side of ':=' "错误。
                                      i,j := 2,3
i,j := 6,8

                                    

(2)不能用于函数外部

  • :=  这种简短变量声明只能用于函数中,用来初始化全局变量是不行的.

  • 可以理解成  :=  会拆分成两个语句,即声明和赋值。赋值语句不能出现在函数外部的,因为在任何函数外,语句都应该以关键字开头,例如 type、var这样的关键字.

比如,像下面这样:

                                      package sugar
import fmt

rule := "Short variable declarations" // syntax error: non-declaration statement outside function body
                                    

这是因为在函数外部声明的变量是全局变量,它们具有包级别的作用域。在包级别作用域中,变量的声明通常是显式的,不需要使用短变量声明语法糖。而且在全局变量的声明中,必须指定变量的类型,这是因为编译器需要知道变量的大小和布局信息,以便在编译时为它们分配内存.

因此,如果要在包级别声明变量,需要使用 var 关键字或 const 关键字进行显式声明,不能使用 := 语法糖。例如:

                                      package main

import "fmt"

// 使用 var 关键字显式声明全局变量
var globalVar = 10

func main() {
    // 在函数内部使用 := 语法糖声明局部变量
    localVar := 20
    fmt.Println(globalVar, localVar)
}

                                    

总之,:= 只能用于局部变量的声明和初始化,而不能用于全局变量的声明和初始化,这是 Go 语言的语法规定.

(3)变量作用域问题

几乎所有的工程师都了解变量作用域,但是由于 := 使用过于频繁的话,还是有可能掉进陷阱里.

下面代码源自真实项目,但为了描述方便,也为了避免信息安全风险,简化如下:

                                      func Redeclare() {
    field, err:= nextField()   // 1号err

    if field == 1{
        field, err:= nextField()     // 2号err
        newField, err := nextField() //  3号err
        ...
    }
    ...
}
                                    

注意上面声明的三个err变量。 2号err与1号err不属于同一个作用域, := 声明了新的变量,所以2号err与1号err属于两个变量。 2号err与3号err属于同一个作用域, := 重新声明了err但没创建新的变量,所以2号err与3号err是同一个变量。(同一变量重复赋值会重新声明,这并没有引入新的变量,只是把变量的值改变了。) 。

如果误把2号err与1号err混淆,就很容易产生意想不到的错误.

2.2 可变参函数 ...

我们先写一个可变参函数:

                                  func Greeting(prefix string, who ...string) {
    if who == nil {
        fmt.Printf("Nobody to say hi.")
        return
    }

    for _, people := range who{
        fmt.Printf("%s %s\n", prefix, people)
    }
}

                                

Greeting函数负责给指定的人打招呼,其参数who为可变参数。这个函数几乎把可变参函数的特征全部表现出来了:

  • 可变参数必须在函数参数列表的最后一个(否则会引起编译时歧义); 。

  • 可变参数在函数内部是作为切片来解析的; 。

  • 可变参数可以不填,不填时函数内部当成 nil 切片处理; 。

  • 可变参数可以填入切片; 。

  • 可变参数必须是相同类型的(如果需要是不同类型的可以定义为 interface{}类型); 。

(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指针语法糖为我们做的简化,这部分在后文会详细介绍。   。

注意 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} // 数组长度是100, 下标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(...) 

                        

 参考: https://books.studygolang.com/GoExpertProgramming/chapter10/

最后此篇关于Golang常用语法糖的文章就讲到这里了,如果你想了解更多关于Golang常用语法糖的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

34 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com