Golang 学习笔记 (1)

把 Hexo 换成了基于 Go 的 Hugo,所以有必要学一下 Golang 了(

之前也一直在想学个新的语言,一直在纠结选什么好,刚巧就是你了。

Table of Content

0x01 包

Go 程序类似 Java,由包构成,并且程序总是从 main 包开始运行。包名与导入路径的最后一个元素相同。

package main    // 声明当前文件所属的包,不需要引号
import "fmt"    // 导入包,需要引号
import (
    "os"
    "math/rand"
)   // 这样也可以

导入却未被使用的包编译器会报错。

Go 中如果一个函数/方法/变量/常量名字以大写字母开头,表示它已经被导出。如:

package main
import "fmt"
import "math"
func main() {
    fmt.Println(math.Pi)    // 3.141592653589793
    // fmt.Println(math.pi)  unexported, return undefined
}

再如上述代码中,Printlnfmt 包导出的一个方法。

0x02 函数

定义函数格式如下:

func [functionName] ([param1 paramType], [param2 paramType]...) [returnValType] {
    // ...
    return // ...
}

与 C/C++/Java 的区别在于变量的类型在变量名之后而不是之前。另外,若有多个参数的类型相同,则可以简写:

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

func dec(x, y int) int {
    return x - y
}

0x03 返回值

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)   // world hello
}

还有“返回值命名”这种操作。直接在声明返回值的时候写下返回值的变量名:

func split(sum int) (x, y int) {
    x = sum * 4 / 9 // 不需要再定义了
    y = sum - x
    return          // 直接返回 x,y
}

这样可以一定程度上省掉注释写文档(

:= 运算符相当于定义变量后直接赋值:

var a int
a = 233
// just the same as..
b := 233

0x04 变量

上文提到用 var [variableName1], [variableName2] [variableType] 声明一个变量.若想在声明变量时初始化值,可以这样:

var a bool = false
var x, y int = 1, 2
t := "naive"            // 如果有初始值,Go 可以进行类型推断

fmt.Println(a, x, y, t)

声明完不用的变量同样会使编译器报错。需要注意 := 不能在函数作用于外部使用。如:

package main
import "fmt"
a := 233        // do not declare a variable like this
func main() {
    // ...
}

0x05 变量类型

Go 的基本类型……

bool    // 布尔

string  // 字符串

// 整型,其中 int, uint, uintptr 的位宽由操作系统位数决定
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr // unsigned

byte // uint8 的别名

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

float32 float64 // 浮点,Go 没有 double

complex64 complex128    // 复数
package main

import (
    "fmt"
    "math/cmplx"
)

// 定义多个变量也可以因式分解关键字
var (
    maxInt uint64 = 1 << 64 - 1 // 2^64-1
    z      complex128 = cmplx.Sqrt(-5 + 12i)
)

func main() {
    fmt.Println("Type: %t Value: %v\n", maxInt, maxInt)
    fmt.Println("Type: %t Value: %v\n", z, z)
}

Println中的占位符:%t表示变量类型,%v表示变量值. 当然你可以用fmt.Printf()` 然后写那些格式占位符。

如果没有对已声明的变量赋值,那么他们的默认值为 0/false/空字符串。

0x06 类型转换

可以用 var a type = type(b) 格式。

var i int = 233
var f float64 = float64(i)
u := uint(f)

与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换。

0x07 常量

声明常量在其前面加上关键字 const, 注意常量不能用 := 语法声明:

const a = 2333
const Pi float64 = 3.14
const World = "世界"
const fake bool = false

0x08 循环

Go 只有 for 循环的结构……虽然 for 可以替代 while 但是有时候还是写 while 比较方便的呢。与 C/C++/JS 等语言的区别在于省略了把初始化语句、条件表达式、后置语句括起来的括号,但是仍然保留大括号。

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

如果去掉初始化和后置语句就变成类似 while 了:

for ; condition; {

}

其实……C 的 while 就是 Go 中的 for( 所以当初始化语句和后置语句没有的时候你甚至可以省略分号,这样 for 就成了 while 了:

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

省略循环条件下的无限循环:

for {
    // do something infinity
}

循环的控制语句和其他语言类似,使用 break 语句退出循环结构,使用 continue 语句继续下一个循环。

0x09 判断

if else 结构

Go 的 if 语句与 for 类似,条件表达式外不需要小括号,而逻辑部分需要大括号。

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

区别于其他语言,Go 允许你在执行 if 的判断前进行初始化,和 for 的初始化语句类似

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	return lim  // 注意,初始化语句的作用域仅限于大括号内,这里如果访问 v 就是 undefined
}

switch 结构

与其他语言类似并继承Go 特色,条件部分不需要小括号,可以有初始化语句。但还有一点与其它语言的不同在于 Go 的 switch 中 case 结束后不需要 break,Go 会自动帮你添加而不会运行选定 case 之后的所有 case(除非你指定了 fallthrough).

Go 的 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)
	}
}

0x10 defer

defer 语句将函数推迟到外层函数返回后进行,如:

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

程序先输出 hello, 等待 main 函数执行完返回后输出 world. 注意 defer 推迟的函数是压入栈(后进先出)中的,也就是说:

func main() {
	defer fmt.Println("world")
	defer fmt.Println("happy")
	fmt.Println("hello")
}

以上程序的输出顺序是 hello happy world 而不是 hello world happy.

0x11 指针

定义一个指向 int 类型变量的指针:var p *int,定义方法和 C/C++ 类似。

同样地,&* 运算符的效果也是相同的。但是 Go 里没有指针运算(啥玩意啊.jpg)

& 操作符会生成一个指向其操作数的指针。

i := 233
p = &i
  • 操作符表示指针指向的底层值。

    fmt.Println(*p) // 通过指针 p 读取 i
    *p = 666        // 通过指针 p 设置 i
    fmt.Println(i)  // 666
    

0x12 结构体

结构体定义格式如下:

type StructName struct {
    member1 type1
    member2 type2
    // ...
}

定义结构体及其类型的变量示意:

package main
import "fmt"
type Vertex struct {
	X int
	Y int
}
func main() {
	t := Vertex{2, 3}
	fmt.Println(Vertex{1, 2})
	fmt.Println(t)
}

与 C/C++ 类似,结构体成员用 . 运算符访问;当拥有一个结构体指正的时候,那么……还是可以用 . 运算符访问。当然。你想用 (*ptr).member 访问也可以呀,只是 Go 允许我们使用隐式间接引用:

func main() {
	v := Vertex{1, 2}
	p := &v
	v.X = 1e8
	fmt.Println(v)
	p.X = 1e9
	fmt.Println(v)
}

上文的赋值法,默认是第一个值赋给 X,第二个赋给 Y;对结构体的具体字段赋值则类似 JS 中的语法:

var (
	v1 = Vertex{1, 2}  // has type Vertex
	v2 = Vertex{X: 1}  // Y:0 is implicit
	v3 = Vertex{}      // X:0 and Y:0
	p  = &Vertex{1, 2} // has type *Vertex
)

特殊的前缀 & 返回一个指向结构体的指针。

0x13 数组

定义一个数组的格式:var arrayName [arrayLength]typeName,注意是否空格。

如:var a [10]int,a 是一个长度为 10 的 int 型数组。这样定义的数组是静态的,也就是说你定义完之后,a 的长度只能是 10 不能再改了。

0x14 切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。类型 []T 表示一个元素类型为 T 的切片。

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:a[low : high]

它会选择一个半开区间,包括第一个元素,但排除最后一个元素:[low, high)。如切片 a[1:4],它包含 a 中下标从 1 到 3 的元素。

package main
import "fmt"
func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}
	var s []int = primes[1:4]
	fmt.Println(s)
}

切片并不存储任何数据,只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改(可以理解为切片是对一个数组的部分引用):

package main

import "fmt"

func main() {
	names := [4]string{
		"John",
		"Paul",
		"George",
		"Ringo",
	}
	fmt.Println(names)

	a := names[0:2]
	b := names[1:3]
	fmt.Println(a, b)

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(names)
}

切片的默认下界为 0,上界为切片/数组的长度。

切片拥有 长度 和 容量 两个属性。切片的长度就是它实际包含的元素个数;切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数(最多可以容纳的元素个数)。

切片 s 的长度和容量可通过表达式 len(s)cap(s) 来获取。

package main
import "fmt"
func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	printSlice(s)

	// Slice the slice to give it zero length.
	s = s[:0]
	printSlice(s)

	// Extend its length.
	s = s[:4]
	printSlice(s)

	// Drop its first two values.
	s = s[2:]
	printSlice(s)
}
func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

输出:

len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]

切片的零值是 nil,类似其他语言的 nullnil 切片的长度和容量为 0 且没有底层数组。

var a [10]int       // 声明的是数组
var b []int         // 声明的是一个 nil 切片

0x15 make

内建函数 make() 可以创建切片,也是创造动态数组的方式。make 函数会分配一个元素为零值的数组并返回一个引用了它的切片,格式如下:

var name = make([]type, length)     // len(name) = length

// e.g.
a := make([]int, 5)     // len(a) = 5

若要指定该切片的容量,则需要向 make() 传入多余的参数:

name := make([]type, from, to)      // len(name) = 0, cap(name) = to

如:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5  emmmm
b = b[1:]      // len(b)=4, cap(b)=4

切片套切片就成了二位切(shu)片(zu);切片可以包含任意类型。

既然是动态数(qie)组(pian)就要可以动态修改数据嘛,比如向切片里添加新元素。Go 内建的 append(s []T, vs ... T) []T 函数就可以做到。

从函数原型中我们知道,append() 函数第一个参数是一个任意类型 T 的切片 s(如果这里理解 C++ 的 template 就更容易理解了),接下来的几个参数分别是要加入切片 sT 型数据,最后返回一个新的 T 型切片。新添加的值会出现在切片末尾。

当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。参考

0x16 range 遍历

对标 foreach,在 Go 中的遍历仍然用的是 for,但是有一个新的辅助关键字 range:当使用 for 循环遍历切片或映射的时候格式如下:

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
	for index, value := range pow {
		fmt.Printf("2**%d = %d\n", index, value)
	}
}

注意 range 循环时每次迭代会返回两个值 indexvalue,第一个值是在切片中的下表,第二个值则是真正的值(但是是一个副本,而不是引用)。

如果我们只需要值不需要下标?你会说多写一个不会死,但是 Go 是不允许无用变量出现的;将 index_ 代替即可:

for _, value := range pow {
    // ...
}

如果不需要值只要下标,去掉 , value 即可:

for index := range pow {
    // ...
}

0x17 映射

数组只能以数字做下标,有时候我想以一个字符串啥的做下表怎么办?可以用 map 映射,这个就类似 C++ STL 的 map 的应用,只不过方法不同而已(

创建一个映射并赋值(注意空格)

var mapName map[keyType]valueType

// e.g. -----------

var m map[string]int
func main() {
	m = make(map[string]int)
	m["abc"] = 123
	fmt.Println(m["abc"])
}

对 map 直接赋值:

package main
import "fmt"
type Vertex struct {
	Lat, Long float64
}
var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}
func main() {
	fmt.Println(m)
}

上面对 m 的赋值中,可以对成员省略类型名:

var m = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google": {37.42202, -122.08408},
}

注意到,数组和映射的最后一个成员末尾要留一个逗号,这是 Go 的规范。

一些对映射的基本操作:

m[key] = element	// 在映射 m 中插入元素
elem := m[key]		// 获取元素
delete(mapInstance, key)	// 从 mapInstance 中删除键值为 key 的元素
elem, ok := m[key]	// 检测元素是否存在,若存在则 ok 为 true(还有这种操作??)

0x18 函数值和函数的闭包

函数本身是一种数据类型。函数可以是一个闭包,引用其函数体外的变量。

package main
import "fmt"
func adder() func(int) int {
	sum := 0
	// 返回的是一个函数闭包
	return func(x int) int {
		sum += x		// 用了外部变量 sum
		return sum
	}
}
func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}