初识变量

1. 变量

在编程语言中,为了方便操作内存特定位置的数据,我们用一个特定的名字与位于特定位置的内存块绑定在一起,这个名字被称为变量。但这并不代表我们可以通过变量随意引用或修改内存,变量所绑定的内存区域是要有一个明确的边界的。也就是说,通过这样一个变量,我们究竟可以操作 4 个字节内存还是 8 个字节内存,又或是 256 个字节内存,编程语言的编译器或解释器需要明确地知道。

那么,编程语言的编译器或解释器是如何知道一个变量所能引用的内存区域边界呢?

其实,动态语言和静态语言有不同的处理方式。动态语言(比如 Python、Ruby 等)的解释器可以在运行时通过对变量赋值的分析,自动确定变量的边界。并且在动态语言中,一个变量可以在运行时被赋予大小不同的边界。

而静态编程语言在这方面的“体验略差”。静态类型语言编译器必须明确知道一个变量的边界才允许使用这个变量,但静态语言编译器又没能力自动提供这个信息,这个边界信息必须由这门语言的使用者提供,于是就有了“变量声明”。通过变量声明,语言使用者可以显式告知编译器一个变量的边界信息。在具体实现层面呢,这个边界信息由变量的类型属性赋予。

作为身处静态编程语言阵营的 Go 语言,它沿袭了静态语言的这一要求:使用变量之前需要先进行变量声明。

  • 声明
    • var 变量名 类型,注意是变量名在前面,类型在后面
    • 简易写法
    • 批量声明( 和批量导入标准库,批量申请常量一样的语法)
  • 驼峰命名法
  • 同一个作用域不能有重复声明同名变量
  • 局部变量声明了必须使用,不然会爆红,并且编译过不去。全局变量可以

2. 声明变量

2.1 单独声明

var name string
var age int
var isok bool

2.2 批量声明

var (
	name string
	age  int
	isok bool
)
var (
    a int = 128
    b int8 = 6
    s string = "hello"
    c rune = 'A'
    t bool = true
)

你看在这个变量声明块中,我们通过一个 var 关键字声明了 5 个不同类型的变量。而且,Go 语言还支持在一行变量声明中同时声明多个变量:

var a, b, c int = 5, 6, 7

这样的多变量声明同样也可以用在变量声明块中,像下面这样

var (
    a, b, c int = 5, 6, 7
    c, d, e rune = 'C', 'D', 'E'
)

这样声明方式是没有赋值的,但是有缺省值

另外,像数组、结构体这样复合类型变量的零值就是它们组成元素都为零值时的结果。

2.3 声明同时并赋值

var s1 string = "bowen"
fmt.Println(s1)

2.4 类型推导

没有声明类型,根据值自动判断

在通用的变量声明的基础上,Go 编译器允许我们省略变量声明中的类型信息,它的标准范式是“var varName = initExpression”,比如下面就是一个省略了类型信息的变量声明:

var b = 13
var s ="hello"

那么 Go 编译器在遇到这样的变量声明后是如何确定变量的类型信息呢?其实很简单,Go 编译器会根据右侧变量初值自动推导出变量的类型,并给这个变量赋予初值所对应的默认类型。比如,

  • 整型值的默认类型 int,
  • 浮点值的默认类型为 float64,
  • 复数值的默认类型为 complex128。其他类型值的默认类型就更好分辨了,

在 Go 语言中仅有唯一与之对应的类型,比如布尔值的默认类型只能是 bool,字符值默认类型只能是 rune,字符串值的默认类型只能是 string 等

如果我们不接受默认类型,而是要显式地为变量指定类型,除了通用的声明形式,我们还可以通过显式类型转型达到我们的目的

var b = int32(13)

结合多变量声明,我们可以使用这种变量声明“语法糖”声明多个不同类型的变量:

var a, b, c = 12, 'A', "hello"

在这个变量声明中,我们声明了三个变量 a、b 和 c,但它们分别具有不同的类型,分别为 int、rune 和 string。在这种变量声明语法糖中,我们省去了变量类型信息,但 Go 编译器会为我们自动推导出类型信息。

2.5 短变量声明

这种声明方式最常用,不需要var关键字,这种声明方式最接近python,在编程中经常使用,较简便
Go 语言还为我们提供了最简化的变量声明形式:短变量声明。使用短变量声明时,我们甚至可以省去 var 关键字以及类型信息,它的标准范式是“varName := initExpression”。我这里也举了几个例子:

a := 12
b := 'A'
c := "hello"
s:="bowen"  // 直接写s="bowen",x=1这样在golang是不可以的,运行时会报undefine 变量

这里我们看到,短变量声明将通用变量声明中的四个部分省去了两个,但它并没有使用赋值操作符“=”,而是使用了短变量声明专用的“:=”。这个原理和上一种省略类型信息的声明语法糖一样,短变量声明中的变量类型也是由 Go 编译器自动推导出来的。而且,短变量声明也支持一次声明多个变量,而且形式更为简洁,是这个样子的:

a, b, c := 12, 'A', "hello"

3.包级变量的声明形式

通常来说,Go 语言的变量可以分为两类:一类称为包级变量 (package varible),也就是在包级别可见的变量。如果是导出变量(大写字母开头),那么这个包级变量也可以被视为全局变量;另一类则是局部变量 (local varible),也就是 Go 函数或方法体内声明的变量,仅在函数或方法体内可见。而我们声明的所有变量都逃不开这两种

首先下个结论:包级变量只能使用带有 var 关键字的变量声明形式,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。具体这个灵活度怎么去考虑呢?我们可以从“变量声明时是否延迟初始化”这个角度,对包级变量的声明形式进行一次分类。

3.1 声明并同时显式初始化

// $GOROOT/src/io/io.go
var ErrShortWrite = errors.New("short write")
var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")

我们可以看到,这个代码块里声明的变量都是 io 包的包级变量。在 Go 标准库中,对于变量声明的同时进行显式初始化的这类包级变量,实践中多使用这种省略类型信息的“语法糖”格式:

var varName = initExpression

就像我们前面说过的那样,Go 编译器会自动根据等号右侧 InitExpression 结果值的类型,来确定左侧声明的变量的类型,这个类型会是结果值对应类型的默认类型.

声明一致性

var (
  a = 13
  b = int32(17)
  f = float32(3.14)
)
var (
  a = 13
  b int32 = 17
  f float32 = 3.14
)

可以明显看到,第一种声明方式要看起来整洁一些。

3.2 声明但延迟初始化

对于声明时并不立即显式初始化的包级变量,我们可以使用下面这种通用变量声明形式:

var a int32
var f float64

我们知道,虽然没有显式初始化,Go 语言也会让这些变量拥有初始的“零值”。如果是自定义的类型,我也建议你尽量保证它的零值是可用的。

3.3 声明聚类

Go 语言提供了变量声明块用来把多个的变量声明放在一起,并且在语法上也不会限制放置在 var 块中的声明类型,那我们就应该学会充分利用 var 变量声明块,让我们变量声明更规整,更具可读性。

通常,我们会将同一类的变量声明放在一个 var 变量声明块中,不同类的声明放在不同的 var 声明块中,比如下面就是我从标准库 net 包中摘取的两段变量声明代码:

// $GOROOT/src/net/net.go
var (
    netGo  bool 
    netCgo bool 
)

var (
    aLongTimeAgo = time.Unix(1, 0)
    noDeadline = time.Time{}
    noCancel   = (chan struct{})(nil)
)

我们可以看到,上面这两个 var 声明块各自声明了一类特定用途的包级变量。其实,我们可以将延迟初始化的变量声明放在一个 var 声明块 (比如上面的第一个 var 声明块),然后将声明且显式初始化的变量放在另一个 var 块中(比如上面的第二个 var 声明块),这里我称这种方式为“声明聚类”,声明聚类可以提升代码可读性。

3.4 就近原则

使用静态编程语言的开发人员都知道,变量声明最佳实践中还有一条:就近原则。也就是说我们尽可能在靠近第一次使用变量的位置声明这个变量。就近原则实际上也是对变量的作用域最小化的一种实现手段。在 Go 标准库中我们也很容易找到符合就近原则的变量声明的例子,比如下面这段标准库 http 包中的代码就是这样

// $GOROOT/src/net/http/request.go
var ErrNoCookie = errors.New("http: named cookie not present")
func (r *Request) Cookie(name string) (*Cookie, error) {
    for _, c := range readCookies(r.Header, name) {
        return c, nil
    }
    return nil, ErrNoCookie
}

在这个代码块里,ErrNoCookie 这个变量在整个包中仅仅被用在了 Cookie 方法中,因此它被声明在紧邻 Cookie 方法定义的地方。当然了,如果一个包级变量在包内部被多处使用,那么这个变量还是放在源文件头部声明比较适合的。

4. 局部变量的声明形式

4.1 延迟初始化变量声明

对于延迟初始化的局部变量声明,我们采用通用的变量声明形式。省略类型信息的声明和短变量声明这两种“语法糖”变量声明形式都不支持变量的延迟初始化,因此对于这类局部变量,和包级变量一样,我们只能采用通用的变量声明形式

var err error

4.2 显式初始化

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

短变量声明形式是局部变量最常用的声明形式,它遍布在 Go 标准库代码中。对于接受默认类型的变量,我们使用下面这种形式:

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

对于不接受默认类型的变量,我们依然可以使用短变量声明形式,只是在":="右侧要做一个显式转型,以保持声明的一致性:

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

这里我们还要注意:尽量在分支控制时使用短变量声明形式.

分支控制应该是 Go 中短变量声明形式应用得最广泛的场景了。在编写 Go 代码时,我们很少单独声明用于分支控制语句中的变量,而是将它与 if、for 等控制语句通过短变量声明形式融合在一起,即在控制语句中直接声明用于控制语句代码块中的变量.

看一下下面这个我摘自 Go 标准库中的代码,strings 包的 LastIndexAny 方法为我们很好地诠释了如何将短变量声明形式与分支控制语句融合在一起使用

// $GOROOT/src/strings/strings.go
func LastIndexAny(s, chars string) int {
    if chars == "" {
        // Avoid scanning all of s.
        return -1
    }
    if len(s) > 8 {
        // 作者注:在if条件控制语句中使用短变量声明形式声明了if代码块中要使用的变量as和isASCII
        if as, isASCII := makeASCIISet(chars); isASCII { 
            for i := len(s) - 1; i >= 0; i-- {
                if as.contains(s[i]) {
                    return i
                }
            }
            return -1
        }
    }
    for i := len(s); i > 0; { 
        // 作者注:在for循环控制语句中使用短变量声明形式声明了for代码块中要使用的变量c
        r, size := utf8.DecodeLastRuneInString(s[:i])
        i -= size
        for _, c := range chars {
            if r == c {
                return i
            }
        }
    }
    return -1
}

而且,短变量声明的这种融合的使用方式也体现出“就近”原则,让变量的作用域最小化

另外,虽然良好的函数 / 方法设计都讲究“单一职责”,所以每个函数 / 方法规模都不大,很少需要应用 var 块来聚类声明局部变量,但是如果你在声明局部变量时遇到了适合聚类的应用场景,你也应该毫不犹豫地使用 var 声明块来声明多于一个的局部变量,具体写法你可以参考 Go 标准库 net 包中 resolveAddrList 方法:

// $GOROOT/src/net/dial.go
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, 
                            addr string, hint Addr) (addrList, error) {
    ... ...
    var (
        tcp      *TCPAddr
        udp      *UDPAddr
        ip       *IPAddr
        wildcard bool
    )
   ... ...
}

5. 总结

良好的变量声明实践需要我们考虑多方面因素,包括明确要声明的变量是包级变量还是局部变量、是否要延迟初始化、是否接受默认类型、是否是分支控制变量并结合聚类和就近原则等。

说起来,Go 语言崇尚“做一件事只用一种方法”,但变量声明却似乎是一个例外。如果让 Go 语言的设计者重新来设计一次变量声明语法,我觉得他们很大可能不会给予开发们这么大的变量声明灵活性。作为开发者,我们要注意的是,在统一项目范围内,我们选择的变量声明的形式应该是一致的

posted @ 2022-08-28 12:12  sunnybowen  阅读(25)  评论(0编辑  收藏  举报