Go 学习笔记(二)简介

本文原创地址:博客园骏马金龙go 基础系列:简介

Go 简介

Go 语言是编译型、静态类型的类 C 的语言,并带有 GC(垃圾收集器,garbage collection)。这意味着 Go 会自动回收一些用不到的对象,让大家把关注的重点更多的放在业务上,从而提高开发的效率。

另外, Go 是一种非常严格的语言,它几乎总是要求我们 "以标准答案去答题",在其它语言可以容忍的不规范编码方式在 Go 语言中几乎都会抛异常 。例如导入了包却没有使用这个包,Go 不会去编译它并报错。再例如,定义了一个变量但从来没用过,也会报错。

初学 Go 的时候,这可能是件无比的苦恼事情,但习惯了之后,编写出来的程序自然是无比规范的。这也正是 Go 和不少语言的区别:其它语言编码、调试阶段可能很快,但维护和优化阶段可能会非常长;而 Go 的编码周期可能稍长,但编码完成后几乎都是足够优化的,维护和优化周期足够短。

编译型

编译表示的是将你所写的源代码转换为低层次的语言,例如汇编语言 (go 采用此底层语言),或者其它中间的语言 (如 Java、C# 编译成字节码)。

编译型语言可能不太友好,因为编译的过程速度很慢。如果一个程序的编译过程就需要花几分钟甚至几小时,那么程序的版本迭代可能会很难进行下去。编译速度是 Go 语言的一个主要设计目标,值得庆幸的是,Go 的编译速度很快,即便对于习惯于使用解释型语言的人来说,它也还是快。

编译型语言虽然编译过程慢,但这类语言在运行阶段可能会更快,而且运行时不再需要加载额外的依赖。

静态类型

静态语言意味着变量必须要指定数据类型 (int,string,bool,[]byte 等 )。虽然必须指定数据类型,但除了在声明变量的时候显式指定数据类型,也可以让 Go 自己去推断数据类型 ( 稍后有示例)。

对于习惯于使用动态型语言的人来说,可能会感觉静态型语言很笨重,事实确实如此。但静态有静态的好处,特别是配合编译操作的时候。

关于静态和动态数据类型,要说的内容其实很多很多,毕竟对于一门语言来说,数据类型牵一发而动全身,无论是静态、还是动态型语言,都因此而衍生出无数的优、缺点。

类 C 型的语言

当我们说一门语言是类 C 型 (C-like) 的语言时,意味着这门语言里有一些语法和特性和 C 语言是类似的。例如,&&表示布尔的 AND,==表示等值比较,数组索引从 0 开始计算,{...}表示一段代码块,也表示它属于一个作用域范围,等等。

类 C 型语言也意味着每行的语句要使用分号 ";" 结束,条件表达式要使用括号包围。但 Go 语言不采用这两种方式,尽管还是可以使用括号包围条件表达式以改变优先级。例如:

if name == "malongshuai" {
	print("name rigth!")
}

一个更复杂一点的条件表达式,使用括号改变优先级:

if (name == "longshuai" && age > 23) || (name == "xiaofang" && age < 22) {
	print("yeyeye!!!")
}

GC

每当创建一个变量后,这个变量都会有其生命周期。例如,函数内部的本地变量将在函数退出的时候消逝。对于非函数内部的变量生命周期,无论是对程序员还是对编译器来说,变量的生命周期都没有那么显而易见。

没有 garbage collection,意味着要让程序员自己来决定变量所占用内存的释放,这是很艰巨的任务,而且很容易出错导致程序崩溃。

带有 GC 的语言可以对变量进行跟踪,并且在它们不再被需要的时候自动释放它们。虽然 GC 带来了一点点的负载,会影响一点点的性能,但对于现在高性能的计算机来说,这点影响相比它带来的优点而言,完全可以将其无视。

尝试写一个简单的 Go 程序

按照国际管理,每一门语言总是以 hello world 开篇。这里就算了,因为我有我的惯例。

先安装 Go,so easy...

目前还没有必要涉及 Go 的工作空间,所以随意找个地方创建一个 test.go 文件,内容如下:

package main

func main() {
	println("Let's Go")
}

然后运行:

go run test.go

显然,它将输出Let's Go。但是 Go 的编译过程呢?go run命令同时进行了编译和运行两个过程:它将使用一个临时目录保存构建的程序,然后执行它,最后自动清理构建出来的临时程序。

可以使用go run --work查看下具体情况:

$ go run --work test.go
WORK=/tmp/go-build267589647
Let's Go

构建的临时目录位于 /tmp/go-buildXXXX 中 (我这是 Linux),在此目录下会有一个二进制程序 (对于 Windows 则是.exe 文件):

$ tree /tmp/go-build267589647/
/tmp/go-build267589647/
├── command-line-arguments
│   └── _obj
│       └── exe
│           └── test    # 这是可执行二进制程序
└── command-line-arguments.a

那个 test 文件就是编译后得到的二进制程序,可以直接用来执行:

$ /tmp/go-build267589647/command-line-arguments/_obj/exe/test 
Let's Go

如果要显式编译,使用go build命令:

go build test.go

它将在当前目录下生成一个名为 test 的二进制文件,可以直接拿来运行,就像前面 /tmp 中的一样。

$ ./tese
Let's Go

在开发阶段,用 go build 还是用 go run,随意即可。但在部署的时候,一般先 go build,再 go run。

main 包和 main 函数

在上面的代码中,声明了这个包的名称为 main,然后创建一个函数,并在此函数中使用println输出了一个字符串,但是go run如何知道要去执行什么? 在 Go 中,程序的入口是 main 包中的 main 函数 ,这两名称都是固定的。

对于一个从没编程过的人,可能不理解程序的入口。它表示程序从此处开始执行,函数 main 中可能会包含很多其它函数的调用,这些函数可能放在其它文件 (包) 中。通过一次次、一层层的调用,从而将整个程序的所有代码、逻辑都连接在一起并运行。

如果你愿意,可以试着修改一下 package 后面的main关键字,然后go rungo build都运行一下。再试着修改一下func mainmain关键字,go rungo build再运行一下。

关于包的内容,后面再做介绍。目前来说,需要理解的只是些基础,对于基础阶段来说,我们将总是在 main 包中写代码。

import

Go 有一些内置的函数,例如上面的println,内置函数无需额外的引用就可直接调用。但内置函数毕竟很少,所以得从已经写好的 Go 标准库和其它第三方库中找出一些工具来使用。

在 Go 中,import关键字用于定义要导入到当前文件的包名,导入某个包后,这个包中的属性就能在当前文件中去访问,例如调用属于这个包的函数。

例如,将前面的代码改改:

package main

import (
	"fmt"
	"os"
)

func main (){
	if len(os.Args) != 2{
		os.Exit(1)
	}
	fmt.Println("Arg0: ",os.Args[0])
	fmt.Println("Arg1: ",os.Args[1])
}

执行一下:

$ go run test.go
exit status 1

$ go run test.go a b
exit status 1

$ go run test.go a 
Arg0:  /tmp/go-build730099388/command-line-arguments/_obj/exe/test
Arg1:  a

上面的 import 导入了两个标准包:fmtos,还使用了另一个内置函数len()

len()函数返回字符串的长度、字典的元素个数以及数组的元素个数。上面使用 len() 判断了该 Go 程序的参数个数必须为 2,否则就以状态码 1 退出该程序。看上面的运行结果,好像只有给一个参数的时候才是正确的,这是因为第一个参数 (Args[0]) 代表的总是当前正在运行的程序名称,正如上面结果所显示的那样。

你可能还注意到了fmt.Println,前缀fmt正好是导入的一个包名,这表示使用 fmt 包中的 Println 函数。

本文的开头就说过了,Go 是一门非常严格的语言,如果这里导入了 fmt 包,但却没有使用它,它将报错。

# command-line-arguments
./test.go:4:5: imported and not used: "fmt"

关于 Go 文档

关于 fmt 的 Println 函数详细用法,可去参考 Go 的官方文档。当然,现阶段去看官方手册,会事倍功半。

还可以使用go doc命令去查找各帮助文档。

例如,查看 fmt 包的帮助文档:

go doc fmt

查看fmt.Println函数的用法:

go doc fmt.Println

完整用法:

go doc
go doc <pkg>
go doc <sym>[.<method>]
go doc [<pkg>].<sym>[.<method>]
go doc <pkg> <sym>[.<method>]

此外,还可以构建本地的网页版官方手册,在断网的时候可以访问:

godoc -http=:6060

然后就可以在浏览器中通过访问本地官方手册

变量和变量声明

很多语言中,要为变量赋值只需一个语句:

x=10

这个语句中实际上包含了两个过程: 变量的声明和变量的赋值。声明一般也被称为 "定义"

在 Go 中, 必须先声明变量,再赋值或使用变量 。最复杂的声明 + 赋值操作为:

package main

import ( "fmt" )

func main(){
	var x int
	x=10
	fmt.Println("x =",x)
}

此处声明了一个变量 x,其数据类型为int。默认情况下,Go 在变量的声明期间会为其做初始化赋值:int 类型初始化赋值为 0,booleans 初始化赋值为 false,strings 初始化赋值为 "",等等。

可以将声明和赋值操作合并:

var x int = 10

还有一种更方便的声明 + 赋值方式:

x := 10

通过这种变量的定义方式,还可以将函数执行结果 (返回值) 赋值给变量。例如:

func main() {
	x := getAdd(10)
}

func getAdd(x int) int {
	return x+1
}

:=在 Go 中属于类型推断操作,它包含了变量声明和变量赋值两个过程。

需要注意的是,变量声明之后不能再次声明 (除非在不同的作用域),之后只能使用=进行赋值。例如,执行下面的代码将报错:

package main

import ("fmt")

func main(){
	x:=10
	fmt.Println("x =",x)
	x:=11
	fmt.Println("x =",x)
}

错误如下:

# command-line-arguments
.\test.go:8:3: no new variables on left side of :=

报错信息很明显,:=左边没有新变量。

如果仔细看上面的报错信息,会发现no new variables是一个复数。实际上,Go 允许我们使用:=一次性声明、赋值多个变量,而且只要左边有任何 一个 新变量,语法就是正确的。

func main(){
	name,age := "longshuai",23
	fmt.Println("name:",name,"age:",age)
	
	// name重新赋值,因为有一个新变量weight
	weight,name := 90,"malongshuai"
	fmt.Println("name:",name,"weight:",weight)
}

需要注意,name 第二次被:=赋值,Go 第一次推断出该变量的数据类型之后,就不允许:=再改变它的数据类型,因为只有第一次:=对 name 进行声明,之后所有的:=对 name 都只是简单的赋值操作。

例如,下面将报错:

weight,name := 90,80

错误信息:

.\test.go:11:14: cannot use 80 (type int) as type string in assignment

另外,变量声明之后必须使用,否则会报错,因为 Go 对规范的要求非常严格。例如,下面定义了weight但却没使用:

weight,name := 90,"malongshuai"
fmt.Println("name:",name)

错误信息:

.\test.go:11:2: weight declared and not used

函数定义

Go 的函数允许有多个返回值。例如:

// 该函数有一个参数,没有返回值
func log(message string){
	...CODE...
}

// 该函数有两个参数,一个返回值,返回值的类型为int
func add(a int,b int) int {
	...CODE...
}

// 该函数一个参数,两个返回值,分别是int和bool
func power(name string) (int,bool) {
	...CODE...
}

既然函数可以有返回值,就可以将其返回值赋值给变量:

value, exists := power("malongshuai")
if exists == false {
	...CODE...
}

有些时候,我们可能并不需要所有的返回值。例如,我们只想要取得 power() 的第二个返回值。这时可以将不想要的返回值丢给特殊符号下划线_,它表示丢弃这部分结果。

_,exists := power("longshuai")
if exists == false {
	...CODE...
}

如果函数的参数类型相同,则不同的参数可以共享数据类型。

func (a,b int) int {
	...CODE...
}