Language GO II

 从本节开始,将阐述GO的基本语法,程序结构。


2.1 命名

 与C语言类似,在GO语言中,不论函数名、常量名等所有的命名都需要遵守以下规则:

1.必须以一个字母或者下划线开头;

2.大写字母与小写字母为不同符号,如变量A和变量a分别代表不同的变量;

3.不能使用以下预设关键字:
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语言中,除了以上规则外,对名字本身并无特殊长度限制,但是为了使代码可读性高,最好使用比较短小的名字。在习惯上,当定义的变量由多个单词组成时,GO语言的风格是使用驼峰式结构而非下划线分割,如FitFuntion而非Fit_Function。
  
  如果一个名字是在函数内部定义,那么它就只在函数内部有效,即类似C语言中的局部变量。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问,即类似C语言中的全局变量。

  名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。

2.2 声明

  Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明,本章中将主要探讨var类型。

package main

import "fmt"

const boilingF = 212.0

func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %g°F or %g°C\n", f, c)
// Output:
// boiling point = 212°F or 100°C
}
   以上程序中,定义了在main包中的全局常量:boilingF,和在main函数中的局部变量:f,c。访问规则与C语言类似,不再赘述。

   一个函数的声明由一个函数名字参数列表(由函数的调用者提供参数变量的具体值)一个可选的返回值列表包含函数定义的函数体组成。函数可以没有返回值,这也与C语言类似。

2.3 变量

  Go语言中运用var这个关键词来声明变量。一般语法如下:
                               var 变量名 类型  = 表达式
  其中类型“或“ = 表达式”两个部分可以省略其中的一个。省略类型会根据表达式自动推断类型,省略表达式则是根据类型分配零值。
   所以在Go语言中可以实现以下操作:
            var b, f, s = true, 2.3, "four" // bool, float64, string
即同时定义多种不同类型的变量。

  2.3.1 简短变量声明

   Go语言中可以使用":="来完成声明并赋值的操作。如:
                                               i := 100 //var i int = 100
i, j := 0, 1

简短变量声明左边的变量可能并不是全部都是刚刚声明的如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了,且简短变量声明语句中必须至少要声明一个新的变量

  2.3.2 指针

    指针的概念也与C语言中类似,指针并非是指向变量,而是指向变量的地址。 

x := 1
p := &x         // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"

    与C语言的语法大致相同,指向x的地址这一操作由&来实现,*p代表指针p指向的地址所存储的值。

    如果用“var x int”声明变量x,那么&x将产生一个指向该整数变量的指针,如p=&x指针对应的数据类型是*int,指针被称之为“指向int类型的指针”,可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时*p为p指针指向的变量的

   对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址,也就是可以有指针指向他们。

   GO语言中,指针的零值是nil。如果p指向某个有效变量,那么p != nil就会为逻辑1。Go语言中逻辑1输出结果为True,逻辑0输出结果为false。指针间如果存在互相等价/等值的关系,则意味着他们指向同一个变量地址或都为nil。如:

     var x, y int

     fmt.Println(&x == &x, &x == &y, &x == nil)   // "true false false" 

     在Go语言中,函数中引用的局部变量的地址也是可以被当作返回值的。例如下面的代码,


func f() *int {
    v := 1
    return &v
}

      在函数f的定义中,类型为*int,上文提到了指针的数据类型为本身变量类型前加一个“*”。而在函数f的调用中,我们返回了创建的局部变量v的地址,这个在GO语言中是可以被执行的,而在C语言中这是违规操作。并且因为这个v是f函数的局部变量,所以每次调用f函数时都会为v分配新的地址,所以每次调用后返回v的地址也是不同的。所以当执行以下语句时,输出会是false:

                                             fmt.Println(f() == f()) // false

     因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数那将可以在函数中通过该指针来更新变量的值

    例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,以下代码并不能被Go编译器直接执行,需要自己更改。

func incr(p *int) int {
    *p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
    return *p
}

v := 1
incr(&v)              // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)

     incr函数为int类型,其中incr定义的参数为 *int类型的指针p,++语法与C语言中一致,*p++为p所在的地址的变量的值加1。而正因为incr中所需要的参数为指针类型,所以实际使用的时候,我们用incr函数所作用的对象也必须为指针变量(*p=v中的p)或变量的地址即&v。第一次调用incr(&v)时,v的值会增加1,所以当打印再次调用incr时,输出结果就会为3    

     当我们对一个变量取地址,其实就是为原变量创建了新的代号

     例如,*p就是变量v的新代号。当我们使用指针后,我们可以不必记得原变量的名字也可以使用原变量的值,因为我们使用指针代指原变量,但是如果我们创建了太多新指针来指向同一个变量,有时就也会适得其反。

   

package main
import (
    "flag"
    "fmt"
    "strings"
)

var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")

func main() {
    flag.Parse()
    fmt.Print(strings.Join(flag.Args(), *sep))
    if !*n {
        fmt.Println()
    }
}

    Go语言中的flag库,高强度使用了指针。flag库主要为对命令行参数进行功能筛选,如linux的指令中ls -a的-a,输入-a表示展示所有。而flag库在Go语言中就是这个工作,为命令行参数提供功能选择

    上方的代码是之前echo函数的加强版。

    代码中,n变量是flag函数中的bool变量,bool变量就是真假变量,只有true or false两种值。n变量的描述中,“n”代表了,激活n这个功能,需要输入-n,false代表默认值,即如果你不输入n的情况,默认是false,也就是假,最后一个“omit trailing newline”是功能描述,即无视换行符。即,此功能的作用就是,如果你在命令行中输入-n,那么输出结果就会无视换行符。

    sep变量类似,flag.String表示sep是字符类型的变量,“s”表示需要输入-s来激活功能,“ ”表示默认是空格,“separator”表示功能是分离符。此功能即是,当你输入-s后,-s后面的第一个字符会被识别为新的分离符,即如果输入 -s / a b, 那么输出结果会是a/b,如果不输入-s,那么默认的分离符就是空格。

     flag库中,基本都是指针变量,所以调用n,sep这两个全局变量时需要加上*。main函数的功能实现,基本是flag库的函数的作用,不做特殊讲解,值得注意的就是,if那一行的if !*n, 这句话代表,当!*n为真时,执行下面语句,!表示否,否*n为真就是当n为false时为真,也就是当你不输入-n时的情况,上文中提到,-n的功能时无视换行符,那么不输入-n也就是不无视换行符,所以会调用一下println,printlin会输出一个换行符

   


  
 
  运行结果如上,可以看到输入-s后,第一个输入的字符串会被当作新的separator,而输入-n后输出结果会不换行,而当不输入-n时,输出结果会带有换行符。
  

    2.3.3. new函数

   Go语言中提供了一种新的定义变量的方法,巧妙地结合了指针的使用也就是new函数。表达式new(V)将创建一个V类型匿名变量,初始化为V类型的零值,然后返回变量地址,返回的指针类型为*T。比如:
   p := new(int)   // p, *int 类型, 指向匿名的 int 变量
 fmt.Println(*p) // "0"
 *p = 2          // 设置 int 匿名变量的值为 2
 fmt.Println(*p) // "2"
  此例中,new函数所创建的匿名变量为int类型,也可以是其他类型,匿名变量被创建后初始值为所属类型的零值。
func newInt() *int {   
return new(int) 
}

func newInt() *int {
    var dummy int
    return &dummy
}
  以上两个newInt函数实现了相同的功能,即直接返回一个匿名int变量的地址,实际使用时可以直接使用一个*int类型的p, p=newInt()来获得匿名变量的地址。

   每次调用new函数都是返回一个新的变量的地址,与2.3.3中的fmt.Printlin(f()==f())的值为false的原因一样,以下两个变量并非等值的:
   
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
  p、q虽然都是使用new(int)来获取匿名变量地址,但是每次调用new函数时,都是返回不同的变量地址,所以必然不同。
 

  2.3.4. 变量的生命周期

    某一变量的生命周期指的是程序在运行期间可以使用某一变量的阶段。对于在包级声明的变量(全局变量)来说,它们的生命周期就是从始到终。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量在内存占据的空间都可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建,这也很好地解释了为什么上文中的几个实验结果为什么会是false
   

2.4. 赋值

     使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在“=”的左边新值的表达式放在"="的右边
x = 1                       // 命名变量的赋值
*p = true                   // 通过指针间接赋值
person.name = "bob"         // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值
     以上为一些赋值语句的example。其中, a=a+b, a=a*b 如此结构的赋值语句可以被写成以下形式:
                                 a * = b
                                 a + = b
    数值变量也可以支持++递增和--递减语句,其中需要注意,++,--都是语句而非运算,所以i=v++这样的语句是绝对错误的。

    

2.4.1. 元组赋值

     元组赋值是特殊形式的赋值语句,可同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:

                             x, y = y, x

                         a[i], a[j] = a[j], a[i]
     因为Go语言会先对右边进行求值,再对左边的变量更新,所以上方的代码中,首先是提取y,x的值,再对根据顺序x,y赋值,自然也就实现了交换数值的功能,a[i]、a[j]同理,此代码在C语言中是无法被执行的。

     使用元组赋值的方法后,计算最大公约数等问题会变得十分容易解决:
        func gcd(x, y int) int {
                      for y != 0 {
                               x, y = y, x%y
                                               }
                      return x
                      }
      上方代码采取欧几里得算法,此为离散数学的基础内容,简单讲解一下原理就是A与B的最大公约数同样也是A(B)和 A、B之间的余数的最大公约数,所以不断辗转相除,直到最终余数为0时的除数就是最大公约数。
      如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。
     且使用元组赋值时左边变量的数目必须和右边一致,即便是想给两个变量使用相同的值也要在右边书写两个数值,否则会报错。

2.4.2. 可赋值性

     赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句会隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量也会产生赋值行为。例如下面的语句:
medals := []string{"gold", "silver", "bronze"}

    隐式地对slice的每个元素进行赋值操作,类似这样写的行为:

medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"
    对于两个值是否可以用==!=进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是可赋值给第一个变量的

2.5. 类型

   变量或表达式的类型定义了对应存储值在计算机存储器的特征,比如int和string的变量的存储空间的分配是不一样的。 

    我们也可以通过自己定义数据类型(基于Go语言本身提供的类型/底层类型)来提高书写程序时的可读性。

定义新数据类型时采用以下结构:

         type 类型名字 底层类型

     类型声明语句一般出现在包一级(如果不是全局可读,那么失去了定义新类型的意义),因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

      以下代码定义了两个新数据类型,华氏度与摄氏度:

// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv

import "fmt"

type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

const (
    AbsoluteZeroC Celsius = -273.15 // 绝对零度
    FreezingC     Celsius = 0       // 结冰点温度
    BoilingC      Celsius = 100     // 沸水温度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

     这个包声明了两种类型:Celsius和Fahrenheit分别对应摄氏度和华氏度。它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算

     也因如此,这两种数据不可被直接进行运算,程序的可读性和维护性都得到了提供高;因此也需要有配套的将华氏度摄氏度相互转换的函数,上文中即CToF和FToC return Fahrenheit(c*9/5 + 32)

    这一句中的Fahrenheit(c*9/5 + 32)的作用为,将最终的转换结果转化为华氏度的数据类型。Fahrenheit(t)与 Celsius(t)这样的操作,是强制数据类型转换,但是数据的值并不会发生变化,所以在括号内需要自己对数据的值根据规则进行改写。

   对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。

   底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对数学运算的支持。这意味着,Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的。

fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC)       // compile error: type mismatch

     比较运算符==<也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!

    命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。

    下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串:

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

      许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印

c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g\n", c)   // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String

2.6. 包和文件

     Go语言中的包和其他语言的库或模块的概念类似,都是为了方便模块使用代码。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,包的路径就是存储路径。
    每个包都对应一个独立的名字空间。不同包的函数,即便名字相同,功能也不尽相同。
    包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的
    那么如何使用自己创建的包呢?
    以上文的摄氏度、华氏度转换包为例。
    我们把变量的声明、对应的常量,还有方法都放到tempconv.go源文件中:
// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconv

import "fmt"

type Celsius float64
type Fahrenheit float64

const (
    AbsoluteZeroC Celsius = -273.15
    FreezingC     Celsius = 0
    BoilingC      Celsius = 100
)

func (c Celsius) String() string    { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }

     转换函数则放在另一个conv.go源文件中:

package tempconv

// CToF converts a Celsius temperature to Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

// FToC converts a Fahrenheit temperature to Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

    每个源文件都是以包的声明语句开始,用来指明包的名字。当包被导入的时候,包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包,但是conv.go源文件并没有,因为这个源文件中的代码并没有用到fmt包。

   然后就可以像一下代码一样引用包的内容。

fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"

    2.6.1. 导入包

    在Go语言程序中,每个包都有一个全局唯一的导入路径。Go编译器会在linux中命名一个叫GOPATH的变量来作为默认GO包的路径,默认路径为下,/home/scl/go/src,如果没有这个路径可以自己新建。将自己新建的包的名字作为文件夹名字,在GOPATH下新建文件夹,存储包的源文件。如/home/scl/go/src/temconv,在此路径下应该有tempconv.go和conv.go两个文件。
    除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定(package后的内容)。导入包就是import内加入包名如:temconv:
// Cf converts its numeric argument to Celsius and Fahrenheit.
package main

import (
    "fmt"
    "os"
    "strconv"

    "tempconv"
)

func main() {
    for _, arg := range os.Args[1:] {
        t, err := strconv.ParseFloat(arg, 64)
        if err != nil {
            fmt.Fprintf(os.Stderr, "cf: %v\n", err)
            os.Exit(1)
        }
        f := tempconv.Fahrenheit(t)
        c := tempconv.Celsius(t)
        fmt.Printf("%s = %s, %s = %s\n",
            f, tempconv.FToC(f), c, tempconv.CToF(c))
    }
}
       t, err := strconv.ParseFloat(arg, 64)此句中,strconv.ParseFloat函数会返回两个返回值,第一个是将命令行中输入的字符转换为Float64的数值,第二个是返回error的值,如果转换正常,error的值应该是nil。所以下一句中,if err!=nil 会起到报错的作用。
      导入语句将导入的包绑定到包内package后的名字上,然后通过该名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问tempconv包内的内容。
     最终程序运行结果如下:

     如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。这样有助于提高程序的运行速度。

     2.6.2. 包的初始化

  包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:
var a = b + c // a 第三个初始化, 为 3
var b = f()   // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1     // c 第一个初始化, 为 1

func f() int { return c + 1 }

    如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。

    对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数

func init() { /* ... */ }

     这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用

      每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了初始化工作是自下而上进行的,main包最后被初始化以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。

       

2.7. 作用域

     作用域是指代码中可以有效使用变量/函数的范围

     作用域与生命周期并不一样。作用域是代码的文本区域;它是一个编译时的属性一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念

     句法块是由花括弧所包含的一系列语句。句法块内部声明的名字是无法被外部访问或使用的。

     对全局的代码来说,存在一个整体的词法块,称为全局词法块;对于每个包;每个for、if和switch语句,也都有对应词法块;每个switch或select的分支也有独立的词法块;当然也包括显式书写的词法块(花括弧包含的语句)。

     声明语句对应的词法域决定了作用域范围的大小

     一个程序可以有很多同名变量,只要他们的声明在不同的块中即可,因为互相不可被调用,所以即使同名也无法被影响。

     当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行如果查找失败,则报告“未声明的名字”这样的错误。如果该名字被两个不同的词法域声明过,则使用两个此法域中较小的那个,如下列代码中,f会首先读取main块中的定义而忽视全局里的f():

func f() {}

var g = "g"

func main() {
    f := "f"
    fmt.Println(f) // "f"; local var f shadows package-level func f
    fmt.Println(g) // "g"; package-level var
    fmt.Println(h) // compile error: undefined: h
}

     在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。

func main() {
    x := "hello!"
    for i := 0; i < len(x); i++ {
        x := x[i]
        if x != '!' {
            x := x + 'A' - 'a'
            fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
        }
    }
}

      在x[i]x + 'A' - 'a'声明语句的初始化的表达式中都引用了上一级的x变量的定义,如x:=x[i]中,右边的x实际上是上一级的x:="hello"。

      正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则

      下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的在for循环的定义行中,隐式的定义了一个x。

func main() {
    x := "hello"
    for _, x := range x {
        x := x + 'A' - 'a'
        fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
    }
}

      和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:

if x := f(); x == 0 {
    fmt.Println(x)
} else if y := g(x); x == y {
    fmt.Println(x, y)
} else {
    fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here

      在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。

  

Comments

Popular posts from this blog

托福 TPO词汇题汇总

浮点数

缓存