Go 语言基础

一、变量和声明

下面的例子是 Go 中,声明变量和赋值最为明确的方法,但也是最为冗长的方法:

package main

import (
  "fmt"
)

func main() {
  var power int
  power = 9000
  fmt.Printf("It's over %d\n", power)
}

这里我们定义了一个 int 类型的变量 power。默认情况下,Go 会为变量分配默认值。Integers 的默认值是 0,booleans 默认值是 false,strings 默认值是 "" 等等。下面,我们创建一个值为 9000 的名为 power 的变量。我们可以将定义和赋值两行代码合并起来:

var power int = 9000

不过,这么写太长了。Go 提供了一个方便的短变量声明运算符 := ,它可以自动推断变量类型,但是这种声明运算符只能用于局部变量,不可用于全局变量:

power := 9000

这非常方便,它可以跟函数结合使用,就像这样:

func main() {
  power := getPower()
}

func getPower() int {
  return 9001
}

值得注意的是要用 := 来声明变量以及给变量赋值。相同变量不能被声明两次(在相同作用域下),如果你尝试这样,会收到错误提示。

func main() {
  power := 9000
  fmt.Printf("It's over %d\n", power)

  // 编译器错误:
  //  := 左侧不是新的变量
  power := 9001
  fmt.Printf("It's also over %d\n", power)
}

编译器会告诉你 := 左侧不是新的变量。这就意味着当我们首次声明一个变量时应该使用 := ,后面再给变量赋值时应该使用 =。这似乎很有道理,但是凭空来记忆且需要根据情况来切换却是很难的事。

如果你仔细阅读代码的错误信息,你会发现 variables 单词是个复数,即有多个变量,那是因为go支持多个变量同时赋值(使用 = 或者 :=):

func main() {
  name, power := "Goku", 9000
  fmt.Printf("%s's power is over %d\n", name, power)
}

另外,多个变量赋值的时候,只要其中有一个变量是新的,就可以使用:=。例如:

func main() {
  power := 1000
  fmt.Printf("default power is %d\n", power)

  name, power := "Goku", 9000
  fmt.Printf("%s's power is over %d\n", name, power)
}

尽管变量 power 使用了两次:=,但是编译器不会在第 2 次使用 :=时报错,因为这里有一个新的 name变量,它可以使用:=。然后你不能改变 power 变量的类型,它已经被声明成一个整型,所以只能赋值整数。

到目前为止,你最后需要了解的一件事是,Go 会像 import 一样,不允许你在程序中拥有未使用的变量。例如:

func main() {
  name, power := "Goku", 1000
  fmt.Printf("default power is %d\n", power)
}

这将不会通过编译,因为 name 是一个被声明但是未被使用的变量,就像 import 的包未被使用时,也将会导致编译失败,但总的来说,我认为这有助于提高代码的清洁度和可读性。

还有更多关于声明和赋值的技巧。初始化一个变量时,请使用:var NAME TYPE;给变量声明及赋值时,请使用: NAME := VALUE ; 给之前已经声明过的变量赋值时,请使用: NAME = VALUE

二、静态类型

静态类型意味着变量必须是特定的类型(如:int, string, bool, []byte 等等),这可以通过在声明变量的时候,指定变量的类型来实现,或者让编译器自行推断变量的类型(我们将很快可以看到实例)。

关于静态类型的东西,可以说的还有很多,但是我相信通过看代码能更好的理解静态类型是什么。如果你习惯于动态类型语言, 你可能会发现这很麻烦。这种想法没错,但是静态类型语言也有优点,特别是当你将静态类型和编译配对使用时。这两者经常混为一谈。确实当你有其中一个的时候,通常也会有另一个,但是这不是硬性规定的。使用强类型系统,编译器能够检测除语法错误之外的问题从而进一步优化。

三、类 C 语法

当说到一门语言是类 C 语法的时候,通常意味着如果你用过其他类 C 语言如:C,C++,Java,JavaScript 和 C#,你会觉得 Go 的语法很熟悉——最少表面上是这样的。举个例子,&& 用于逻辑 AND,== 用于判断是否相等,{} 是块的开始和结束,数组下标的起始值为 0。

类 C 语法也倾向于用分号表示作为语句结束符,并将条件写在括号中。Go 不支持这些,但是仍然使用括号来控制优先级。例如,一个 if 语句是这样的:

if name == "Leto" {
  print("the spice must flow")
}

在很多复杂系统中,括号符还是很有用的:

if (name == "Goku" && power > 9000) || (name == "gohan" && power < 4000)  {
  print("super Saiyan")
}

除此之外,Go 要比 C# 或 Java 更接近 C - 不仅是语法方面,还有目的方面。这反映在语言的简洁和简单上,希望你在学习它的时候能慢慢体会这一点。

四、垃圾回收

一些变量,在创建的时候,就拥有一个简单定义的生命周期。对于函数中的变量,会在函数执行完后进行销毁。在别的语言中,对于编译器而言,这不会很明显。例如:函数返回的变量,或者由其他变量和对象所调用的变量,它们的生命周期是很难确定的。 如果没有垃圾回收机制,那么开发人员就得知道有哪些不需要用到的变量,并将它们释放。就像 C 语言,你需要使用 free(str); 来释放变量。

语言的垃圾回收机制(像:Ruby, Python, Java, JavaScript, C# , Go)是会对变量进行跟踪,并在没有使用它们的时候,进行释放。垃圾回收会增加一些额外的开销,但是也减少了一些致命性的 BUG。

五、运行 go 代码

创建一个简单的程序然后学习如何编译和运行它。打开你的文本编辑器写入下面的代码:

package main

func main() {
  println("it's over 9000!")
}

保存文件并命名为 main.go 。 你可以将文件保存在任何地方;不必将这些琐碎的例子放在 go 的工作空间内。

接下来,打开一个 shell 或者终端提示符,进入到文件保存的目录内, 对于我而言, 应该输入 cd ~/code 进入到文件保存目录。

最后,通过敲入以下命令来运行程序:

go run main.go

如果一切正常(即你的 golang 环境配置的正确),你将看到 it's over 9000!

但是编译步骤是怎么样的呢? go run 命令已经包含了编译运行。它使用一个临时目录来构建程序,执行完然后清理掉临时目录。你可以执行以下命令来查看临时文件的位置:

go run --work main.go

明确要编译代码的话,使用 go build:

go build main.go

这将产生一个可执行文件,名为 main ,你可以执行该文件。如果是在 Linux / OSX 系统中,别忘了使用 ./ 前缀来执行,也就是输入 ./main

在开发中,你既可以使用 go run 也可以使用 go build 。但当你正式部署代码的时候,你应该部署通过 go build 产生的二进制文件并且执行它。

入口函数 Main

希望刚才执行的代码是可以理解的。我们刚刚创建了一个函数,并且使用内置函数 println 打印出了字符串。难道仅因为这里只有一个选择,所以 go run 知道执行什么吗??不。在 go 中程序入口必须是 main 函数,并且在 main 包内。

我们将在后面的章节中详细介绍。目前,我们将专注于理解 go 基础,一直会在 main 包中写代码。

如果你想尝试,你可以修改代码并且可以更改包名。使用 go run 执行程序将出现一个错误。 接着你可以将包名改回 main ,换一个不同的方法名,你会看到一个不同的错误。尝试使用 go build 代替 go run 来执行刚才的代码,注意代码编译时,没有入口点可以执行。但当你构建一个库时,却是完全正确的。

六、导入包

Go 有很多内建函数,例如 println,可以在没有引用情况下直接使用。但是,如果只使用 Go 的标准库而不使用第三方库,我们就无法走的更远。import 关键字被用于去声明文件中代码要使用的包。

修改下我们的程序:

package main

import (
  "fmt"
  "os"
)

func main() {
  if len(os.Args) != 2 {
    os.Exit(1)
  }
  fmt.Println("It's over", os.Args[1])
}

你可以这样运行:

go run main.go 9000

我们现在用了 Go 的两个标准包:fmtos 。我们也介绍了另一个内建函数 lenlen 返回字符串的长度,字典值的数量,或者我们这里看到的,它返回了数组元素的数量。如果你想知道我们这里为什么期望得到两个参数,它是因为第一个参数 -- 索引0处 -- 总是当前可运行程序的路径。(更改程序将它打印出来亲自看看就知道了)

你可能注意到了,我们在函数名称前加上了前缀包名,例如,fmt.PrintLn。这是不同于其他很多语言的。后续的章节中我们将学习到更多关于包的知识。现在,知道如何导入以及使用一个包就是一个好的开始。

在 Go 中,关于导包是很严格的。如果你导入了一个包却没有使用将会导致编译不通过。尝试运行下面的程序:

package main

import (
  "fmt"
  "os"
)

func main() {
}

你应该会得到两个关于 fmtos 被导入却没有被使用的错误。这很烦人的是不是呀?绝对是这样的,不过随着时间的推移,你将慢慢习惯它(虽然仍然烦人,不过要以 Go 的思维写 Go)。Go 如此严格是因为没用的导入会降低编译速度;诚然,我们大多数人不会关注这个问题。

另一个需要记住的事情是 Go 的标准库已经有了很好的文档。你可以访问 https://golang.org/pkg/fmt/#Println 去看更多关于 PrintLn 函数的信息。你可以点击那个部分的头去看源代码。另外,也可以滚动到顶部查看关于 Go 格式化功能的更多消息。

如果没有互联网,你可以这样在本地获取文档:

godoc -http=:6060

然后浏览器中访问 http://localhost:6060

七、函数声明

这是个指出函数是可以返回多个值的好时机。让我们看三个函数:一个没有返回值,一个有一个返回值,一个有两个返回值。

func log(message string) {
}

func add(a int, b int) int {
}

func power(name string) (int, bool) {
}

我们可以像这样使用最后一个:

value, exists := power("goku")
if exists == false {
  // 处理错误情况
}

有时候,你仅仅关注其中一个返回值。这个情况下,你可以将其他的返回值赋值给空白符_

_, exists := power("goku")
if exists == false {
  // handle this error case
}

这不仅仅是一个惯例。_ ,空白标识符,特殊在于实际上返回值并没有赋值。这让你可以一遍又一遍地使用 _ 而不用管它的类型。

最后,关于函数声明还有些要说的。如果参数有相同的类型,您可以用这样一个简洁的用法:

func add(a, b int) int {

}

返回多个值可能是你经常使用的,你也可能会频繁地使用 _ 丢弃一个值。命名返回值和稍微冗长的参数声明不太常用。尽管如此,你将很快遇到他们,所以了解他们很重要。