GO-C-Java-student

 

1. 介绍与安装

Golang 是什么

Go 亦称为 Golang(按照 Rob Pike 说法,语言叫做 Go,Golang 只是官方网站的网址),是由谷歌开发的一个开源的编译型的静态语言。

Golang 的主要关注点是使得高可用性和可扩展性的 Web 应用的开发变得简便容易。(Go 的定位是系统编程语言,只是对 Web 开发支持较好)

为何选择 Golang

既然有很多其他编程语言可以做同样的工作,如 Python,Ruby,Nodejs 等,为什么要选择 Golang 作为服务端编程语言?

以下是我使用 Go 语言时发现的一些优点:

  • 并发是语言的一部分(并非通过标准库实现),所以编写多线程程序会是一件很容易的事。后续教程将会讨论到,并发是通过 Goroutines 和 channels 机制实现的。
  • Golang 是一种编译型语言。源代码会编译为二进制机器码。而在解释型语言中没有这个过程,如 Nodejs 中的 JavaScript。
  • 语言规范十分简洁。所有规范都在一个页面展示,你甚至都可以用它来编写你自己的编译器呢。
  • Go 编译器支持静态链接。所有 Go 代码都可以静态链接为一个大的二进制文件(相对现在的磁盘空间,其实根本不大),并可以轻松部署到云服务器,而不必担心各种依赖性。

安装

Golang 支持三个平台:Mac,Windows 和 Linux(译注:不只是这三个,也支持其他主流平台)。你可以在 golang.org/dl/中下载相应平台的二进制文件。(因为众所周知的原因,如果下载不了,请到 studygolang.com/dl下载)

Mac OS

在 golang.org/dl/下载安装程序。双击开始安装并且遵循安装提示,会将 Golang 安装到 /usr/local/go目录下,同时 /usr/local/go/bin文件夹也会被添加到 PATH环境变量中。

Windows

在 golang.org/dl/下载 MSI 安装程序。双击开始安装并且遵循安装提示,会将 Golang 安装到 C:\Go目录下,同时 c:\Go\bin目录也会被添加到你的 PATH环境变量中。

Linux

在 golang.org/dl/下载 tar 文件,并解压到 /usr/local

请添加 /usr/local/go/bin到 PATH环境变量中。Go 就已经成功安装在 Linux上了。

2. Hello World

建立 Go 工作区

在编写代码之前,我们首先应该建立 Go 的工作区(Workspace)。

在 Mac 或 Linux操作系统下,Go 工作区应该设置在 HOME/go**。所以我们要在 **HOME目录下创建 go目录。

而在 Windows下,工作区应该设置在 C:\Users\YourName\go。所以请将 go目录放置在 C:\Users\YourName

其实也可以通过设置 GOPATH 环境变量,用其他目录来作为工作区。但为了简单起见,我们采用上面提到的放置方法。

所有 Go 源文件都应该放置在工作区里的 src目录下。请在刚添加的 go目录下面创建目录 src

所有 Go 项目都应该依次在 src 里面设置自己的子目录。我们在 src 里面创建一个目录 hello来放置整个 hello world 项目。

创建上述目录之后,其目录结构如下:

go
  src
    hello
复制代码

在我们刚刚创建的 hello 目录下,在 helloworld.go文件里保存下面的程序。

package main

import "fmt"

func main() {  
    fmt.Println("Hello World")
}
复制代码

创建该程序之后,其目录结构如下:

go
  src
    hello
      helloworld.go
复制代码

运行 Go 程序

运行 Go 程序有多种方式,我们下面依次介绍。

1.使用 go run命令 - 在命令提示符旁,输入 go run workspacepath/src/hello/helloworld.go

上述命令中的 workspacepath应该替换为你自己的工作区路径(Windows 下的 C:/Users/YourName/go,Linux 或 Mac 下的 $HOME/go)。

在控制台上会看见 Hello World的输出。

2.使用 go install命令 - 运行 go install hello,接着可以用 workspacepath/bin/hello来运行该程序。

上述命令中的 workspacepath应该替换为你自己的工作区路径(Windows 下的 C:/Users/YourName/go,Linux 或 Mac 下的 $HOME/go)。

当你输入 go install hello时,go 工具会在工作区中搜索 hello 包(hello 称之为包,我们后面会更加详细地讨论包)。接下来它会在工作区的 bin 目录下,创建一个名为 hello(Windows 下名为 hello.exe)的二进制文件。运行 go install hello后,其目录结构如下所示:

go
  bin
    hello
  src
    hello
      helloworld.go
复制代码

3.第 3 种运行程序的好方法是使用 go playground。尽管它有自身的限制,但该方法对于运行简单的程序非常方便。我已经在 playground 上创建了一个 hello world 程序。点击这里在线运行程序。 你可以使用 go playground与其他人分享你的源代码。

简述 hello world 程序

下面就是我们刚写下的 hello world 程序。

package main //1

import "fmt" //2

func main() { //3  
    fmt.Println("Hello World") //4
}
复制代码

现在简单介绍每一行大概都做了些什么,在以后的教程中还会深入探讨每个部分。

package main - 每一个 Go 文件都应该在开头进行 package name 的声明(译注:只有可执行程序的包名应当为 main)。包(Packages)用于代码的封装与重用,这里的包名称是main

import "fmt"- 我们引入了 fmt 包,用于在 main 函数里面打印文本到标准输出。

func main()- main 是一个特殊的函数。整个程序就是从 main 函数开始运行的。main 函数必须放置在 main 包中{和 }分别表示 main 函数的开始和结束部分。

fmt.Println("Hello World")fmt包中的 Println函数用于把文本写入标准输出。

3. 变量

变量是什么

变量指定了某存储单元(Memory Location)的名称,该存储单元会存储特定类型的值。在 Go 中,有多种语法用于声明变量。

声明单个变量

var name type是声明单个变量的语法。

package main

import "fmt"

func main() {
    var age int // 变量声明
    fmt.Println("my age is", age)
}
复制代码

语句 var age int声明了一个 int 类型的变量,名字为 age。我们还没有给该变量赋值。如果变量未被赋值,Go 会自动地将其初始化,赋值该变量类型的零值(Zero Value)。本例中 age 就被赋值为 0。如果你运行该程序,你会看到如下输出:

my age is 0
复制代码

变量可以赋值为本类型的任何值。上一程序中的 age 可以赋值为任何整型值(Integer Value)。

package main

import "fmt"

func main() {
    var age int // 变量声明
    fmt.Println("my age is", age)
    age = 29 // 赋值
    fmt.Println("my age is", age)
    age = 54 // 赋值
    fmt.Println("my new age is", age)
}
复制代码

上面的程序会有如下输出:

my age is  0  
my age is 29  
my new age is 54
复制代码

声明变量并初始化

声明变量的同时可以给定初始值。 var name type = initialvalue的语法用于声明变量并初始化。

package main

import "fmt"

func main() {
    var age int = 29 // 声明变量并初始化

    fmt.Println("my age is", age)
}
复制代码

在上面的程序中,age 是具有初始值 29 的 int 类型变量。如果你运行上面的程序,你可以看见下面的输出,证实 age 已经被初始化为 29。

my age is 29
复制代码

类型推断(Type Inference)

如果变量有初始值,那么 Go 能够自动推断具有初始值的变量的类型。因此,如果变量有初始值,就可以在变量声明中省略 type

如果变量声明的语法是 var name = initialvalue,Go 能够根据初始值自动推断变量的类型。

在下面的例子中,你可以看到在第 6 行,我们省略了变量 age的 int类型,Go 依然推断出了它是 int 类型。

package main

import "fmt"

func main() {
    var age = 29 // 可以推断类型

    fmt.Println("my age is", age)
}
复制代码

声明多个变量

Go 能够通过一条语句声明多个变量。

声明多个变量的语法是 var name1, name2 type = initialvalue1, initialvalue2

package main

import "fmt"

func main() {
    var width, height int = 100, 50 // 声明多个变量

    fmt.Println("width is", width, "height is", heigh)
}
复制代码

上述程序将在标准输出打印 width is 100 height is 50

你可能已经想到,如果 width 和 height 省略了初始化,它们的初始值将赋值为 0。

package main

import "fmt"

func main() {  
    var width, height int
    fmt.Println("width is", width, "height is", height)
    width = 100
    height = 50
    fmt.Println("new width is", width, "new height is ", height)
}
复制代码

上面的程序将会打印:

width is 0 height is 0  
new width is 100 new height is  50
复制代码

在有些情况下,我们可能会想要在一个语句中声明不同类型的变量。其语法如下:

var (  
    name1 = initialvalue1,
    name2 = initialvalue2
)
复制代码

使用上述语法,下面的程序声明不同类型的变量。

package main

import "fmt"

func main() {
    var (
        name   = "naveen"
        age    = 29
        height int
    )
    fmt.Println("my name is", name, ", age is", age, "and height is", height)
}
复制代码

这里我们声明了 string 类型的 name、int 类型的 age 和 height(我们将会在下一教程中讨论 golang 所支持的变量类型)。运行上面的程序会产生输出 my name is naveen , age is 29 and height is 0

简短声明

Go 也支持一种声明变量的简洁形式,称为简短声明(Short Hand Declaration),该声明使用了 :=操作符。

声明变量的简短语法是 name := initialvalue

package main

import "fmt"

func main() {  
    name, age := "naveen", 29 // 简短声明

    fmt.Println("my name is", name, "age is", age)
}
复制代码

运行上面的程序,可以看到输出为 my name is naveen age is 29

简短声明要求 :=操作符左边的所有变量都有初始值。下面程序将会抛出错误 cannot assign 1 values to 2 variables,这是因为 age 没有被赋值

package main

import "fmt"

func main() {  
    name, age := "naveen" //error

    fmt.Println("my name is", name, "age is", age)
}
复制代码

简短声明的语法要求 :=操作符的左边至少有一个变量是尚未声明的。考虑下面的程序:

package main

import "fmt"

func main() {
    a, b := 20, 30 // 声明变量a和b
    fmt.Println("a is", a, "b is", b)
    b, c := 40, 50 // b已经声明,但c尚未声明
    fmt.Println("b is", b, "c is", c)
    b, c = 80, 90 // 给已经声明的变量b和c赋新值
    fmt.Println("changed b is", b, "c is", c)
}
复制代码

在上面程序中的第 8 行,由于 b 已经被声明,而 c 尚未声明,因此运行成功并且输出:

a is 20 b is 30  
b is 40 c is 50  
changed b is 80 c is 90
复制代码

但是如果我们运行下面的程序:

package main

import "fmt"

func main() {  
    a, b := 20, 30 // 声明a和b
    fmt.Println("a is", a, "b is", b)
    a, b := 40, 50 // 错误,没有尚未声明的变量
}

复制代码

上面运行后会抛出 no new variables on left side of :=的错误,这是因为 a 和 b 的变量已经声明过了,:=的左边并没有尚未声明的变量。

变量也可以在运行时进行赋值。考虑下面的程序:

package main

import (  
    "fmt"
    "math"
)

func main() {  
    a, b := 145.8, 543.8
    c := math.Min(a, b)
    fmt.Println("minimum value is ", c)
}

复制代码

在上面的程序中,c 的值是运行过程中计算得到的,即 a 和 b 的最小值。上述程序会打印:

minimum value is  145.8

复制代码

由于 Go 是强类型(Strongly Typed)语言,因此不允许某一类型的变量赋值为其他类型的值。下面的程序会抛出错误 cannot use "naveen" (type string) as type int in assignment,这是因为 age 本来声明为 int 类型,而我们却尝试给它赋字符串类型的值。

package main

func main() {  
    age := 29      // age是int类型
    age = "naveen" // 错误,尝试赋值一个字符串给int类型变量
}

复制代码

4. 类型

下面是 Go 支持的基本类型:

  • bool
  • 数字类型
    • int8, int16, int32, int64, int
    • uint8, uint16, uint32, uint64, uint
    • float32, float64
    • complex64, complex128
    • byte
    • rune
  • string

bool

bool 类型表示一个布尔值,值为 true 或者 false。

package main

import "fmt"

func main() {  
    a := true
    b := false
    fmt.Println("a:", a, "b:", b)
    c := a && b
    fmt.Println("c:", c)
    d := a || b
    fmt.Println("d:", d)
}
复制代码

在上面的程序中,a 赋值为 true,b 赋值为 false。

c 赋值为 a && b。仅当 a 和 b 都为 true 时,操作符 && 才返回 true。因此,在这里 c 为 false。

当 a 或者 b 为 true 时,操作符 || 返回 true。在这里,由于 a 为 true,因此 d 也为 true。我们将得到程序的输出如下。

a: true b: false  
c: false  
d: true
复制代码

有符号整型

int8:表示 8 位有符号整型大小:8 位范围:-128~127

int16:表示 16 位有符号整型大小:16 位范围:-32768~32767

int32:表示 32 位有符号整型大小:32 位范围:-2147483648~2147483647

int64:表示 64 位有符号整型大小:64 位范围:-9223372036854775808~9223372036854775807

int:根据不同的底层平台(Underlying Platform),表示 32 或 64 位整型。除非对整型的大小有特定的需求,否则你通常应该使用 int表示整型。大小:在 32 位系统下是 32 位,而在 64 位系统下是 64 位。范围:在 32 位系统下是 -2147483648~2147483647,而在 64 位系统是 -9223372036854775808~9223372036854775807。

package main

import "fmt"

func main() {  
    var a int = 89
    b := 95
    fmt.Println("value of a is", a, "and b is", b)
}
复制代码

在线运行程序

上面程序会输出 value of a is 89 and b is 95

在上述程序中,a 是 int 类型,而 b 的类型通过赋值(95)推断得出。上面我们提到,int 类型的大小在 32 位系统下是 32 位,而在 64 位系统下是 64 位。接下来我们会证实这种说法。

在 Printf 方法中,使用 %T格式说明符(Format Specifier),可以打印出变量的类型。Go 的 unsafe包提供了一个 Sizeof函数,该函数接收变量并返回它的字节大小。unsafe包应该小心使用,因为使用 unsafe 包可能会带来可移植性问题。不过出于本教程的目的,我们是可以使用的。

下面程序会输出变量 a 和 b 的类型和大小。格式说明符 %T用于打印类型,而 %d用于打印字节大小。

package main

import (  
    "fmt"
    "unsafe"
)

func main() {  
    var a int = 89
    b := 95
    fmt.Println("value of a is", a, "and b is", b)
    fmt.Printf("type of a is %T, size of a is %d", a, unsafe.Sizeof(a)) // a 的类型和大小
    fmt.Printf("\ntype of b is %T, size of b is %d", b, unsafe.Sizeof(b)) // b 的类型和大小
}
复制代码

在线运行程序

以上程序会输出:

value of a is 89 and b is 95  
type of a is int, size of a is 4  
type of b is int, size of b is 4
复制代码

从上面的输出,我们可以推断出 a 和 b 为 int类型,且大小都是 32 位(4 字节)。如果你在 64 位系统上运行上面的代码,会有不同的输出。在 64 位系统下,a 和 b 会占用 64 位(8 字节)的大小。

无符号整型

uint8:表示 8 位无符号整型大小:8 位范围:0~255

uint16:表示 16 位无符号整型大小:16 位范围:0~65535

uint32:表示 32 位无符号整型大小:32 位范围:0~4294967295

uint64:表示 64 位无符号整型大小:64 位范围:0~18446744073709551615

uint:根据不同的底层平台,表示 32 或 64 位无符号整型。大小:在 32 位系统下是 32 位,而在 64 位系统下是 64 位。范围:在 32 位系统下是 0~4294967295,而在 64 位系统是 0~18446744073709551615。

浮点型

float32:32 位浮点数float64:64 位浮点数

下面一个简单程序演示了整型和浮点型的运用。

package main

import (  
    "fmt"
)

func main() {  
    a, b := 5.67, 8.97
    fmt.Printf("type of a %T b %T\n", a, b)
    sum := a + b
    diff := a - b
    fmt.Println("sum", sum, "diff", diff)

    no1, no2 := 56, 89
    fmt.Println("sum", no1+no2, "diff", no1-no2)
}
复制代码

a 和 b 的类型根据赋值推断得出。在这里,a 和 b 的类型为 float64(float64 是浮点数的默认类型)。我们把 a 和 b 的和赋值给变量 sum,把 b 和 a 的差赋值给 diff,接下来打印 sum 和 diff。no1 和 no2 也进行了相同的计算。上述程序将会输出:

type of a float64 b float64  
sum 14.64 diff -3.3000000000000007  
sum 145 diff -33
复制代码

复数类型

complex64:实部和虚部都是 float32 类型的的复数。complex128:实部和虚部都是 float64 类型的的复数。

内建函数 complex用于创建一个包含实部和虚部的复数。complex 函数的定义如下:

func complex(r, i FloatType) ComplexType
复制代码

该函数的参数分别是实部和虚部,并返回一个复数类型。实部和虚部应该是相同类型,也就是 float32 或 float64。如果实部和虚部都是 float32 类型,则函数会返回一个 complex64 类型的复数。如果实部和虚部都是 float64 类型,则函数会返回一个 complex128 类型的复数。

还可以使用简短语法来创建复数:

c := 6 + 7i
复制代码

下面我们编写一个简单的程序来理解复数。

package main

import (  
    "fmt"
)

func main() {  
    c1 := complex(5, 7)
    c2 := 8 + 27i
    cadd := c1 + c2
    fmt.Println("sum:", cadd)
    cmul := c1 * c2
    fmt.Println("product:", cmul)
}
复制代码

在上面的程序里,c1 和 c2 是两个复数。c1的实部为 5,虚部为 7。c2 的实部为8,虚部为 27。c1 和 c2 的和赋值给 cadd,而 c1 和 c2 的乘积赋值给 cmul。该程序将输出:

sum: (13+34i)  
product: (-149+191i)
复制代码

其他数字类型

byte是 uint8 的别名。rune是 int32 的别名。

在学习字符串的时候,我们会详细讨论 byte 和 rune。

string 类型

在 Golang 中,字符串是字节的集合。如果你现在还不理解这个定义,也没有关系。我们可以暂且认为一个字符串就是由很多字符组成的。我们后面会在一个教程中深入学习字符串。 下面编写一个使用字符串的程序。

package main

import (  
    "fmt"
)

func main() {  
    first := "Naveen"
    last := "Ramanathan"
    name := first +" "+ last
    fmt.Println("My name is",name)
}
复制代码

上面程序中,first 赋值为字符串 "Naveen",last 赋值为字符串 "Ramanathan"。+ 操作符可以用于拼接字符串。我们拼接了 first、空格和 last,并将其赋值给 name。上述程序将打印输出 My name is Naveen Ramanathan

还有许多应用于字符串上面的操作,我们将会在一个单独的教程里看见它们。

类型转换

Go 有着非常严格的强类型特征。Go 没有自动类型提升或类型转换。我们通过一个例子说明这意味着什么。

package main

import (  
    "fmt"
)

func main() {  
    i := 55      //int
    j := 67.8    //float64
    sum := i + j //不允许 int + float64
    fmt.Println(sum)
}
复制代码

上面的代码在 C 语言中是完全合法的,然而在 Go 中,却是行不通的。i 的类型是 int ,而 j 的类型是 float64 ,我们正试图把两个不同类型的数相加,Go 不允许这样的操作。如果运行程序,你会得到 main.go:10: invalid operation: i + j (mismatched types int and float64)

要修复这个错误,i 和 j 应该是相同的类型。在这里,我们把 j 转换为 int 类型。把 v 转换为 T 类型的语法是 T(v)。

package main

import (  
    "fmt"
)

func main() {  
    i := 55      //int
    j := 67.8    //float64
    sum := i + int(j) //j is converted to int
    fmt.Println(sum)
}
复制代码

现在,当你运行上面的程序时,会看见输出 122

赋值的情况也是如此。把一个变量赋值给另一个不同类型的变量,需要显式的类型转换。下面程序说明了这一点。

package main

import (  
    "fmt"
)

func main() {  
    i := 10
    var j float64 = float64(i) // 若没有显式转换,该语句会报错
    fmt.Println("j", j)
}
复制代码

在第 9 行,i 转换为 float64 类型,接下来赋值给 j。如果不进行类型转换,当你试图把 i 赋值给 j 时,编译器会抛出错误。

5. 常量

定义

在 Go 语言中,术语"常量"用于表示固定的值。比如 5-89、 I love Go67.89等等。

看看下面的代码:

var a int = 50  
var b string = "I love Go"
复制代码

在上面的代码中,变量 a 和 b 分别被赋值为常量 50 和 I love GO。关键字 const被用于表示常量,比如 50和 I love Go。即使在上面的代码中我们没有明确的使用关键字 const,但是在 Go 的内部,它们是常量。

顾名思义,常量不能再重新赋值为其他的值。因此下面的程序将不能正常工作,它将出现一个编译错误: cannot assign to a.

package main

func main() {  
    const a = 55 // 允许
    a = 89       // 不允许重新赋值
}
复制代码

常量的值会在编译的时候确定。因为函数调用发生在运行时,所以不能将函数的返回值赋值给常量。

package main

import (  
    "fmt"
    "math"
)

func main() {  
    fmt.Println("Hello, playground")
    var a = math.Sqrt(4)   // 允许
    const b = math.Sqrt(4) // 不允许
}
复制代码

在上面的程序中,因为 a是变量,因此我们可以将函数 math.Sqrt(4)的返回值赋值给它(我们将在单独的地方详细讨论函数)。

b是一个常量,它的值需要在编译的时候就确定。函数 math.Sqrt(4)只会在运行的时候计算,因此 const b = math.Sqrt(4)将会抛出错误 error main.go:11: const initializer math.Sqrt(4) is not a constant)

字符串常量

双引号中的任何值都是 Go 中的字符串常量。例如像 Hello World或 Sam等字符串在 Go 中都是常量。

什么类型的字符串属于常量?答案是他们是无类型的。

像 Hello World这样的字符串常量没有任何类型。

const hello = "Hello World"
复制代码

上面的例子,我们把 Hello World分配给常量 hello。现在常量 hello有类型吗?答案是没有。常量仍然没有类型。

Go 是一门强类型语言,所有的变量必须有明确的类型。那么, 下面的程序是如何将无类型的常量 Sam赋值给变量 name的呢?

package main

import (  
    "fmt"
)

func main() {  
    var name = "Sam"
    fmt.Printf("type %T value %v", name, name)

}
复制代码

答案是无类型的常量有一个与它们相关联的默认类型,并且当且仅当一行代码需要时才提供它。在声明中 var name = "Sam" , name 需要一个类型,它从字符串常量 Sam 的默认类型中获取。

有没有办法创建一个带类型的常量?答案是可以的。以下代码创建一个有类型常量。

const typedhello string = "Hello World"
复制代码

上面代码中, typedhello就是一个 string类型的常量。

Go 是一个强类型的语言,在分配过程中混合类型是不允许的。让我们通过以下程序看看这句话是什么意思。

package main

func main() {  
        var defaultName = "Sam" // 允许
        type myString string
        var customName myString = "Sam" // 允许
        customName = defaultName // 不允许

}
复制代码

在上面的代码中,我们首先创建一个变量 defaultName并分配一个常量 Sam常量 Sam 的默认类型是 string ,所以在赋值后 defaultName 是 string 类型的。

下一行,我们将创建一个新类型 myString,它是 string的别名。

然后我们创建一个 myString的变量 customName并且给他赋值一个常量 Sam。因为常量 Sam是无类型的,它可以分配给任何字符串变量。因此这个赋值是允许的,customName的类型是 myString

现在,我们有一个类型为 string的变量 defaultName和另一个类型为 myString的变量 customName。即使我们知道这个 myString是 string类型的别名。Go 的类型策略不允许将一种类型的变量赋值给另一种类型的变量。因此将 defaultName赋值给 customName是不允许的,编译器会抛出一个错误 main.go:7:20: cannot use defaultName (type string) as type myString in assignmen

布尔常量

布尔常量和字符串常量没有什么不同。他们是两个无类型的常量 true和 false。字符串常量的规则适用于布尔常量,所以在这里我们不再重复。以下是解释布尔常量的简单程序。

package main

func main() {  
    const trueConst = true
    type myBool bool
    var defaultBool = trueConst // 允许
    var customBool myBool = trueConst // 允许
    defaultBool = customBool // 不允许
}
复制代码

上面的程序是自我解释的。

数字常量

数字常量包含整数、浮点数和复数的常量。数字常量中有一些微妙之处。

让我们看一些例子来说清楚。

package main

import (  
    "fmt"
)

func main() {  
    const a = 5
    var intVar int = a
    var int32Var int32 = a
    var float64Var float64 = a
    var complex64Var complex64 = a
    fmt.Println("intVar",intVar, "\nint32Var", int32Var, "\nfloat64Var", float64Var, "\ncomplex64Var",complex64Var)
}
复制代码

上面的程序,常量 a是没有类型的,它的值是 5。您可能想知道 a的默认类型是什么,如果它确实有一个的话, 那么我们如何将它分配给不同类型的变量。答案在于 a的语法。下面的程序将使事情更加清晰。

package main

import (  
    "fmt"
)

func main() {  
    var i = 5
    var f = 5.6
    var c = 5 + 6i
    fmt.Printf("i's type %T, f's type %T, c's type %T", i, f, c)

}
复制代码

在上面的程序中,每个变量的类型由数字常量的语法决定。5在语法中是整数, 5.6是浮点数,5+6i的语法是复数。当我们运行上面的程序,它会打印出 i's type int, f's type float64, c's type complex128

现在我希望下面的程序能够正确的工作。

package main

import (  
    "fmt"
)

func main() {  
    const a = 5
    var intVar int = a
    var int32Var int32 = a
    var float64Var float64 = a
    var complex64Var complex64 = a
    fmt.Println("intVar",intVar, "\nint32Var", int32Var, "\nfloat64Var", float64Var, "\ncomplex64Var",complex64Var)
}
复制代码

在这个程序中, a的值是 5a的语法是通用的(它可以代表一个浮点数、整数甚至是一个没有虚部的复数),因此可以将其分配给任何兼容的类型。这些常量的默认类型可以被认为是根据上下文在运行中生成的。 var intVar int = a要求 a是 int,所以它变成一个 int常量。 var complex64Var complex64 = a要求 a是 complex64,因此它变成一个复数类型。很简单的:)。

数字表达式

数字常量可以在表达式中自由混合和匹配,只有当它们被分配给变量或者在需要类型的代码中的任何地方使用时,才需要类型。

package main

import (  
    "fmt"
)

func main() {  
    var a = 5.9/8
    fmt.Printf("a's type %T value %v",a, a)
}
复制代码

在上面的程序中, 5.9在语法中是浮点型,8是整型,5.9/8是允许的,因为两个都是数字常量。除法的结果是 0.7375是一个浮点型,所以 a的类型是浮点型。这个程序的输出结果是: a's type float64 value 0.7375

6. 函数(Function)

函数是什么?

函数是一块执行特定任务的代码。一个函数是在输入源基础上,通过执行一系列的算法,生成预期的输出。

函数的声明

在 Go 语言中,函数声明通用语法如下:

func functionname(parametername type) returntype {  
    // 函数体(具体实现的功能)
}
复制代码

函数的声明以关键词 func开始,后面紧跟自定义的函数名 functionname (函数名)。函数的参数列表定义在 (和 )之间,返回值的类型则定义在之后的 returntype (返回值类型)处。声明一个参数的语法采用 参数名参数类型的方式,任意多个参数采用类似 (parameter1 type, parameter2 type) 即(参数1 参数1的类型,参数2 参数2的类型)的形式指定。之后包含在 {和 }之间的代码,就是函数体。

函数中的参数列表和返回值并非是必须的,所以下面这个函数的声明也是有效的

func functionname() {  
    // 译注: 表示这个函数不需要输入参数,且没有返回值
}
复制代码

示例函数

我们以写一个计算商品价格的函数为例,输入参数是单件商品的价格和商品的个数,两者的乘积为商品总价,作为函数的输出值。

func calculateBill(price int, no int) int {  
    var totalPrice = price * no // 商品总价 = 商品单价 * 数量
    return totalPrice // 返回总价
}
复制代码

上述函数有两个整型的输入 price和 no,返回值 totalPrice为 price和 no的乘积,也是整数类型。

如果有连续若干个参数,它们的类型一致,那么我们无须一一罗列,只需在最后一个参数后添加该类型。例如,price int, no int可以简写为 price, no int,所以示例函数也可写成

func calculateBill(price, no int) int {  
    var totalPrice = price * no
    return totalPrice
}
复制代码

现在我们已经定义了一个函数,我们要在代码中尝试着调用它。调用函数的语法为 functionname(parameters)。调用示例函数的方法如下:

calculateBill(10, 5)
复制代码

完成了示例函数声明和调用后,我们就能写出一个完整的程序,并把商品总价打印在控制台上:

package main

import (  
    "fmt"
)

func calculateBill(price, no int) int {  
    var totalPrice = price * no
    return totalPrice
}
func main() {  
    price, no := 90, 6 // 定义 price 和 no,默认类型为 int
    totalPrice := calculateBill(price, no)
    fmt.Println("Total price is", totalPrice) // 打印到控制台上
}
复制代码

该程序在控制台上打印的结果为

Total price is 540
复制代码

多返回值

Go 语言支持一个函数可以有多个返回值。我们来写个以矩形的长和宽为输入参数,计算并返回矩形面积和周长的函数 rectProps。矩形的面积是长度和宽度的乘积, 周长是长度和宽度之和的两倍。即:

  • 面积 = 长 * 宽
  • 周长 = 2 * ( 长 + 宽 )
package main

import (  
    "fmt"
)

func rectProps(length, width float64)(float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}

func main() {  
    area, perimeter := rectProps(10.8, 5.6)
    fmt.Printf("Area %f Perimeter %f", area, perimeter) 
}
复制代码

如果一个函数有多个返回值,那么这些返回值必须用 (和 )括起来。func rectProps(length, width float64)(float64, float64)示例函数有两个 float64 类型的输入参数 length和 width,并返回两个 float64 类型的值。该程序在控制台上打印结果为

Area 60.480000 Perimeter 32.800000
复制代码

命名返回值

从函数中可以返回一个命名值。一旦命名了返回值,可以认为这些值在函数第一行就被声明为变量了。

上面的 rectProps 函数也可用这个方式写成:

func rectProps(length, width float64)(area, perimeter float64) {  
    area = length * width
    perimeter = (length + width) * 2
    return // 不需要明确指定返回值,默认返回 area, perimeter 的值
}
复制代码

请注意, 函数中的 return 语句没有显式返回任何值。由于 area和 perimeter在函数声明中指定为返回值, 因此当遇到 return 语句时, 它们将自动从函数返回。

空白符

_在 Go 中被用作空白符,可以用作表示任何类型的任何值。

我们继续以 rectProps函数为例,该函数计算的是面积和周长。假使我们只需要计算面积,而并不关心周长的计算结果,该怎么调用这个函数呢?这时,空白符 _就上场了。

下面的程序我们只用到了函数 rectProps的一个返回值 area

package main

import (  
    "fmt"
)

func rectProps(length, width float64) (float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}
func main() {  
    area, _ := rectProps(10.8, 5.6) // 返回值周长被丢弃
    fmt.Printf("Area %f ", area)
}
复制代码

在程序的 area, _ := rectProps(10.8, 5.6)这一行,我们看到空白符 _用来跳过不要的计算结果。

7. 包

什么是包,为什么使用包?

到目前为止,我们看到的 Go 程序都只有一个文件,文件里包含一个 main 函数和几个其他的函数。在实际中,这种把所有源代码编写在一个文件的方法并不好用。以这种方式编写,代码的重用和维护都会很困难。而包(Package)解决了这样的问题。

包用于组织 Go 源代码,提供了更好的可重用性与可读性。由于包提供了代码的封装,因此使得 Go 应用程序易于维护。

例如,假如我们正在开发一个 Go 图像处理程序,它提供了图像的裁剪、锐化、模糊和彩色增强等功能。一种组织程序的方式就是根据不同的特性,把代码放到不同的包中。比如裁剪可以是一个单独的包,而锐化是另一个包。这种方式的优点是,由于彩色增强可能需要一些锐化的功能,因此彩色增强的代码只需要简单地导入(我们会在随后讨论)锐化功能的包,就可以使用锐化的功能了。这样的方式使得代码易于重用。

我们会逐步构建一个计算矩形的面积和对角线的应用程序。

通过这个程序,我们会更好地理解包。

main 函数和 main 包

所有可执行的 Go 程序都必须包含一个 main 函数。这个函数是程序运行的入口。main 函数应该放置于 main 包中。

package packagename 这行代码指定了某一源文件属于一个包。它应该放在每一个源文件的第一行。

下面开始为我们的程序创建一个 main 函数和 main 包。在 Go 工作区内的 src 文件夹中创建一个文件夹,命名为 geometry。在 geometry文件夹中创建一个 geometry.go文件。

在 geometry.go 中编写下面代码。

// geometry.go
package main 

import "fmt"

func main() {  
    fmt.Println("Geometrical shape properties")
}
复制代码

package main这一行指定该文件属于 main 包。import "packagename"语句用于导入一个已存在的包。在这里我们导入了 fmt包,包内含有 Println 方法。接下来是 main 函数,它会打印 Geometrical shape properties

键入 go install geometry,编译上述程序。该命令会在 geometry文件夹内搜索拥有 main 函数的文件。在这里,它找到了 geometry.go。接下来,它编译并产生一个名为 geometry(在 windows 下是 geometry.exe)的二进制文件,该二进制文件放置于工作区的 bin 文件夹。现在,工作区的目录结构会是这样:

src
    geometry
        gemometry.go
bin
    geometry
复制代码

键入 workspacepath/bin/geometry,运行该程序。请用你自己的 Go 工作区来替换 workspacepath。这个命令会执行 bin 文件夹里的 geometry二进制文件。你应该会输出 Geometrical shape properties

创建自定义的包

我们将组织代码,使得所有与矩形有关的功能都放入 rectangle包中。

我们会创建一个自定义包 rectangle,它有一个计算矩形的面积和对角线的函数。

属于某一个包的源文件都应该放置于一个单独命名的文件夹里。按照 Go 的惯例,应该用包名命名该文件夹。

因此,我们在 geometry文件夹中,创建一个命名为 rectangle的文件夹。在 rectangle文件夹中,所有文件都会以 package rectangle作为开头,因为它们都属于 rectangle 包。

在我们之前创建的 rectangle 文件夹中,再创建一个名为 rectprops.go的文件,添加下列代码。

// rectprops.go
package rectangle

import "math"

func Area(len, wid float64) float64 {  
    area := len * wid
    return area
}

func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}
复制代码

在上面的代码中,我们创建了两个函数用于计算 Area和 Diagonal。矩形的面积是长和宽的乘积。矩形的对角线是长与宽平方和的平方根。math包下面的 Sqrt函数用于计算平方根。

注意到函数 Area 和 Diagonal 都是以大写字母开头的。这是有必要的,我们将会很快解释为什么需要这样做。

导入自定义包

为了使用自定义包,我们必须要先导入它。导入自定义包的语法为 import path。我们必须指定自定义包相对于工作区内 src文件夹的相对路径。我们目前的文件夹结构是:

src
    geometry
        geometry.go
        rectangle
            rectprops.go
复制代码

import "geometry/rectangle"这一行会导入 rectangle 包。

在 geometry.go里面添加下面的代码:

// geometry.go
package main 

import (  
    "fmt"
    "geometry/rectangle" // 导入自定义包
)

func main() {  
    var rectLen, rectWidth float64 = 6, 7
    fmt.Println("Geometrical shape properties")
    /*Area function of rectangle package used*/
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
    /*Diagonal function of rectangle package used*/
    fmt.Printf("diagonal of the rectangle %.2f ", rectangle.Diagonal(rectLen, rectWidth))
}
复制代码

上面的代码导入了 rectangle包,并调用了里面的 Area 和 Diagonal 函数,得到矩形的面积和对角线。Printf 内的格式说明符 %.2f会将浮点数截断到小数点两位。应用程序的输出为:

Geometrical shape properties  
area of rectangle 42.00  
diagonal of the rectangle 9.22
复制代码

导出名字(Exported Names)

我们将 rectangle 包中的函数 Area 和 Diagonal 首字母大写。在 Go 中这具有特殊意义。在 Go 中,任何以大写字母开头的变量或者函数都是被导出的名字。其它包只能访问被导出的函数和变量。在这里,我们需要在 main 包中访问 Area 和 Diagonal 函数,因此会将它们的首字母大写。

在 rectprops.go中,如果函数名从 Area(len, wid float64)变为 area(len, wid float64),并且在 geometry.go中, rectangle.Area(rectLen, rectWidth)变为 rectangle.area(rectLen, rectWidth), 则该程序运行时,编译器会抛出错误 geometry.go:11: cannot refer to unexported name rectangle.area。因为如果想在包外访问一个函数,它应该首字母大写。

init 函数

所有包都可以包含一个 init函数。init 函数不应该有任何返回值类型和参数,在我们的代码中也不能显式地调用它。init 函数的形式如下:

func init() {  
}
复制代码

init 函数可用于执行初始化任务,也可用于在开始执行之前验证程序的正确性。

包的初始化顺序如下:

  1. 首先初始化包级别(Package Level)的变量
  2. 紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。

如果一个包导入了另一个包,会先初始化被导入的包。

尽管一个包可能会被导入多次,但是它只会被初始化一次。

为了理解 init 函数,我们接下来对程序做了一些修改。

首先在 rectprops.go文件中添加了一个 init 函数。

// rectprops.go
package rectangle

import "math"  
import "fmt"

/*
 * init function added
 */
func init() {  
    fmt.Println("rectangle package initialized")
}
func Area(len, wid float64) float64 {  
    area := len * wid
    return area
}

func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}
复制代码

我们添加了一个简单的 init 函数,它仅打印 rectangle package initialized

现在我们来修改 main 包。我们知道矩形的长和宽都应该大于 0,我们将在 geometry.go中使用 init 函数和包级别的变量来检查矩形的长和宽。

修改 geometry.go文件如下所示:

// geometry.go
package main 

import (  
    "fmt"
    "geometry/rectangle" // 导入自定义包
    "log"
)
/*
 * 1. 包级别变量
*/
var rectLen, rectWidth float64 = 6, 7 

/*
*2. init 函数会检查长和宽是否大于0
*/
func init() {  
    println("main package initialized")
    if rectLen < 0 {
        log.Fatal("length is less than zero")
    }
    if rectWidth < 0 {
        log.Fatal("width is less than zero")
    }
}

func main() {  
    fmt.Println("Geometrical shape properties")
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
    fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
复制代码

我们对 geometry.go做了如下修改:

  1. 变量 rectLen和 rectWidth从 main 函数级别移到了包级别。
  2. 添加了 init 函数。当 rectLen 或 rectWidth 小于 0 时,init 函数使用 log.Fatal函数打印一条日志,并终止了程序。

main 包的初始化顺序为:

  1. 首先初始化被导入的包。因此,首先初始化了 rectangle 包。
  2. 接着初始化了包级别的变量 rectLen和 rectWidth
  3. 调用 init 函数。
  4. 最后调用 main 函数。

当运行该程序时,会有如下输出。

rectangle package initialized  
main package initialized  
Geometrical shape properties  
area of rectangle 42.00  
diagonal of the rectangle 9.22
复制代码

果然,程序会首先调用 rectangle 包的 init 函数,然后,会初始化包级别的变量 rectLen和 rectWidth。接着调用 main 包里的 init 函数,该函数检查 rectLen 和 rectWidth 是否小于 0,如果条件为真,则终止程序。我们会在单独的教程里深入学习 if 语句。现在你可以认为 if rectLen < 0能够检查 rectLen是否小于 0,并且如果是,则终止程序。rectWidth条件的编写也是类似的。在这里两个条件都为假,因此程序继续执行。最后调用了 main 函数。

让我们接着稍微修改这个程序来学习使用 init 函数。

将 geometry.go中的 var rectLen, rectWidth float64 = 6, 7改为 var rectLen, rectWidth float64 = -6, 7。我们把 rectLen初始化为负数。

现在当运行程序时,会得到:

rectangle package initialized  
main package initialized  
2017/04/04 00:28:20 length is less than zero
复制代码

像往常一样, 会首先初始化 rectangle 包,然后是 main 包中的包级别的变量 rectLen 和 rectWidth。rectLen 为负数,因此当运行 init 函数时,程序在打印 length is less than zero后终止。

使用空白标识符(Blank Identifier)

导入了包,却不在代码中使用它,这在 Go 中是非法的。当这么做时,编译器是会报错的。其原因是为了避免导入过多未使用的包,从而导致编译时间显著增加。将 geometry.go中的代码替换为如下代码:

// geometry.go
package main 

import (
    "geometry/rectangle" // 导入自定的包
)
func main() {

}
复制代码

上面的程序将会抛出错误 geometry.go:6: imported and not used: "geometry/rectangle"

然而,在程序开发的活跃阶段,又常常会先导入包,而暂不使用它。遇到这种情况就可以使用空白标识符 _

下面的代码可以避免上述程序的错误:

package main

import (  
    "geometry/rectangle" 
)

var _ = rectangle.Area // 错误屏蔽器

func main() {

}
复制代码

var _ = rectangle.Area这一行屏蔽了错误。我们应该了解这些错误屏蔽器(Error Silencer)的动态,在程序开发结束时就移除它们,包括那些还没有使用过的包。由此建议在 import 语句下面的包级别范围中写上错误屏蔽器。

有时候我们导入一个包,只是为了确保它进行了初始化,而无需使用包中的任何函数或变量。例如,我们或许需要确保调用了 rectangle 包的 init 函数,而不需要在代码中使用它。这种情况也可以使用空白标识符,如下所示。

package main 

import (
    _ "geometry/rectangle" 
)
func main() {

}
复制代码

运行上面的程序,会输出 rectangle package initialized。尽管在所有代码里,我们都没有使用这个包,但还是成功初始化了它。

8. if-else 语句

if 是条件语句。if 语句的语法是

if condition {  
}
复制代码

如果 condition为真,则执行 {和 }之间的代码。

不同于其他语言,例如 C 语言,Go 语言里的 { }是必要的,即使在 { }之间只有一条语句。

if 语句还有可选的 else if和 else部分。

if condition {  
} else if condition {
} else {
}
复制代码

if-else 语句之间可以有任意数量的 else if。条件判断顺序是从上到下。如果 if或 else if条件判断的结果为真,则执行相应的代码块。 如果没有条件为真,则 else代码块被执行。

让我们编写一个简单的程序来检测一个数字是奇数还是偶数。

package main

import (  
    "fmt"
)

func main() {  
    num := 10
    if num % 2 == 0 { //checks if number is even
        fmt.Println("the number is even") 
    }  else {
        fmt.Println("the number is odd")
    }
}
复制代码

if num%2 == 0语句检测 num 取 2 的余数是否为零。 如果是为零则打印输出 "the number is even",如果不为零则打印输出 "the number is odd"。在上面的这个程序中,打印输出的是 the number is even

if还有另外一种形式,它包含一个 statement可选语句部分,该组件在条件判断之前运行。它的语法是

if statement; condition {  
}
复制代码

让我们重写程序,使用上面的语法来查找数字是偶数还是奇数。

package main

import (  
    "fmt"
)

func main() {  
    if num := 10; num % 2 == 0 { //checks if number is even
        fmt.Println(num,"is even") 
    }  else {
        fmt.Println(num,"is odd")
    }
}
复制代码

在上面的程序中,num在 if语句中进行初始化,num只能从 if和 else中访问。也就是说 num的范围仅限于 ifelse代码块。如果我们试图从其他外部的 if或者 else访问 num,编译器会不通过。

让我们再写一个使用 else if的程序。

package main

import (  
    "fmt"
)

func main() {  
    num := 99
    if num <= 50 {
        fmt.Println("number is less than or equal to 50")
    } else if num >= 51 && num <= 100 {
        fmt.Println("number is between 51 and 100")
    } else {
        fmt.Println("number is greater than 100")
    }

}
复制代码

在上面的程序中,如果 else if num >= 51 && num <= 100为真,程序将输出 number is between 51 and 100

一个注意点

else语句应该在 if语句的大括号 }之后的同一行中。如果不是,编译器会不通过。

让我们通过以下程序来理解它。

package main

import (  
    "fmt"
)

func main() {  
    num := 10
    if num % 2 == 0 { //checks if number is even
        fmt.Println("the number is even") 
    }  
    else {
        fmt.Println("the number is odd")
    }
}
复制代码

在上面的程序中,else语句不是从 if语句结束后的 }同一行开始。而是从下一行开始。这是不允许的。如果运行这个程序,编译器会输出错误,

main.go:12:5: syntax error: unexpected else, expecting }
复制代码

出错的原因是 Go 语言的分号是自动插入。

在 Go 语言规则中,它指定在 }之后插入一个分号,如果这是该行的最终标记。因此,在if语句后面的 }会自动插入一个分号。

实际上我们的程序变成了

if num%2 == 0 {  
      fmt.Println("the number is even") 
};  //semicolon inserted by Go
else {  
      fmt.Println("the number is odd")
}
复制代码

分号插入之后。从上面代码片段可以看出第三行插入了分号。

由于 if{…} else {…}是一个单独的语句,它的中间不应该出现分号。因此,需要将 else语句放置在 }之后处于同一行中。

我已经重写了程序,将 else 语句移动到 if 语句结束后 }的后面,以防止分号的自动插入。

package main

import (  
    "fmt"
)

func main() {  
    num := 10
    if num%2 == 0 { //checks if number is even
        fmt.Println("the number is even") 
    } else {
        fmt.Println("the number is odd")
    }
}
复制代码

现在编译器会很开心,我们也一样 ?。

9. 循环

循环语句是用来重复执行某一段代码。

for是 Go 语言唯一的循环语句。Go 语言中并没有其他语言比如 C 语言中的 while和 do while循环。

for 循环语法

for initialisation; condition; post {  
}
复制代码

初始化语句只执行一次。循环初始化后,将检查循环条件。如果条件的计算结果为 true,则 {}内的循环体将执行,接着执行 post 语句。post 语句将在每次成功循环迭代后执行。在执行 post 语句后,条件将被再次检查。如果为 true, 则循环将继续执行,否则 for 循环将终止。(译注:这是典型的 for 循环三个表达式,第一个为初始化表达式或赋值语句;第二个为循环条件判定表达式;第三个为循环变量修正表达式,即此处的 post )

这三个组成部分,即初始化,条件和 post 都是可选的。让我们看一个例子来更好地理解循环。

例子

让我们用 for循环写一个打印出从 1 到 10 的程序。

package main

import (  
    "fmt"
)

func main() {  
    for i := 1; i <= 10; i++ {
        fmt.Printf(" %d",i)
    }
}
复制代码

在上面的程序中,i 变量被初始化为 1。条件语句会检查 i 是否小于 10。如果条件成立,i 就会被打印出来,否则循环就会终止。循环语句会在每一次循环完成后自增 1。一旦 i 变得比 10 要大,循环中止。

上面的程序会打印出 1 2 3 4 5 6 7 8 9 10

在 for循环中声明的变量只能在循环体内访问,因此 i 不能够在循环体外访问。

break

break语句用于在完成正常执行之前突然终止 for 循环,之后程序将会在 for 循环下一行代码开始执行。

让我们写一个从 1 打印到 5 并且使用 break跳出循环的程序。

package main

import (  
    "fmt"
)

func main() {  
    for i := 1; i <= 10; i++ {
        if i > 5 {
            break //loop is terminated if i > 5
        }
        fmt.Printf("%d ", i)
    }
    fmt.Printf("\nline after for loop")
}
复制代码

在上面的程序中,在循环过程中 i 的值会被判断。如果 i 的值大于 5 然后 break语句就会执行,循环就会被终止。打印语句会在 for循环结束后执行,上面程序会输出为

1 2 3 4 5  
line after for loop
复制代码

continue

continue语句用来跳出 for循环中当前循环。在 continue语句后的所有的 for循环语句都不会在本次循环中执行。循环体会在一下次循环中继续执行。

让我们写一个打印出 1 到 10 并且使用 continue的程序。

package main

import (  
    "fmt"
)

func main() {  
    for i := 1; i <= 10; i++ {
        if i%2 == 0 {
            continue
        }
        fmt.Printf("%d ", i)
    }
}
复制代码

在上面的程序中,这行代码 if i%2==0会判断 i 除以 2 的余数是不是 0,如果是 0,这个数字就是偶数然后执行 continue语句,从而控制程序进入下一个循环。因此在 continue后面的打印语句不会被调用而程序会进入一下个循环。上面程序会输出 1 3 5 7 9

 

更多例子

让我们写更多的代码来演示 for循环的多样性吧

下面这个程序打印出从 0 到 10 所有的偶数。

package main

import (  
    "fmt"
)

func main() {  
    i := 0
    for ;i <= 10; { // initialisation and post are omitted
        fmt.Printf("%d ", i)
        i += 2
    }
}
复制代码

正如我们已经知道的那样,for循环的三部分,初始化语句、条件语句、post 语句都是可选的。在上面的程序中,初始化语句和 post 语句都被省略了。i 在 for循环外被初始化成了 0。只要 i<=10循环就会被执行。在循环中,i 以 2 的增量自增。上面的程序会输出 0 2 4 6 8 10

上面程序中 for循环中的分号也可以省略。这个格式的 for循环可以看作是二选一的 for while循环。上面的程序可以被重写成:

package main

import (  
    "fmt"
)

func main() {  
    i := 0
    for i <= 10 { //semicolons are ommitted and only condition is present
        fmt.Printf("%d ", i)
        i += 2
    }
}
复制代码

在 for循环中可以声明和操作多个变量。让我们写一个使用声明多个变量来打印下面序列的程序。

10 * 1 = 10  
11 * 2 = 22  
12 * 3 = 36  
13 * 4 = 52  
14 * 5 = 70  
15 * 6 = 90  
16 * 7 = 112  
17 * 8 = 136  
18 * 9 = 162  
19 * 10 = 190
package main

import (  
    "fmt"
)

func main() {  
    for no, i := 10, 1; i <= 10 && no <= 19; i, no = i+1, no+1 { //multiple initialisation and increment
        fmt.Printf("%d * %d = %d\n", no, i, no*i)
    }

}
复制代码

在上面的程序中 no和 i被声明然后分别被初始化为 10 和 1 。在每一次循环结束后 no和 i都自增 1 。布尔型操作符 &&被用来确保 i 小于等于 10 并且 no小于等于 19 。

无限循环

无限循环的语法是:

for {  
}
复制代码

下一个程序就会一直打印Hello World不会停止。

package main

import "fmt"

func main() {  
    for {
        fmt.Println("Hello World")
    }
}
复制代码

在你本地系统上运行,来无限的打印 “Hello World” 。

这里还有一个 range结构,它可以被用来在 for循环中操作数组对象。当我们学习数组时我们会补充这方面内容。

10. switch 语句

switch 是一个条件语句,用于将表达式的值与可能匹配的选项列表进行比较,并根据匹配情况执行相应的代码块。它可以被认为是替代多个 if else子句的常用方式。

看代码比文字更容易理解。让我们从一个简单的例子开始,它将把一个手指的编号作为输入,然后输出该手指对应的名字。比如 0 是拇指,1 是食指等等。

package main

import (
    "fmt"
)

func main() {
    finger := 4
    switch finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    case 5:
        fmt.Println("Pinky")

    }
}
复制代码

在上述程序中,switch finger将 finger的值与每个 case语句进行比较。通过从上到下对每一个值进行对比,并执行与选项值匹配的第一个逻辑。在上述样例中, finger值为 4,因此打印的结果是 Ring

在选项列表中,case不允许出现重复项。如果您尝试运行下面的程序,编译器会报这样的错误: main.go:18:2:在tmp / sandbox887814166 / main.go:16:7

package main

import (
    "fmt"
)

func main() {
    finger := 4
    switch finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    case 4://重复项
        fmt.Println("Another Ring")
    case 5:
        fmt.Println("Pinky")

    }
}
复制代码

默认情况(Default Case)

我们每个人一只手只有 5 个手指。如果我们输入了不正确的手指编号会发生什么?这个时候就应该是属于默认情况。当其他情况都不匹配时,将运行默认情况。

package main

import (
    "fmt"
)

func main() {
    switch finger := 8; finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    case 5:
        fmt.Println("Pinky")
    default: // 默认情况
        fmt.Println("incorrect finger number")
    }
}
复制代码

在上述程序中 finger的值是 8,它不符合其中任何情况,因此会打印 incorrect finger number。default 不一定只能出现在 switch 语句的最后,它可以放在 switch 语句的任何地方。

您可能也注意到我们稍微改变了 finger变量的声明方式。finger声明在了 switch 语句内。在表达式求值之前,switch 可以选择先执行一个语句。在这行 switch finger:= 8; finger中, 先声明了finger变量,随即在表达式中使用了它。在这里,finger变量的作用域仅限于这个 switch 内。

多表达式判断

通过用逗号分隔,可以在一个 case 中包含多个表达式。

package main

import (
    "fmt"
)

func main() {
    letter := "i"
    switch letter {
    case "a", "e", "i", "o", "u": // 一个选项多个表达式
        fmt.Println("vowel")
    default:
        fmt.Println("not a vowel")
    }
}
复制代码

在 case "a","e","i","o","u":这一行中,列举了所有的元音。只要匹配该项,则将输出 vowel

无表达式的 switch

在 switch 语句中,表达式是可选的,可以被省略。如果省略表达式,则表示这个 switch 语句等同于 switch true,并且每个 case表达式都被认定为有效,相应的代码块也会被执行。

package main

import (
    "fmt"
)

func main() {
    num := 75
    switch { // 表达式被省略了
    case num >= 0 && num <= 50:
        fmt.Println("num is greater than 0 and less than 50")
    case num >= 51 && num <= 100:
        fmt.Println("num is greater than 51 and less than 100")
    case num >= 101:
        fmt.Println("num is greater than 100")
    }

}
复制代码

在上述代码中,switch 中缺少表达式,因此默认它为 true,true 值会和每一个 case 的求值结果进行匹配。case num >= 51 && <= 100:为 true,所以程序输出 num is greater than 51 and less than 100。这种类型的 switch 语句可以替代多个 if else子句。

Fallthrough 语句

在 Go 中,每执行完一个 case 后,会从 switch 语句中跳出来,不再做后续 case 的判断和执行。使用 fallthrough语句可以在已经执行完成的 case 之后,把控制权转移到下一个 case 的执行代码中。

让我们写一个程序来理解 fallthrough。我们的程序将检查输入的数字是否小于 50、100 或 200。例如我们输入 75,程序将输出75 is lesser than 100和 75 is lesser than 200。我们用 fallthrough 来实现了这个功能。

package main

import (
    "fmt"
)

func number() int {
    num := 15 * 5 
    return num
}

func main() {

    switch num := number(); { // num is not a constant
    case num < 50:
        fmt.Printf("%d is lesser than 50\n", num)
        fallthrough
    case num < 100:
        fmt.Printf("%d is lesser than 100\n", num)
        fallthrough
    case num < 200:
        fmt.Printf("%d is lesser than 200", num)
    }

}
复制代码

switch 和 case 的表达式不一定是常量。它们也可以在运行过程中通过计算得到。在上面的程序中,num 被初始化为函数 number()的返回值。程序运行到 switch 中时,会计算出 case 的值。case num < 100:的结果为 true,所以程序输出 75 is lesser than 100。当执行到下一句 fallthrough时,程序控制直接跳转到下一个 case 的第一个执行逻辑中,所以打印出 75 is lesser than 200。最后这个程序的输出会是

75 is lesser than 100  
75 is lesser than 200
复制代码

fallthrough 语句应该是 case 子句的最后一个语句。如果它出现在了 case 语句的中间,编译器将会报错:fallthrough statement out of place

11. 数组和切片

数组

数组是同一类型元素的集合。例如,整数集合 5,8,9,79,76 形成一个数组。Go 语言中不允许混合不同类型的元素,例如包含字符串和整数的数组。(译者注:当然,如果是 interface{} 类型数组,可以包含任意类型)

数组的声明

一个数组的表示形式为 [n]Tn表示数组中元素的数量,T代表每个元素的类型。元素的数量 n也是该类型的一部分(稍后我们将详细讨论这一点)。

可以使用不同的方式来声明数组,让我们一个一个的来看。

package main

import (
    "fmt"
)

func main() {
    var a [3]int //int array with length 3
    fmt.Println(a)
}
复制代码

var a[3]int声明了一个长度为 3 的整型数组。数组中的所有元素都被自动赋值为数组类型的零值。在这种情况下,a是一个整型数组,因此 a的所有元素都被赋值为 0,即 int 型的零值。运行上述程序将 输出[0 0 0]

数组的索引从 0开始到 length - 1结束。让我们给上面的数组赋值。

package main

import (
    "fmt"
)

func main() {
    var a [3]int //int array with length 3
    a[0] = 12 // array index starts at 0
    a[1] = 78
    a[2] = 50
    fmt.Println(a)
}
复制代码

a[0] 将值赋给数组的第一个元素。该程序将 输出[12 78 50]

让我们使用 简略声明来创建相同的数组。

package main

import (
    "fmt"
)

func main() {
    a := [3]int{12, 78, 50} // short hand declaration to create array
    fmt.Println(a)
}
复制代码

上面的程序将会打印相同的 输出[12 78 50]

在简略声明中,不需要将数组中所有的元素赋值。

package main

import (
    "fmt"
)

func main() {
    a := [3]int{12} 
    fmt.Println(a)
}
复制代码

在上述程序中的第 8 行 a := [3]int{12}声明一个长度为 3 的数组,但只提供了一个值 12,剩下的 2 个元素自动赋值为 0。这个程序将输出[12 0 0]

你甚至可以忽略声明数组的长度,并用 ...代替,让编译器为你自动计算长度,这在下面的程序中实现。

package main

import (
    "fmt"
)

func main() {
    a := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a)
}
复制代码

数组的大小是类型的一部分。因此 [5]int和 [25]int是不同类型。数组不能调整大小,不要担心这个限制,因为 slices的存在能解决这个问题。

package main

func main() {
    a := [3]int{5, 78, 8}
    var b [5]int
    b = a // not possible since [3]int and [5]int are distinct types
}
复制代码

在上述程序的第 6 行中, 我们试图将类型 [3]int的变量赋给类型为 [5]int的变量,这是不允许的,因此编译器将抛出错误 main.go:6: cannot use a (type [3]int) as type [5]int in assignment。

数组是值类型

Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。

package main

import "fmt"

func main() {
    a := [...]string{"USA", "China", "India", "Germany", "France"}
    b := a // a copy of a is assigned to b
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}
复制代码

在上述程序的第 7 行,a的副本被赋给 b。在第 8 行中,b的第一个元素改为 Singapore。这不会在原始数组 a中反映出来。该程序将 输出,

a is [USA China India Germany France]  
b is [Singapore China India Germany France]
复制代码

同样,当数组作为参数传递给函数时,它们是按值传递,而原始数组保持不变。

package main

import "fmt"

func changeLocal(num [5]int) {
    num[0] = 55
    fmt.Println("inside function ", num)
}
func main() {
    num := [...]int{5, 6, 7, 8, 8}
    fmt.Println("before passing to function ", num)
    changeLocal(num) //num is passed by value
    fmt.Println("after passing to function ", num)
}
复制代码

在上述程序的 13 行中, 数组 num实际上是通过值传递给函数 changeLocal,数组不会因为函数调用而改变。这个程序将输出,

before passing to function  [5 6 7 8 8]
inside function  [55 6 7 8 8]
after passing to function  [5 6 7 8 8]
复制代码

数组的长度

通过将数组作为参数传递给 len函数,可以得到数组的长度。

package main

import "fmt"

func main() {
    a := [...]float64{67.7, 89.8, 21, 78}
    fmt.Println("length of a is",len(a))
}
复制代码

上面的程序输出为 length of a is 4

使用 range 迭代数组

for循环可用于遍历数组中的元素。

package main

import "fmt"

func main() {
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ { // looping from 0 to the length of the array
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}
复制代码

上面的程序使用 for循环遍历数组中的元素,从索引 0到 length of the array - 1。这个程序运行后打印出,

0 th element of a is 67.70  
1 th element of a is 89.80  
2 th element of a is 21.00  
3 th element of a is 78.00
复制代码

Go 提供了一种更好、更简洁的方法,通过使用 for循环的 range方法来遍历数组。range返回索引和该索引处的值。让我们使用 range 重写上面的代码。我们还可以获取数组中所有元素的总和。

package main

import "fmt"

func main() {
    a := [...]float64{67.7, 89.8, 21, 78}
    sum := float64(0)
    for i, v := range a {//range returns both the index and value
        fmt.Printf("%d the element of a is %.2f\n", i, v)
        sum += v
    }
    fmt.Println("\nsum of all elements of a",sum)
}
复制代码

上述程序的第 8 行 for i, v := range a利用的是 for 循环 range 方式。 它将返回索引和该索引处的值。 我们打印这些值,并计算数组 a中所有元素的总和。 程序的 输出是

0 the element of a is 67.70
1 the element of a is 89.80
2 the element of a is 21.00
3 the element of a is 78.00

sum of all elements of a 256.5
复制代码

如果你只需要值并希望忽略索引,则可以通过用 _空白标识符替换索引来执行。

for _, v := range a { // ignores index  
}
复制代码

上面的 for 循环忽略索引,同样值也可以被忽略。

多维数组

到目前为止我们创建的数组都是一维的,Go 语言可以创建多维数组。

package main

import (
    "fmt"
)

func printarray(a [3][2]string) {
    for _, v1 := range a {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}

func main() {
    a := [3][2]string{
        {"lion", "tiger"},
        {"cat", "dog"},
        {"pigeon", "peacock"}, // this comma is necessary. The compiler will complain if you omit this comma
    }
    printarray(a)
    var b [3][2]string
    b[0][0] = "apple"
    b[0][1] = "samsung"
    b[1][0] = "microsoft"
    b[1][1] = "google"
    b[2][0] = "AT&T"
    b[2][1] = "T-Mobile"
    fmt.Printf("\n")
    printarray(b)
}

复制代码

在上述程序的第 17 行,用简略语法声明一个二维字符串数组 a 。20 行末尾的逗号是必需的。这是因为根据 Go 语言的规则自动插入分号。至于为什么这是必要的,如果你想了解更多,请阅读golang.org/doc/effecti…

另外一个二维数组 b 在 23 行声明,字符串通过每个索引一个一个添加。这是另一种初始化二维数组的方法。

第 7 行的 printarray 函数使用两个 range 循环来打印二维数组的内容。上述程序的 输出是

lion tiger
cat dog
pigeon peacock

apple samsung
microsoft google
AT&T T-Mobile

复制代码

这就是数组,尽管数组看上去似乎足够灵活,但是它们具有固定长度的限制,不可能增加数组的长度。这就要用到 切片了。事实上,在 Go 中,切片比传统数组更常见。

切片

切片是由数组建立的一种方便、灵活且功能强大的包装(Wrapper)。切片本身不拥有任何数据。它们只是对现有数组的引用。

创建一个切片

带有 T 类型元素的切片由 []T表示

package main

import (
    "fmt"
)

func main() {
    a := [5]int{76, 77, 78, 79, 80}
    var b []int = a[1:4] // creates a slice from a[1] to a[3]
    fmt.Println(b)
}

复制代码

使用语法 a[start:end]创建一个从 a数组索引 start开始到 end - 1结束的切片。因此,在上述程序的第 9 行中, a[1:4]从索引 1 到 3 创建了 a数组的一个切片表示。因此, 切片 b的值为 [77 78 79]

让我们看看另一种创建切片的方法。

package main

import (  
    "fmt"
)

func main() {  
    c := []int{6, 7, 8} // creates and array and returns a slice reference
    fmt.Println(c)
}

复制代码

在上述程序的第 9 行,c:= [] int {6,7,8}创建一个有 3 个整型元素的数组,并返回一个存储在 c 中的切片引用。

切片的修改

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。

package main

import (
    "fmt"
)

func main() {
    darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
    dslice := darr[2:5]
    fmt.Println("array before", darr)
    for i := range dslice {
        dslice[i]++
    }
    fmt.Println("array after", darr)
}

复制代码

在上述程序的第 9 行,我们根据数组索引 2,3,4 创建一个切片 dslice。for 循环将这些索引中的值逐个递增。当我们使用 for 循环打印数组时,我们可以看到对切片的更改反映在数组中。该程序的输出是

array before [57 89 90 82 100 78 67 69 59]  
array after [57 89 91 83 101 78 67 69 59]

复制代码

当多个切片共用相同的底层数组时,每个切片所做的更改将反映在数组中。

package main

import (
    "fmt"
)

func main() {
    numa := [3]int{78, 79 ,80}
    nums1 := numa[:] // creates a slice which contains all elements of the array
    nums2 := numa[:]
    fmt.Println("array before change 1", numa)
    nums1[0] = 100
    fmt.Println("array after modification to slice nums1", numa)
    nums2[1] = 101
    fmt.Println("array after modification to slice nums2", numa)
}

复制代码

在 9 行中,numa [:]缺少开始和结束值。开始和结束的默认值分别为 0和 len (numa)。两个切片 nums1和 nums2共享相同的数组。该程序的输出是

array before change 1 [78 79 80]  
array after modification to slice nums1 [100 79 80]  
array after modification to slice nums2 [100 101 80]

复制代码

从输出中可以清楚地看出,当切片共享同一个数组时,每个所做的修改都会反映在数组中。

切片的长度和容量

切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。

让我们写一段代码来更好地理解这点。

package main

import (
    "fmt"
)

func main() {
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) // length of is 2 and capacity is 6
}

复制代码

在上面的程序中,fruitslice是从 fruitarray的索引 1 和 2 创建的。 因此,fruitlice的长度为 2

fruitarray的长度是 7。fruiteslice是从 fruitarray的索引 1创建的。因此, fruitslice的容量是从 fruitarray索引为 1开始,也就是说从 orange开始,该值是 6。因此, fruitslice的容量为 6。该[程序]输出切片的 长度为 2 容量为 6

切片可以重置其容量。任何超出这一点将导致程序运行时抛出错误。

package main

import (
    "fmt"
)

func main() {
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) // length of is 2 and capacity is 6
    fruitslice = fruitslice[:cap(fruitslice)] // re-slicing furitslice till its capacity
    fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
}

复制代码

在上述程序的第 11 行中,fruitslice的容量是重置的。以上程序输出为,

length of slice 2 capacity 6 
After re-slicing length is 6 and capacity is 6

复制代码

使用 make 创建一个切片

func make([]T,len,cap)[]T 通过传递类型,长度和容量来创建切片。容量是可选参数, 默认值为切片长度。make 函数创建一个数组,并返回引用该数组的切片。

package main

import (
    "fmt"
)

func main() {
    i := make([]int, 5, 5)
    fmt.Println(i)
}

复制代码

使用 make 创建切片时默认情况下这些值为零。上述程序的输出为 [0 0 0 0 0]

追加切片元素

正如我们已经知道数组的长度是固定的,它的长度不能增加。 切片是动态的,使用 append可以将新元素追加到切片上。append 函数的定义是 func append(s[]T,x ... T)[]T

x ... T在函数定义中表示该函数接受参数 x 的个数是可变的。这些类型的函数被称为[可变函数]。

有一个问题可能会困扰你。如果切片由数组支持,并且数组本身的长度是固定的,那么切片如何具有动态长度。以及内部发生了什么,当新的元素被添加到切片时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。现在新切片的容量是旧切片的两倍。下面的程序会让你清晰理解。

package main

import (
    "fmt"
)

func main() {
    cars := []string{"Ferrari", "Honda", "Ford"}
    fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) // capacity of cars is 3
    cars = append(cars, "Toyota")
    fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) // capacity of cars is doubled to 6
}

复制代码

在上述程序中,cars的容量最初是 3。在第 10 行,我们给 cars 添加了一个新的元素,并把 append(cars, "Toyota")返回的切片赋值给 cars。现在 cars 的容量翻了一番,变成了 6。上述程序的输出是

cars: [Ferrari Honda Ford] has old length 3 and capacity 3  
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6

复制代码

切片类型的零值为 nil。一个 nil切片的长度和容量为 0。可以使用 append 函数将值追加到 nil切片。

package main

import (  
    "fmt"
)

func main() {  
    var names []string //zero value of a slice is nil
    if names == nil {
        fmt.Println("slice is nil going to append")
        names = append(names, "John", "Sebastian", "Vinay")
        fmt.Println("names contents:",names)
    }
}

复制代码

在上面的程序 names是 nil,我们已经添加 3 个字符串给 names。该程序的输出是

slice is nil going to append  
names contents: [John Sebastian Vinay]

复制代码

也可以使用 ...运算符将一个切片添加到另一个切片。 你可以在[可变参数函数]教程中了解有关此运算符的更多信息。

package main

import (
    "fmt"
)

func main() {
    veggies := []string{"potatoes", "tomatoes", "brinjal"}
    fruits := []string{"oranges", "apples"}
    food := append(veggies, fruits...)
    fmt.Println("food:",food)
}

复制代码

在上述程序的第 10 行,food 是通过 append(veggies, fruits...) 创建。程序的输出为 food: [potatoes tomatoes brinjal oranges apples]

切片的函数传递

我们可以认为,切片在内部可由一个结构体类型表示。这是它的表现形式,

type slice struct {  
    Length        int
    Capacity      int
    ZerothElement *byte
}

复制代码

切片包含长度、容量和指向数组第零个元素的指针。当切片传递给函数时,即使它通过值传递,指针变量也将引用相同的底层数组。因此,当切片作为参数传递给函数时,函数内所做的更改也会在函数外可见。让我们写一个程序来检查这点。

package main

import (
    "fmt"
)

func subtactOne(numbers []int) {
    for i := range numbers {
        numbers[i] -= 2
    }
}
func main() {
    nos := []int{8, 7, 6}
    fmt.Println("slice before function call", nos)
    subtactOne(nos)                               // function modifies the slice
    fmt.Println("slice after function call", nos) // modifications are visible outside
}

复制代码

上述程序的行号 17 中,调用函数将切片中的每个元素递减 2。在函数调用后打印切片时,这些更改是可见的。如果你还记得,这是不同于数组的,对于函数中一个数组的变化在函数外是不可见的。上述[程序]的输出是,

array before function call [8 7 6]  
array after function call [6 5 4]

复制代码

多维切片

类似于数组,切片可以有多个维度。

package main

import (
    "fmt"
)

func main() {  
     pls := [][]string {
            {"C", "C++"},
            {"JavaScript"},
            {"Go", "Rust"},
            }
    for _, v1 := range pls {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}

复制代码

程序的输出为,

C C++  
JavaScript  
Go Rust

复制代码

内存优化

切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中。

一种解决方法是使用 [copy] 函数 func copy(dst,src[]T)int来生成一个切片的副本。这样我们可以使用新的切片,原始数组可以被垃圾回收。

package main

import (
    "fmt"
)

func countries() []string {
    countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
    neededCountries := countries[:len(countries)-2]
    countriesCpy := make([]string, len(neededCountries))
    copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
    return countriesCpy
}
func main() {
    countriesNeeded := countries()
    fmt.Println(countriesNeeded)
}

复制代码

在上述程序的第 9 行,neededCountries := countries[:len(countries)-2创建一个去掉尾部 2 个元素的切片 countries,在上述程序的 11 行,将 neededCountries复制到 countriesCpy同时在函数的下一行返回 countriesCpy。现在 countries数组可以被垃圾回收, 因为 neededCountries不再被引用。

 

12. 可变参数函数

什么是可变参数函数

可变参数函数是一种参数个数可变的函数。

语法

如果函数最后一个参数被记作 ...T,这时函数可以接受任意个 T类型参数作为最后一个参数。

请注意只有函数的最后一个参数才允许是可变的。

通过一些例子理解可变参数函数如何工作

你是否曾经想过 append 函数是如何将任意个参数值加入到切片中的。这样 append 函数可以接受不同数量的参数。

func append(slice []Type, elems ...Type) []Type
复制代码

上面是 append 函数的定义。在定义中 elems 是可变参数。这样 append 函数可以接受可变化的参数。

让我们创建一个我们自己的可变参数函数。我们将写一段简单的程序,在输入的整数列表里查找某个整数是否存在。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    find(89, 89, 90, 95)
    find(45, 56, 67, 45, 90, 109)
    find(78, 38, 56, 98)
    find(87)
}
复制代码

在上面程序中 func find(num int, nums ...int)中的 nums可接受任意数量的参数。在 find 函数中,参数 nums相当于一个整型切片。

可变参数函数的工作原理是把可变参数转换为一个新的切片。以上面程序中的第 22 行为例,find 函数中的可变参数是 89,90,95 。 find 函数接受一个 int 类型的可变参数。因此这三个参数被编译器转换为一个 int 类型切片 int []int{89, 90, 95} 然后被传入 find函数。

在第 10 行, for循环遍历 nums切片,如果 num在切片中,则打印 num的位置。如果 num不在切片中,则打印提示未找到该数字。

上面代码的输出值如下,

type of nums is []int
89 found at index 0 in [89 90 95]

type of nums is []int
45 found at index 2 in [56 67 45 90 109]

type of nums is []int
78 not found in  [38 56 98]

type of nums is []int
87 not found in  []
复制代码

在上面程序的第 25 行,find 函数仅有一个参数。我们没有给可变参数 nums ...int传入任何参数。这也是合法的,在这种情况下 nums是一个长度和容量为 0 的 nil切片。

给可变参数函数传入切片

下面例子中,我们给可变参数函数传入一个切片,看看会发生什么。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95}
    find(89, nums)
}
复制代码

在第 23 行中,我们将一个切片传给一个可变参数函数。

这种情况下无法通过编译,编译器报出错误 main.go:23: cannot use nums (type []int) as type int in argument to find

为什么无法工作呢?原因很直接,find函数的说明如下,

func find(num int, nums ...int)
复制代码

由可变参数函数的定义可知,nums ...int意味它可以接受 int类型的可变参数。

在上面程序的第 23 行,nums作为可变参数传入 find函数。前面我们知道,这些可变参数参数会被转换为 int类型切片然后在传入 find函数中。但是在这里 nums已经是一个 int 类型切片,编译器试图在 nums基础上再创建一个切片,像下面这样

find(89, []int{nums})
复制代码

这里之所以会失败是因为 nums是一个 []int类型 而不是 int类型。

那么有没有办法给可变参数函数传入切片参数呢?答案是肯定的。

有一个可以直接将切片传入可变参数函数的语法糖,你可以在在切片后加上 ... 后缀。如果这样做,切片将直接传入函数,不再创建新的切片

在上面的程序中,如果你将第 23 行的 find(89, nums)替换为 find(89, nums...),程序将成功编译并有如下输出

type of nums is []int
89 found at index 0 in [89 90 95]
复制代码

下面是完整的程序供您参考。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95}
    find(89, nums...)
}
复制代码

不直观的错误

当你修改可变参数函数中的切片时,请确保你知道你正在做什么。

下面让我们来看一个简单的例子。

package main

import (
    "fmt"
)

func change(s ...string) {  
    s[0] = "Go"
}

func main() {
    welcome := []string{"hello", "world"}
    change(welcome...)
    fmt.Println(welcome)
}
复制代码

你认为这段代码将输出什么呢?如果你认为它输出 [Go world]。恭喜你!你已经理解了可变参数函数和切片。如果你猜错了,那也不要紧,让我来解释下为什么会有这样的输出。

在第 13 行,我们使用了语法糖 ...并且将切片作为可变参数传入 change函数。

正如前面我们所讨论的,如果使用了 ...welcome切片本身会作为参数直接传入,不需要再创建一个新的切片。这样参数 welcome将作为参数传入 change函数

在 change函数中,切片的第一个元素被替换成 Go,这样程序产生了下面的输出值

[Go world]
复制代码

这里还有一个例子来理解可变参数函数。

package main

import (
    "fmt"
)

func change(s ...string) {
    s[0] = "Go"
    s = append(s, "playground")
    fmt.Println(s)
}

func main() {
    welcome := []string{"hello", "world"}
    change(welcome...)
    fmt.Println(welcome)
}
复制代码

13. Maps

什么是 map ?

map 是在 Go 中将值(value)与键(key)关联的内置类型。通过相应的键可以获取到值。

如何创建 map ?

通过向 make函数传入键和值的类型,可以创建 map。make(map[type of key]type of value)是创建 map 的语法。

personSalary := make(map[string]int)
复制代码

上面的代码创建了一个名为 personSalary的 map,其中键是 string 类型,而值是 int 类型。

map 的零值是 nil。如果你想添加元素到 nil map 中,会触发运行时 panic。因此 map 必须使用 make函数初始化。

package main

import (
    "fmt"
)

func main() {  
    var personSalary map[string]int
    if personSalary == nil {
        fmt.Println("map is nil. Going to make one.")
        personSalary = make(map[string]int)
    }
}
复制代码

上面的程序中,personSalary 是 nil,因此需要使用 make 方法初始化,程序将输出 map is nil. Going to make one.

给 map 添加元素

给 map 添加新元素的语法和数组相同。下面的程序给 personSalarymap 添加了几个新元素。

package main

import (
    "fmt"
)

func main() {
    personSalary := make(map[string]int)
    personSalary["steve"] = 12000
    personSalary["jamie"] = 15000
    personSalary["mike"] = 9000
    fmt.Println("personSalary map contents:", personSalary)
}
复制代码

上面的程序输出:personSalary map contents: map[steve:12000 jamie:15000 mike:9000]

你也可以在声明的时候初始化 map。

package main

import (  
    "fmt"
)

func main() {  
    personSalary := map[string]int {
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("personSalary map contents:", personSalary)
}
复制代码

上面的程序声明了 personSalary,并在声明的同时添加两个元素。之后又添加了键 mike。程序输出:

personSalary map contents: map[steve:12000 jamie:15000 mike:9000]
复制代码

键不一定只能是 string 类型。所有可比较的类型,如 boolean,interger,float,complex,string 等,都可以作为键。关于可比较的类型,如果你想了解更多,请访问 golang.org/ref/spec#Co…

获取 map 中的元素

目前我们已经给 map 添加了几个元素,现在学习下如何获取它们。获取 map 元素的语法是 map[key]

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    employee := "jamie"
    fmt.Println("Salary of", employee, "is", personSalary[employee])
}
复制代码

上面的程序很简单。获取并打印员工 jamie的薪资。程序输出 Salary of jamie is 15000

如果获取一个不存在的元素,会发生什么呢?map 会返回该元素类型的零值。在 personSalary这个 map 里,如果我们获取一个不存在的元素,会返回 int类型的零值 0

package main

import (  
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    employee := "jamie"
    fmt.Println("Salary of", employee, "is", personSalary[employee])
    fmt.Println("Salary of joe is", personSalary["joe"])
}
复制代码

上面程序输出:

Salary of jamie is 15000
Salary of joe is 0
复制代码

上面程序返回 joe的薪资是 0。personSalary中不包含 joe的情况下我们不会获取到任何运行时错误。

如果我们想知道 map 中到底是不是存在这个 key,该怎么做:

value, ok := map[key]
复制代码

上面就是获取 map 中某个 key 是否存在的语法。如果 ok是 true,表示 key 存在,key 对应的值就是 value,反之表示 key 不存在。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    newEmp := "joe"
    value, ok := personSalary[newEmp]
    if ok == true {
        fmt.Println("Salary of", newEmp, "is", value)
    } else {
        fmt.Println(newEmp,"not found")
    }
}
复制代码

上面的程序中,第 15 行,joe不存在,所以 ok是 false。程序将输出:

joe not found
复制代码

遍历 map 中所有的元素需要用 for range循环。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("All items of a map")
    for key, value := range personSalary {
        fmt.Printf("personSalary[%s] = %d\n", key, value)
    }

}
复制代码

上面程序输出:

All items of a map
personSalary[mike] = 9000
personSalary[steve] = 12000
personSalary[jamie] = 15000
复制代码

有一点很重要,当使用 for range 遍历 map 时,不保证每次执行程序获取的元素顺序相同。

删除 map 中的元素

删除 map中 key的语法是 [delete(map, key)]。这个函数没有返回值。

package main

import (  
    "fmt"
)

func main() {  
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("map before deletion", personSalary)
    delete(personSalary, "steve")
    fmt.Println("map after deletion", personSalary)

}
复制代码

上述程序删除了键 "steve",输出:

map before deletion map[steve:12000 jamie:15000 mike:9000]
map after deletion map[mike:9000 jamie:15000]
复制代码

获取 map 的长度

获取 map 的长度使用 [len]函数。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("length is", len(personSalary))

}
复制代码

上述程序中的 len(personSalary)函数获取了 map 的长度。程序输出 length is 3

Map 是引用类型

和 [slices]类似,map 也是引用类型。当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("Original person salary", personSalary)
    newPersonSalary := personSalary
    newPersonSalary["mike"] = 18000
    fmt.Println("Person salary changed", personSalary)

}
复制代码

上面程序中的第 14 行,personSalary被赋值给 newPersonSalary。下一行 ,newPersonSalary中 mike的薪资变成了 18000personSalary中 Mike的薪资也会变成 18000。程序输出:

Original person salary map[steve:12000 jamie:15000 mike:9000]
Person salary changed map[steve:12000 jamie:15000 mike:18000]
复制代码

当 map 作为函数参数传递时也会发生同样的情况。函数中对 map 的任何修改,对于外部的调用都是可见的。

Map 的相等性

map 之间不能使用 ==操作符判断,==只能用来检查 map 是否为 nil

package main

func main() {
    map1 := map[string]int{
        "one": 1,
        "two": 2,
    }

    map2 := map1

    if map1 == map2 {
    }
}
复制代码

上面程序抛出编译错误 invalid operation: map1 == map2 (map can only be compared to nil)

判断两个 map 是否相等的方法是遍历比较两个 map 中的每个元素。我建议你写一段这样的程序实现这个功能

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值