go 编程笔记一

开始学 go 编程语言, 老规矩,学习任何一门编程语言都需要把该语言的语法特性过一遍

这篇编程笔记主要讲的是基本语法,下一篇会分享一些 go 并发及高级特性

目录

开发环境安装

  • go 的有 3 种安装方式
  1. Go 源码安装
  2. Go 标准包安装
  3. 第三方工具安装

go 多版本控制可参看gvm,有点类似 node 中多版本控制的 nvm,ruby 里面的 rvm 工具

Go 目录约定

  • src 存放源代码
  • pkg 编译后生成的文件
  • bin 编译后生成的可执行文件

Go 标准入库文件

主函数必须包含 package mainmain 函数

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {

fmt.Println("hello world")
}

为了避免新增一个 GO 项目就要往 GOPATH 中增加一个路径,这里我们写一个脚本来运行和编译我们的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
if [! -f install ]; then
echo 'install must be run within its container folder' 1>&2
exit 1
fi

CURDIR=`pwd`
OLDGOPATH="$GOPATH"
export GOPATH="$CURDIR"

gofmt -w src

## 安装 pac 下面的包
go install pac

export GOPATH="$OLDGOPATH"


echo 'finished'

变量

  • 变量的声明必须使用,在 go 语言中声明的变量不使用, 会报错,我们可以使用 idea 去帮我们优化变量和包的引用

  • 全局变量

1
2
var name = "feel"
var name string = "feel"
  • 在函数内部, 甚至可以省略 var 关键字, “:=” 这种更短的表达式完成变量类型推断和初始化
1
2
a := 1    // 简易声明局部变量
println(a)
  • 常量的声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const PI = 3.14

const a, b, c = "meat", 2, "veg" // 同样 持 次定义多个常量。

const (
z = false
a = 123
)



const (// 定义常用是, 如果不初始化变量,默认是和上一行的类型,值表达式相同
a = "abc"
b
)
  • 枚举

可以 iota 成从 0 开始的 动增 枚举值。按 递增, 可以省略后续 的 iota 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)

const (
B float64 = 1 << (iota * 10) // 后续 的类型和值表达式与 KB 相同, 只有 iota 值发 变化
KB
MB
GB
)

const (
A, B = iota, iota // 可以在同一行使用多个 iota, 它们各 增
C, D
E, F
)
  • 多变量的声明
1
2
3
4
5
6
7
8
9
10
var x, y int    // 类型相同的多个变量

var (// 类型不同的多个变量
a int
b bool
)

var c, d int = 1, 2 // 指定类型, 多个变量类型相同。
var e, f = 123, "hello" // 自动推断, 多个变量类型按初始值推断
g, h := 123, "hello" // 自动推断, 多个变量类型按初始值推断。
  • 只写变量,不可读变量

    在 python 和 scala 中都有类似的变量, 我们称为垃圾变量

1
2
age, _ := 10, "feel"
println(age)
  • 类型转换

    在 go 语言中不支持隐式转换, 都必须显示的类型转换

1
2
b := 123.34
println(uint(b))

字符串

go 语言中 字符串是不可以修改的。

  • 字符串的转换

要修改字符串,需要显示的转换[]byte 或者 []rune

1
2
3
4
5
6
7
var name string = "feel"
bs := []byte(name) // 转换成 bytes
bsi := []rune(name) // 转换成 Unicode
println(bs)
println(bsi)

println(string(bs)) // byte 转换为 string
  • 字符串的连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

s := "abc" + ", 123"
println(s)

// 使用 strings.Join 工具连接

s1, s2 := "feel", "best"
println(strings.Join([]string{s1, s2}, "-"))

// 类似 StringBuilder
sb := bytes.Buffer{}
sb.WriteString("hello")
sb.WriteString("feel")
sb.WriteString("best")

println(sb.String())

控制结构 (if for switch)

  • if
    • 条件表达式没有括号;
    • 持 1 个初始化表达式 (可以是多变量初始化语句);
    • 左括号必须和条件语句在同
1
2
3
4
5
6
7
8
9
10
11
12
13
a := 10
if a > 0 {
// 左括号必须写在这, 否则被解释为 "if a > 0;" 导致编译出错。
a += 100
} else if a == 0 {
// 注意左括号位置。
a = 0
} else {
// 注意左括号位置。
a -= 100
}

println(a)
  • 迭代器 for 循环和 range 操作

string、array、slice、map、channel 都可以 range 进 迭代器操作。
如果不想要序号, 可以使用 _ 这个特殊变量, 或者只要序号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for i, c := range "abc" {
fmt.Printf("s[%d] = %c\n", i, c)
}

// 类似其他语言中的 while 循环

func main() {
i := 0
for i < 10 { // 也用 for
fmt.Print(i, "")
i++
}
fmt.Println()
}
  • switch 表达式的使用

switch case 表达式可以使 任意类型或表达式。也不必写 break, 动终 。如希望继续下
case 处理, 则须显式执 fallthrough。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

switch a := 5; a {
case 0, 1: println("a") // 逗号指定多个分
case 100: // 什么都不做。不是 fallthrough
case 5: // 多 代码, 需使 {}。
println("b")
fallthrough // 进 后续 case 处理
default:
println("c")
}


a := 5
switch {
case a > 1: println("a")
case a > 2: println("b")
default: println("c")
}

type 定义的新类型

type 定义的新类型 (基于其他类型) 并不是 个别名, 是 个全新的类型。
如果底层类型是命名类型, 那么必须进 显式转换, 才能将新类型对象转换为底层类型。

1
2
3
4
5
6
7
8
9
10
11
12

type MyInt int
type MyIntSlice []int
var a MyInt = 10
// var b int = a // cannot use a (type MyInt) as type int in assignment
var b int = int(a)
// var c MyInt = b // cannot use b (type int) as type MyInt in assignment
var c MyInt = MyInt(b)
println(a, b, c)
var e MyIntSlice = []int{1, 2, 3}
var f []int = e
println(e, f)

Array,Slices 和 Maps

  • Array 数组是值类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
   a := [10]int{1, 2, 3, 4 }  // 未提供初始化值的元素为默认值 0
b := [...]int{1, 2 } // 由初始化列表决定数组 度, 不能省略 "...", 否则就成 slice 了。
c := [10]int{2:1, 5:100} // 按序号初始化元素 , 角标为 2 初始值为 1, 脚本为 5 初始值为 100.

fmt.Println(a)
fmt.Println(b)
fmt.Println(c)


array := [3] string{"a", "b", "c"}
// 数组切片, 初始化长度为 3 , 5 的预留空间
array := make([]int, 3, 5)
array[0] = 1
array[1] = 2
array[2] = 3

//// 动态添加数组
array = append(array, 4, 5, 6, 7, 8)
//
for i, v := range array {
fmt.Println("Array element[", i, "]=", v)

}
//// 查看数组空间 10
fmt.Println(cap(array))
  • Slices
1
2
3
4
5
6
7
8
9

x := [...]int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }

vars1[]int=x[1:3]
s2 := x[4:]

s3 := x[:6]

s4 := x[:]
  • reslice

类似于重新切割

1
2
3
4
5
6
7
a := [...]int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }

s1:=a[5:7] //[56],len=2,cap=5

s1=s1[0:4] //[5,6,7,8],len=4,cap=5
// 注意 reslice 不能超过 cap 的限制。
// 是在 slice 上重新切分, 不是 Array, 因此序号是以 slice 为准。
  • map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

// map 定义的申明
var myMap map[string]int
// map 创建
myMap = make(map[string]int)
// map 赋值
myMap["a"] = 1
myMap["b"] = 2
var d = map[string]int{ "a":1, "b":2 };
d2 := map[int]string{ 1:"a", 2:"b" };
// map 遍历
for i, v := range myMap {
fmt.Println("Array element[", i, "]=", v)
}

// 查找元素
value, ok := myMap["a"]
if ok {
fmt.Println("exixt value", value)
}
// map 的 删除和更新

d := map[string]int { "b":2, "c":3, "e":5 }
for k, v := range d {
println(k, v)
if k == "b" { delete(d, k) }
if k == "c" { d["a"] = 1 }
}

内置函数

  • close: 关闭 channel。
  • len: 获取 string、array、slice 长度,map key 的数量, 以及 buffer,channel 中当前可变数
    据数量。
  • cap: 获取 array 长度,slice 容量, 以及 buffer,channel 的最大缓冲容量。
  • new: 通常用于值类型, 为指定类型分配初始化过的内存空间, 返回指针。
  • make: 仅用于 slice、map、channel 引 类型, 除了初始化内存, 还负责设置相关属性。
  • append: 向 slice 追加 (在其尾部添加)1 个或多个元素。
  • copy: 在不同 slice 间复制数据。
  • print/println: 不支持 format, 要格式化输出, 须使使用 fmt 包。
  • complex/real/imag: 复数处理。
  • panic/recover: 错误处理。

函数

Go 函数不支持嵌套 (nested)、重载 (overload) 和默认参数 (default parameter), 但支持以下特性:

  • 需声明原型;
  • 不定变参;
  • 多返回值;
  • 命名返回值参数;
  • 匿名函数;
  • 闭包;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

func myprint(v interface{}) { //go 语言中 interface{} 可以代表任何类型,可以当成 java 中的 Object 类型
fmt.Println(v)
}


// 单个参数
func example(x int) int {
if x == 0 {
return 5
} else {
return x
}
}

// 不定参数
func add(args... int) int {
sum := 0
for _, v := range args {
sum += v
}
return sum
}

// 接收多个参数, 返回值。
func test(a, b int, c string) {
println(a, b, c)
}


type callback func(s string) // 定义函数类型
func test(a, b int, sum func(int, int) int){ // 接收函数类型参数, 也可以直接 callback 类型。

println(sum(a, b))
}

// 多个返回值
func swap(a, b int) (int, int) {
return b, a
}


// 命名返回参数
func change(a, b int) (x, y int) {
x = a + 100
y = b + 100
return // 也可以写成 return x, y

}


a, b := change(1, 2)



// 闭包

func closures(x int) (func(int) int) {
// 返回匿名函数
return func(y int) int {
return x + y
}
}
  • Defer

常 来做资源清理、关闭 件、解锁、记录执 时间等等操作,就算函数发 严重错误,defer 依然会被执 。
defer 语句会延迟函数的执行直到上层函数返回。

延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

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

fmt.Println("hello")
}

以上代码输出

1
2
hello
world
  • 延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

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

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

fmt.Println("done")
}
1
2
3
4
5
6
7
8
9
10
11
12
func test(a, b int) int {
defer println("defer1:", a, "/", b)
defer func() {
println("defer2:", a, b)
}()
return a / b
}
func main() {
a := test(10, 2)
b := test(10, 0)
print(a, b)
}

Structs

  • Go 没有 class, struct 来实现面向对象编程模型。
1
2
3
4
5
6
7
8
9
10
11
type User struct {
Id int
Name string
}

func main() {
user1 := User{ 1, "Tom" }
user2 := User{ Name:"Jack" }
println(user1.Id, user1.Name)
println(user2.Id, user2.Name)
}
  • 方法

方法接收者 出现在 func 关键字和方法名之间的参数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := &Vertex{3, 4}
fmt.Println(v.Abs())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
)

type MyInt int

func (this MyInt) getVal() int {
return int(this)
}

func main() {
var myInt MyInt = MyInt(1)
fmt.Println(myInt.getVal())
}

方法内部变量的改变,
如果需要改变 struct 内部的值,就需要使用指针,而不使用指针的话,则相当于完全拷贝了一份,相比指针,一是不能改变变量的值,二是浪费储存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

package main

import (
"fmt"
"strconv"
)

type Person struct {
Name string
Age int64
}

func (this Person) String() string { // 类似 java 的 toString 方法
ageStr := strconv.FormatInt(this.Age, 10) // 把int 转换为 string 类型,第二个参数代表按照 10 进制转换
return "名字:" + this.Name + "年龄:" + ageStr
}

func (this Person) ChangeName1(name string) {
this.Name = name
}

func (this *Person) ChangeName2(name string) {
this.Name = name
}

func main() {
p := Person{Name: "nladuo", Age: 20}
fmt.Println(p)
p.ChangeName1("Kalen Blue")
fmt.Println(p) // 没有改变 name
p.ChangeName2("Kalen Blue")
fmt.Println(p) //name 发生改变
}

receiver 在多数语 中是隐式传递的 this, 在 Python 中通常写作 self。

1
2
3
4
5
6
7
8
type User struct {
Id int
Name string
}

func (this *User)test() {
println(this.Id, this.Name)
}
  • 字段标签
1
2
3
4
5

type User struct {
Name string "姓名"
Age int "年龄"
}

接口

接 定义看上去和 struct 类似, 区别在于:

  • 只有方法签名, 没有实现。
  • 没有数据字段。

    习惯性地将接 命名以 er 结尾, 不是以往习惯性地以大写字"I" 开头。

1
2
3
4

type Tester interface {
Test()
}

  • 接口的实现

    和其他语言有些不一样,go 语言中接口的实现,只要方法同名即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

type Users struct {
Id int
Name string
}

type Myiner interface {
show()
}

func (this Users) show() {

fmt.Printf("[Id]=%d,[Name]=%s", this.Id, this.Name)
}

// 推断并检查是否实现了 Myiner 接口,不会引发错误
func doSomething( i interface{}) {

if o,ok:=i.(Myiner); ok {
o.show()
}
}

func main() {

u := Users{10, "feel"}

Myiner(u).show()

doSomething(u)
// end main
}

  • 在 Go 中, 首字母大写的名称是被导出的。
  • 在导入包之后,你只能访问包所导出的名字,任何未导出的名字是不能被包外的代码访问的。
  • go 语言如果让外部引入包的话,函数名称开头必须大写,如果开头小写的函数只能在当前包内部引用。
  • 包引入后,必须使用,如果不使用的话会报错。

包的导入

  • 在 Go 语言中,我们可以使用 go get 命令安装远程仓库中托管的代码,不同于 Ruby Gem、pypi 等集中式的包管理机制, Go 语言的包管理系统是去中心化的。简单来讲,go get 命令支持任何一个位置托管的 Git 或 Mercurial 的仓库,无论是 Github 还是 Google Code 上的包,都可以通过这个命令安装。
    我们知道,在 Go 语言中的 import 语句对于已经使用 go get 安装到本地的包,依然要使用其去绝对路径引入。 比如对于从 Github 上安装的 goji,其在 Github 上的路径 URL 是 https://github.com/zenazn/
    goji,因此在 import 它的时候需要使用下面的代码:

  • 包的安装

可以使用 go get 来下载一些第三方库,下载来都会保存到 $GOPATH 下面

1
2
3
go get -u -v github.com/kardianos/govendor
go get -u -v github.com/tools/godep
go get -u -v github.com/astaxie/beedb
1
import "github.com/zenazn/goji"

包重命名

在使用多个包函数名一样的时候,会报错,这时候我们可以是包别名来解决此问题。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
a "github.com/pkgtest/pkg1"
b "github.com/pkgtest/pkg2"
)

func main() {
a.Foo()
b.Foo()
}