Go学习笔记(一)

这周学了下Go,这里记录了一下学习的笔记,主要来自官方Go指南 《Go 语言之旅》

每个 Go 程序都是由包构成的。

程序从 main 包开始运行。

本程序通过导入路径 "fmt" 和 "math/rand" 来使用这两个包。

按照约定,包名与导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	fmt.Println("My favorite number is", rand.Intn(10))
}

这是官方教程的第一段,可以发现 Go 和 C 语言不同,不是通过一个个文件的方式来管理代码,而是通过

C 语言是把代码放在 .c 文件中,然后通过编写对应的 .h 文件,最后 #include 需要的 .h 文件

Go 则是把代码放在 .go 文件里,通过 package 指定一个包名,最后直接 import 这个包

由于 Go 的程序是从 main 这个包开始的,而一个程序只能有一个 main 函数,所以 main 函数就必须在 main 这个包里

导入和导出

导入很简单,可以向上面一样,将所有需要导入的包,写在一个 import 里,这是一种分组的形式

另一种就是和 C 语言一样,一个包一个 import

import "fmt"
import "math"

官方是推荐采用分组导入语句

在 Go 里面导出更加简单,不需要主动导出,如果一个名字以大写字母开头,那么它就是已导出的。

也就是包中首字母大写的函数、变量、常量之类的都可以在其他包中访问到,反之就无法访问

类型声明

Go 的类型声明和 C不同,是类型写在后面,变量名写在前面,类似于 python 、 typescript 的类型注解的方式,参考 这篇关于 Go 语法声明的文章可以了解这种类型声明形式出现的原因

简单来说就是 Go 的开发者觉得 C 在复杂的函数声明时,比如下面这种会难以分辨,不知道该把参数名往哪个位置加,也不知道 fp 是个什么东西

int (*(*fp)(int (*)(int, int), int))(int, int)

而 Go 采用的是一种从左往右的说明的方式

x: int
p: pointer to int
a: array[3] of int
x int
p *int
a [3]int

这样可以保证始终是最左边是参数名,而类型在参数名的右边

f func(func(int,int) int, int) func(int, int) int

函数

go 里面函数声明的方式就是通过 func 关键字,参数都是统一的左边变量名,右边类型,最后函数的返回值的类型同样是在最右边

func add(x int, y int) int {
	return x + y
}

连续两个或多个相同的已命名形参可以省略前面几个的类型,只留下最后一个的类型

func add(x, y int) int {
	return x + y
}

在 Go 中 函数可以返回任意数量的返回值,通过,连接

package main

import "fmt"

func swap(x, y string) (string, string) {
	return y, x
}

func main() {
	a, b := swap("hello", "world")
	fmt.Println(a, b)
}

对于已经命名的函数返回值,可以通过直接返回的方式

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

没有参数的 return 语句会返回已命名的返回值,这就是直接返回

变量

Go 中变量声明的方式类似 JavaScript,是通过 var 关键字

var c, python, java bool

同样的,我们也可在声明的同时初始化,不过每个变量必须都对应一个,也就是要么都初始化要么都不初始化

如果初始化的值存在,可以省略类型,变量会从初始值中推导出类型(类似于 C11 的 auto 关键字的作用)

package main

import "fmt"

var i, j int = 1, 2

func main() {
	var c, python, java = true, false, "no!"
	fmt.Println(i, j, c, python, java)
}

还有一种偷懒的声明方式,叫短变量声明

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

package main

import "fmt"

func main() {
	var i, j int = 1, 2
	k := 3
	c, python, java := true, false, "no!"

	fmt.Println(i, j, k, c, python, java)
}

基本类型

Go 的基本类型有

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
// 表示一个 Unicode 码点

float32 float64

complex64 complex128

本例展示了几种类型的变量。 同导入语句一样,变量声明也可以“分组”成一个语法块。

int, uintuintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 当你需要一个整数值时应使用 int 类型,除非你有特殊的理由使用固定大小或无符号的整数类型。

package main

import (
	"fmt"
	"math/cmplx"
)

var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
	z      complex128 = cmplx.Sqrt(-5 + 12i)
)

func main() {
	fmt.Printf("Type: %T Value: %v
", ToBe, ToBe)
	fmt.Printf("Type: %T Value: %v
", MaxInt, MaxInt)
	fmt.Printf("Type: %T Value: %v
", z, z)
}

Type: bool Value: false
Type: uint64 Value: 18446744073709551615
Type: complex128 Value: (2+3i)

零值

没有明确初始值的变量声明会被赋予它们的 零值

零值是:

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)。

类型转换

表达式 T(v) 将值 v 转换为类型 T

一些关于数值的转换:

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

或者,更加简单的形式:

i := 42
f := float64(i)
u := uint(f)

与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换,否则会报错

常量

常量的声明与变量类似,只不过是使用 const 关键字。

常量可以是字符、字符串、布尔值或数值。

常量不能用 := 语法声明。

const Pi = 3.14

数值常量是高精度的

一个未指定类型的常量由上下文来决定其类型。

package main

import "fmt"

const (
	// 将 1 左移 100 位来创建一个非常大的数字
	// 即这个数的二进制是 1 后面跟着 100 个 0
	Big = 1 << 100
	// 再往右移 99 位,即 Small = 1 << 1,或者说 Small = 2
	Small = Big >> 99
)

func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
	return x * 0.1
}

func main() {
	fmt.Println(needInt(Small))
	fmt.Println(needFloat(Small))
	fmt.Println(needFloat(Big))
}

这里如果 Big 是个变量,就会报错,提示超出 int 范围,因为 Go 的类型推导里,没有小数点的整数常量就会推导为 int 类型,但常量的类型推导,不是初始化时进行的,而是根据后面使用时的上下文推导

上面的代码运行不会报错,运行结果如下

21
0.2
1.2676506002282295e+29

流程控制

for

Go 只有一种循环结构:for 循环。

基本的 for 循环由三部分组成,它们用分号隔开:

  • 初始化语句:在第一次迭代前执行
  • 条件表达式:在每次迭代前求值
  • 后置语句:在每次迭代的结尾执行

初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。

一旦条件表达式的布尔值为 false,循环迭代就会终止。

注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。

package main

import "fmt"

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}

初始化语句和后置语句是可选的。

package main

import "fmt"

func main() {
	sum := 1
	for ; sum < 1000; {
		sum += sum
	}
	fmt.Println(sum)
}

此时你可以去掉分号,因为 C 的 while 在 Go 中叫做 for

package main

import "fmt"

func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
	fmt.Println(sum)
}

再省略循环条件,就是无限循环了

package main

func main() {
	for {
	}
}

if

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

package main

import (
	"fmt"
	"math"
)

func sqrt(x float64) string {
	if x < 0 {
		return sqrt(-x) + "i"
	}
	return fmt.Sprint(math.Sqrt(x))
}

func main() {
	fmt.Println(sqrt(2), sqrt(-4))
}

if 的简短语句

同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。

该语句声明的变量作用域仅在 if 之内。

package main

import (
	"fmt"
	"math"
)

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	return lim
}

func main() {
	fmt.Println(
		pow(3, 2, 10),
		pow(3, 3, 20),
	)
}

if 和 else

if 的简短语句中声明的变量同样可以在任何对应的 else 块中使用。

(在 mainfmt.Println 调用开始前,两次对 pow 的调用均已执行并返回其各自的结果。)

package main

import (
	"fmt"
	"math"
)

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	} else {
		fmt.Printf("%g >= %g
", v, lim)
	}
	// 这里开始就不能使用 v 了
	return lim
}

func main() {
	fmt.Println(
		pow(3, 2, 10),
		pow(3, 3, 20),
	)
}

运行结果如下

27 >= 20
9 20

switch

switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。

Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.
", os)
	}
}

没有条件的 switch

没有条件的 switch 同 switch true 一样。

这种形式能将一长串 if-then-else 写得更加清晰。(所以超过两个的条件分支可以用这种方式来代替)

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

package main

import "fmt"

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}

运行结果如下,这个一般用在并发控制比较多

hello
world

defer 栈

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

package main

import "fmt"

func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

运行结果如下

counting
done
9
8
7
6
5
4
3
2
1
0

未完待续...