声明、定义、初始化,这三者的关系和区别

  1. 声明(Declaration):

    • 声明是告诉编译器变量的名称和类型,但不分配内存或赋值。
    • 在Go中,可以使用 var 关键字来声明变量。

    例如:

    1
    
    var x int
    
  2. 定义(Definition)

    • 定义包括声明,并且为变量分配内存空间
    • 在Go中,声明和定义通常是同时进行的。
    1
    
    var x int // 这既是声明也是定义
    
  3. 初始化(Initialization):

    • 初始化是在定义变量的同时为其赋予一个初始值
    • 在Go中,可以在声明时直接初始化,也可以使用短变量声明来同时完成声明、定义和初始化。
    1
    2
    
    var x int = 5 // 声明、定义并初始化
    y := 10       // 短变量声明,同时完成声明、定义和初始化
    

关系和区别:

  1. 在Go中,声明和定义通常是同时发生的。当你声明一个变量时,Go会自动为其分配内存。
  2. 初始化是可选的。你可以先声明和定义一个变量,稍后再初始化它。
  3. 使用短变量声明(:=)可以一步完成声明、定义和初始化。
  4. 在某些其他语言中(如C),声明和定义可能是分开的,特别是在处理函数原型或外部变量时。
声明、定义和初始化的关系 声明 var x int 定义 var x int 初始化 x = 5 声明 + 定义 + 初始化 var x int = 5 或 x := 5

命名

Go 语言中的函数名、变量名、常量名、类型名、语句符号和包名,必须遵循一个简单的命名规则:由数字、字母、下划线组成,且必须以字母或下划线开头。

Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。

1
2
3
4
5
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

此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
          uint uint8 uint16 uint32 uint64 uintptr
          float32 float64 complex128 complex64
          bool byte rune string error

内建函数: make len cap new append copy close delete
          complex real imag
          panic recover

如果一个名字在函数内部定义,那么它只能在函数内部有效。如果在函数外部定义,那么将在当前包内的所有文件中都可以访问。

名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(函数外部定义的包级名字),那么它是可以被外部的包访问。

名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;你会经常看到i之类的短名字,而不是冗长的theLoopIndex命名。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。

在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。

声明

Go 语言属于静态编译型语言:使用变量之前需要先进行变量的声明,声明后必须被使用。

Go 语言常见的变量声明形式:

1
2
3
4
5
6
7
8
9
var a int32
var s string = "hello world"
n := 15
var (
    a string
    b int
    c bool
    d float32
)

💡在变量声明的形式上应该尽量保持项目范围内一致

Go 语言可以分为两类变量:

  • 包级别变量:在 package 级别可见。如果是导出变量,则该包级变量也可以被视为全局变量。
  • 局部变量:函数或方法体内部声明的变量,仅在函数或方法体内部可见。

包级变量的声明

包级变量只能使用带 var 关键字的变量声明形式。

  1. 声明的同时显式初始化

    1
    
    var variableName = InitExpression
    

    Go编译器会自动根据等号右侧的InitExpression表达式求值的类型确定左侧所声明变量的类型。

    如果InitExpression采用的是不带有类型信息的常量表达式,比如下面的语句

    1
    2
    
    var a = 255
    var f = 3.14
    

    则包级变量会被设置为常量表达式的默认类型:a 为 int,b 为 float64。

    如果InitExpression采用的是不带有类型信息的常量表达式,比如下面的语句:

    1
    2
    3
    4
    5
    6
    7
    
    // 第一种
    var a int32 = 18
    var f float = 3.14
    
    // 第二种
    var a = int32(18)
    var f = float(3.14)
    

    从声明一致性的角度出发,Go语言官方更推荐后者,这样就统一了接受默认类型和显式指定类型两种声明形式。尤其是在将这些变量放在一个var块中声明时,我们更青睐这样的形式:

    1
    2
    3
    4
    
    var (
    	a = 18
        f = float32(3.14)
    )
    

    而不是下面这种看起来不一致的声明形式:

    1
    2
    3
    4
    
    var (
    	a = 18
        f float32 = 3.14
    )
    
  2. 声明但延迟初始化

    对于声明时并不显式初始化的包级变量,我们使用最基本的声明形式:

    1
    2
    
    var a int32
    var f float64
    

    Go 语言会让这些变量拥有初始的零值。

  3. 声明聚类和就近原则

    声明聚类的意思是:将同一类的变量声明放在一个var块中,将不同类的声明放在不同的var块中;或者将延迟初始化的变量声明放在一个var块,而将声明并显式初始化的变量放在另一个var块中。

    就近原则指的是:变量声明尽可能在靠近第一次使用变量的位置声明该变量。但是如果一个包级变量被内部多次使用,那么还是放在头部比较合适。

局部变量的声明

与包级变量相比,局部变量多了一个短变量声明的形式。这也是局部变量采用最多的一种方式。

  1. 对于延迟初始化的局部变量声明,采用带有var关键字的声明形式

  2. 对于声明且显式初始化的局部变量,建议使用短变量声明形式

    1
    2
    3
    
    a := 17
    f := 3.14
    s := "hello, gopher!"
    

    对于不接受默认类型的变量,依然可以使用短变量声明形式,只是在“:=”右侧要进行显式转型:

    1
    2
    3
    
    a := int32(17)
    f := float32(3.14)
    s := []byte("hello, gopher!")
    

变量声明形式使用决策流程图

常量

Go 语言中常量有关的声明通过 const 来进行。

例如:

1
2
3
4
5
6
7
8
// $GOROOT/src/os/file.go
const (
    O_RDONLY int = syscall.O_RDONLY
    O_WRONLY int = syscall.O_WRONLY
    O_RDWR   int = syscall.O_RDWR
    O_APPEND int = syscall.O_APPEND
    ...
)

上面这段代码通过const声明了一组常量,这种对常量的声明方式仅仅是Go标准库中的少数个例,绝大多数情况下,Go常量在声明时并不显式指定类型,也就是说使用的是无类型常量(untyped constant)。比如:

1
2
3
4
5
6
// $GOROOT/src/io/io.go
const (
    SeekStart   = 0
    SeekCurrent = 1
    SeekEnd     = 2
)

所有常量表达式的求值计算都可以在编译期而不是在运行期完成,这样既可以减少运行时的工作,也能方便编译器进行编译优化。当操作数是常量时,在编译时也能发现一些运行时的错误,例如整数除零、字符串索引越界等。

iota

iota是go语言的常量计数器,只能在常量的表达式中使用。

iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

例:

1
2
3
4
5
6
const (
		n1 = iota //0
		n2        //1
		n3        //2
		n4        //3
	)

使用_跳过某些值

1
2
3
4
5
6
const (
		n1 = iota //0
		n2        //1
		_
		n4        //3
	)

iota声明中间插队

1
2
3
4
5
6
7
const (
		n1 = iota //0
		n2 = 100  //100
		n3 = iota //2
		n4        //3
	)
	const n5 = iota //0

定义数量级 (这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。)

1
2
3
4
5
6
7
8
const (
		_  = iota
		KB = 1 << (10 * iota)
		MB = 1 << (10 * iota)
		GB = 1 << (10 * iota)
		TB = 1 << (10 * iota)
		PB = 1 << (10 * iota)
	)

多个iota定义在一行

1
2
3
4
5
const (
		a, b = iota + 1, iota + 2 //1,2
		c, d                      //2,3
		e, f                      //3,4
	)

类型零值

当通过声明或调用new为变量分配存储空间,或者通过复合文字字面量或调用make创建新值,且不提供显式初始化时,Go会为变量或值提供默认值。

Go语言中的每个原生类型都有其默认值,这个默认值就是这个类型的零值。

  • 整型:0
  • 浮点类型:0.0
  • 布尔类型:false
  • 字符串类型:""
  • 指针、interface、slice、channel、map、function:nil
  • Go的零值初始是递归的,即数组、结构体等类型的零值初始化就是对其组成元素逐一进行零值初始化。