包包的引入go是面向过程的语言, go的代码不讲究封装和整理,很多源码都是几千行代码呆在单个文件里面,表现出了典型的过程式语言的基本特征. c也是过程式的,go的语言特性跟c语言很相似,只是增加了gc,goroitine等常用工具而已,作者本身就对oop,fp等缺乏了解,只是根据c的经验,整理出了一些常用的工具,然后将其放到语言的runtime中去罢了.
另外go官网对于是否面询对象编程语言,他们自己说是:Yes and No。明显go是允许OO的编程风格的,但又缺乏一些常见类型继承结构和类对象。Go自己觉得这一套挺好的,更加的容易使用且通用性更强。很多时候我们用OO的思想来组织我们的项目,但需要注意的是继承关系是一种非常强的耦合,有时候会给以后的升级带来麻烦。大型项目中可能是非常有帮助的,当然小型的项目可能好处不是很明显。
接下来正式学习Go语言
- 和其他语言的单个文件就是一个包的组织方式不同,go的package不局限于一个文件,多个文件可以声明同一个名称的包,互相间可以直接调用,不用import,同一个package内全局变量和函数等不能同名。
- go不要求package的名称和所在目录名相同,但建议最好保持相同。引入包的时候,go会使用子目录名作为包的路径,例如:import “root_dir/sub_dir/package”,
- import导入包,调用其属性或方法时,其属性和方法都是大写字母开头的,写自己的方法和属性时也要遵循这个规则.
- 一个文件夹下只能有一个package!!!
package mainimport (// Python是要加逗号隔开,而且没有引号"fmt""math"// 子包,Python是from...import..."math/rand")func main() {fmt.Println(math.Pi) // Python建议小写字母开头fmt.Println("My favorite number is", rand.Intn(10))}
int, string, bool, float32 // Python是int, str, bool, floatuint, int64, float64byte // uint8 的别名
声明变量
var i intvar i, j int = 1, 2 // 同类型变量可以在一起声明, 且一次赋值多个变量var c, python, java = true, false, "no!" // 可以省略类型,变量从初始值中获得类型i := 1 // 不用加var关键词的声明,compiler会自动识别, 这和Python差不多c, python, java := true, false, "no!”// Python也可以指定数据类型, i: int = 0, j: int = i
声明常量
const Pi int = 3.14 // 常量不能使用 := 语法定义,必须指定数据类型。是Python的全局变量吗?
默认空值
int: 0string: ""bool: false // Python是Falsefloat32: 0.0
类型转换
var i int = 42var f float64 = float64(i)var u uint = uint(f)
- string和int类型相互转换
- string转成int:
- int, err := strconv.Atoi(string)
- string转成int64:
- int64, err := strconv.ParseInt(string, 10, 64)
- int转成string:
- string := strconv.Itoa(int)
int64转成string:
- string := strconv.FormatInt(int64,10)
- string := strconv.FormatInt(int64,10)
- 查看变量数据类型
fmt.Printf(`%T`, num)或fmt.Println(reflect.TypeOf(num))
- && 逻辑 与 AND
- || 逻辑 或 OR
- ! 逻辑 非 NOT
if控制语句
i := 2if i < 3 {j := 3}
if
语句可以在条件之前执行一个简单的语句, 用分号;分隔if i := 2; i < 3 {j := 3}// 这比Python便捷
// if else语句
if i := 2; i < 3 { ����,����j := 3} else { j := i}// else if 使用switch实现
- switch, Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch。
1) 一分支多值
当出现多个 case 要放在一起的时候,可以写成下面这样://一分支,多值switch str {case "hello", "nihao": fmt.Printf("一分支,多值:%s \n", str)default: fmt.Println("hi")}不同的 case 表达式使用逗号分隔。
- 2) 分支表达式
case 后不仅仅只是常量,还可以和 if 一样添加表达式,代码如下://分支表达式var num = 7switch {case num > 1 && num < 5: fmt.Println("小于5的数")case num > 5 && num < 10: fmt.Println("大于5,小于10的数")}
代码输出如下:
大于5,小于10的数
- 使用switch实现if, else if, else
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.")
}
}
## for循环
// 感觉比Python的要统一些, 新语言比较大胆.
func main() {
// 实现0一直+到9,Σ(0-9)
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
// 类似while语句,累积小于1000的sum := 1for sum < 1000 { sum += sum}// while true 省略了循环条件,就成了死循环for {}
}
# 数据结构## 数组 array- 定义变量 a 是有2个字符串的数组
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
}
- 遍历数组
func main() {
var p = []int{2, 3, 5, 7, 11, 13}
fmt.Println("p ==", p)
// 遍历, 迭代
for i := 0; i < len(p); i++ {
fmt.Printf("p[%d] == %d\n", i, p[i])
}
}
## 序列 slice- 定义slice, 类似单数据类型的Python列表
var z []int // slice 的零值是 nil
s := []{1, 2, 3}
- 对 slice 切片
s[:2], s[0:0], s[1:2] // s[lo:hi] 表示从lo到hi-1的元素,不包含第hi个元素
- 示例
func main() {
p := []int{2, 3, 5, 7, 11, 13}
fmt.Println("p ==", p)
fmt.Println("p[1:4] ==", p[1:4])
// 省略起始index代表从 0 开始
fmt.Println("p[:3] ==", p[:3])
// 省略结束index代表到 len(s) 结束
fmt.Println("p[4:] ==", p[4:])
}
- range语句遍历序列,可以用for循环 配合 range语句对 slice 进行迭代循环。
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v) // 2的次方, i表示索引 0开始, v表示值
}
}
- make 构造slice
slice 由函数 make 创建。这会分配一个零长度的数组并且返回一个 slice 指向这个数组,【按python的range函数理解】:
a := make([]int, 5) // len(a)=5, 长度为5, 容量为5的全0序列,[0 0 0 0 0]
b := make([]int, 0, 5) // len(b)=0, cap(b)=5, 长度为0,容量为5的空序列, 占空间较少,[]
b = b[:cap(b)] // len(b)=5, cap(b)=5, 展开所有容量,变成全0序列
b = b[1:] // len(b)=4, cap(b)=4
- 向 slice 添加元素
func main() {
var a []int
a = append(a, 1) // 尾部追加
a = append(0, a) // 头部追加
a = append(a, 2, 3, 4) // 可以添加多个
a = append(a, "a”) // 但不能是其他类型数据(报错)
}
- slice遍历range出来的是index和value,如果只需要索引,可以去掉“, value”的部分即可。仅需要value,则把i赋值给 _ 来忽略索引。
func main() {
pow := make([]int, 10)
for i := range pow {
pow[i] = 1 << uint(i)
}
for _, value := range pow {
fmt.Printf("%d\n", value)
}
}
- 多层序列嵌套
func main() {
dx, dy := 8, 4
sdy := make([][]uint8, dy)
for i := range sdy {
sdy[i] = make([]uint8, dx)
}
fmt.Println(sdy)
}
[[0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0]]
## 映射表 map- 定义map, 相当于只有一种数据类型的Python字典
mm := make(map[string]int) // make一个key是string类型,value是int类型的map
mm["Answer"] = 42 // 赋值
value := mm["Answer"] // 取值
v1, ok := mm["Answer"] // 取值并判断元素是否存在, true, 不存在则返回false
delete(mm, "Answer") // 删除元素,没有Python字典的pop方法
- 遍历map,range出来的是key和value,统计单词中字母个数
func main() {
s := "Hello World"
ss := strings.Split(s, "") // 分隔字符串
fmt.Println(ss)
wc := make(map[string]int)
for _, v := range ss {
wc[v]++
}
fmt.Println(wc)
}
[H e l l o W o r l d]
map[ :1 H:1 W:1 d:1 e:1 l:3 o:2 r:1]
# 高级数据类型## 结构体 struct- 定义struct
// 类似Python的class,指定了固定属性,使用时要“实例”
// 声明的方式也很像Python的类
type Vertex struct {
X, Y int // 有2个int型元素的结构体
// 也可以加入其它类型, Z string
}
- 赋值和取值
var (
v1 = Vertex{1, 2} // 类型为 Vertex, X:1 和 Y:2
v2 = Vertex{X: 1} // Y:0 被省略
v3 = Vertex{} // X:0 和 Y:0
)
func main() {
fmt.Println(v1, v2, v3)
v := Vertex{1, 2}
v.X = 4 // 点号来访问
fmt.Println(v.X)
}
- 重新分配(转换)
// 需要按照bson包,go get -u labix.org/v2/mgo/bson
type Task struct {
id int
name string
age int
sex int
}
type TaskForm struct {
name string
age int
}
// 转换
task_form := TaskForm{”name”: ”bb”, “age”: 17}
var _task Task
jsonstr, := bson.Marshal(task_form)
bson.Unmarshal(json_str, &_task)
fmt.Println(_task)
## 结构体 与 map- map装入结构体, 即一个key对应的value值是一个struct类型数据. 类似Python字典套class.- map 在使用之前须用 make 来创建;否则其值为 nil 的 map 是空的,并不能赋值。
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex // 声明变量 名称叫m的map机构类型
func main() {
m = make(map[string]Vertex) // 使用前要make下
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
}
- 也可以这样直接赋值, 跟结构体文法相似,不过必须有键名。
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)
}
- 也可以直接在文法的元素中省略结构名。
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
func main() {
fmt.Println(m)
}
# 指针 * &- 一元操作符 *[取值] &[取地址] ; - Go 具有指针。 指针保存了变量的内存地址。默认零值是 `nil`。- 个人理解其目的是节省变量传递时内存的消耗, 只传递一个地址(一串16进制值), 比传递整个变量(需要新开辟地址空间), 节省空间- 这也就是通常所说的“间接引用”或“非直接引用”。Python里没有指针- 定义指针
var p int
fmt.Println(p, &p) // 打印指针地址,此时p是没有值的,也就是还没有分配地址
p = new(int) // 创建一块内存,并把内存地址分配给 指针p,不能直接分配值 p = 12 会报错,因为没有地址来存这个值
// 或 p = &int
fmt.Println(p, &p)
fmt.Println(p)
// 输出:
指针p指向的地址 指针p的地址
<nil> 0xc0000ae018
0xc0000b4010 0xc0000ae018
0 // 分配内存后的指针默认值为 0
- 赋值,两种方式:改地址 和 改值
i := 42
p := &i // 第一种:指向新的地址
fmt.Println(p, p)
p = 21 // 第二种:修改成新的值, 地址不变
fmt.Println(*p, p)
// 输出:
42 0xc00001e080
21 0xc00001e080
// 理解:*p == i, p == &i
- 接下来指针会被用到更多的地方,例如传参和函数返回。# 过程式编程## 函数- 函数的入参,也可以多个同类型变量公用一个类型来声明
func add(x, y int) int {
return x + y
}
- 函数可以返回多个值
func swap(x, y string) (string, string) {
return y, x
}
a, b := swap("hello", "world") // a, b := b, a
- 函数也是值, 函数名可以当变量来传递给其他方法或函数
// 有点类似Python的内置函数
func main() {
hypot := func(x, y float64) float64 { // 相当于 func hypot(x, y float64) float64
return math.Sqrt(xx + yy)
}
fmt.Println(hypot(3, 4))
}
- 匿名函数
// 如果只调用一次,则不用给变量赋值,直接调用,并不给函数命名,称之为匿名函数
func main() {
i2 := func(x, y int) int { return x + y }(1,2)
fmt.Println(i2)
}
- 函数回调 - 函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调 。
func callback(a,b int) {
fmt.Print(a+b)
}
func add(c int, f func(int, int)) {
if c <= 4 {
f(c,c)
}
}
func main() {
add(4, callback)
}
// 输出:
8
- 闭包函数 - 一个函数中包含另一个函数,内部的函数被封闭在外部函数里,自成环境,则该外部函数称为闭包函数。 - 调用该闭包函数时,返回的是内部函数,由它来接受闭包外传入的变量。 传递变量的访问和赋值,都在内部环境完成。 - 有点类似Python的类,先“实例化”外部函数,再“调用”内部函数,传入的变量会作为“类属性”被记忆,不会因为多次调用而被初始化。
// 闭包函数
func adder() func(int) int {
sum := 0 // 闭包函数首次调用默认值,再次被调用时,不会初始化该值。
return func(x int) int {
// 内部函数环境
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder() // 新分配内存, 并指向adder函数, 新的变量引用互不干扰
for i := 0; i < 5; i++ {
fmt.Println(
// 执行闭包内部函数,由于没有给闭包函数传值,故sum初始值都是:0
pos(i),
neg(-2*i),
)
}
}
// 输出
0 0
1 -2
3 -6
6 -12
10 -20
## 方法- go语言中没有类class的声明方法, 不过, 仍然可以把一些同类函数定义到一个类似”类”对象的结构体下面, 也就是成为类中的”方法”.- 函数式编程, 要实现对象编程那套真的好别扭
type Vertex struct { // 类似结构体, 可定义多个方法
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}
func (v Vertex) Avg() float64 {
return (v.X + v.Y) / 2 // 方法内, 可像类一样, 调用自己的属性值
}
func main() {
v := Vertex{3, 4} // 类似对象的实例化
fmt.Println(v.Abs()) // 类似对象的方法调用
}
- 可以对包中的 任意 类型定义任意方法,而不仅仅是针对结构体。
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
- 方法可以与命名类型或命名类型的指针关联, 可以节省结构体”实例化”带来的内存开销.
type Vertex struct {
X, Y float64
}
func (v Vertex) Scale(f float64) {
v.X = v.X f
v.Y = v.Y f
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}
func main() {
v := &Vertex{3, 4}
v.Scale(5)
fmt.Println(v, v.Abs())
}
## 接口- 接口类型是(类似方法)由一组方法定义的集合, 方法实现了接口中的函数,则表示这些方法都该接口下。 - 有点绕,方法已经存在了,干嘛还要弄个接口呢?想调用的时候,直接struct.func不就好了,实际意义在哪? - 那如果一次执行多个struct的func时,且func逻辑差不多的,是选择单个执行,还是放入数组中遍历执行? - 如果放在数组,那这个数组的属性是哪个struct呢?所以就有了接口,来代表这些struct;好比一个Python列表中含有多个不同类实例(只是这些类的方法是必须含有相同的方法名)。 - 如果想传的方法又不想实现同一个函数,可以试试空接口:interface{},相当于所有方法和数据类型都实现了这个接口。
//接口定义
type Abser interface {
Abs() float64
}
- 实现不同形状的体积计算接口
// 接口
type Shape interface {
area() float64
}
// 方法
type Circle struct {
x,y,radius float64
}
type Rectangle struct {
width, height float64
}
// 实现了接口中的函数
func(circle Circle) area() float64 {
return math.Pi circle.radius circle.radius
}
func(rect Rectangle) area() float64 {
return rect.width * rect.height
}
// 所有形状体积和,传入接口数组
func getArea(shapes []Shape) float64 {
return shape.area()
}
func main() {
circle := Circle{x:0,y:0,radius:5}
rectangle := Rectangle {width:10, height:5}
fmt.Printf("all area: %f\n",getArea([]Shape{circle, rectangle}))
}
// 输出
all area: 128.539816
- 判断接口的具体数据类型
// 如果类型不匹配则返回false
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
var s interface{} = 56
assert(s)
var i interface{} = "Steven Paul"
assert(i)
}
程序将输出:
56 true
0 false
## 空接口 与 map- 后端接口常常返回的是多个数据类型组合一起的map(转成json)
data := make(map[string]interface{})
data[“key”] = “value”
data[“code”] = 0
# 错误处理- go中错误必须自行处理, 处于传参严谨态度, 实际避免调用方的误传导致系统崩溃的问题; 没有提供Python那样的try...except...可供试错的语法.- Go 程序使用 error 值来表示错误状态。`error` 类型是一个内建接口:
type error interface {
Error() string
}
- 通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 `nil`, 来进行错误处理。
i, err := strconv.Atoi("42”) // 可以忽略i,部分
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
}
fmt.Println("Converted integer:", i)
// error 为 nil 时表示成功;非 nil 的 error 表示错误。
- go语言没有try excepte等异常处理语法, 可以通过判断函数执行结果来处理error
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}
-函数通常返回错误作为最后一个返回值。 可使用errors.New来构造一个基本的错误消息,如下所示:
func Sqrt(value float64)(float64, error) {
if(value < 0){
return 0, errors.New("Math: negative number passed to Sqrt")
}
return math.Sqrt(value) // 默认返回最后一次出参: nil == return math.Sqrt(value), nil
}
// 使用返回值和错误消息,如下所示
result, err:= Sqrt(-1)
if err != nil { // 错误值不等于空, 即: 有错误 == if err {}
fmt.Println(err)
}
- 可以使用第三方包 github.com/pkg/errors 错误库# Go的并发## goroutine协程- goroutine 是由 Go 运行时环境管理的轻量级线程。
go f(x, y, z) // 声明
f(x, y, z) // 开启一个新的 goroutine 执行
- 定义和执行
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world") // 异步执行
say("hello”) // 同步执行
}
- 思考 - 有结果如何获取? 使用管道从线程中带出来(也就类似Python的Thread类中定义一个result方法取返回值)## channel暂存管道- channel 是有类型的管道, 可以理解为一个特殊的队列
ch <- v // 将 v 送入 channel ch, 在管道未流出前, 数据都是排队等待流出的(即阻塞)。
v := <-ch // 从 ch 接收,并且赋值给 v。
(“箭头”就是数据流的方向。)
- 和 map 与 slice 一样,channel 使用前必须创建: `ch := make(chan int)`- 默认情况下,在另一端准备好之前,发送和接收都会阻塞。可以和go线程配合使用进行数据同步, 不用单独执行.
func sum(a []int, c chan int) {
sum := 0
for _, v := range a {
sum += v
}
c <- sum // 将和送入 c 管道暂存起来 等待
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) // 主线程中定义管道 达到内存共享
go sum(a[:len(a)/2], c) // 后三个之和 先暂存到c管道
go sum(a[len(a)/2:], c) // 前三个之和 后暂存到c管道
x, y := <-c, <-c // 从 c 中获取, 管道内数据排队流出, 如何连续不断流出?
fmt.Println(x, y, x+y)
}
- 缓冲管道(有容量的队列)
cc := make(chan int, 2)
cc <- 1
cc <- 2
fmt.Println(<-cc)
fmt.Println(<-cc)
- channel 可以是 带缓冲的[容量]。为 make 提供第二个参数作为缓冲长度来初始化一个缓冲 channel: `ch := make(chan int, 100)`- 向缓冲 channel 发送数据的时候,只有在缓冲区满的时候才会阻塞。当缓冲区清空的时候接受阻塞。如果满了之后仍然塞入数据则会报错, 如何判断是否满, 避免报错?- close 和 range 关闭管道并遍历管道
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
发送者可以 close 一个 channel 来表示再没有值会被发送了。
接收者可以通过赋值语句的第二参数来测试 channel 是否被关闭:当没有值可以接收并且 channel 已经被关闭,那么经过
v, ok := <-ch
之后 ok 会被设置为 false
。
循环 for i := range c
会不断从 channel 接收值,直到它被关闭, 如果没有close, 则会一直处于阻塞情况并报错。
注意: 只有发送者才能关闭 channel,而不是接收者。向一个已经关闭的 channel 发送数据会引起 panic。
还要注意: channel 与文件不同;通常情况下无需关闭它们。只有在需要告诉接收者没有更多的数据的时候才有必要进行关闭,例如中断一个 range
。
## select- 个人理解
选择通信信道的一个语法, case语句不同于switch的条件, 而必须是chan操作.
select 语句使得一个 goroutine 在多个通讯操作上等待。
select 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。
当 select 中的其他条件分支都没有准备好的时候,default
分支会被执行。
- 示例
func main() {
tick := time.Tick(100 time.Millisecond)
boom := time.After(500 time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
# defer- 在函数中,我们经常需要创建资源(如,数据库连接 / 文件句柄 / 锁等),为了在函数执行完毕后,可以及时释放资源,Go框架设计了defer。
Note:
当go执行到一个defer时,不会立即执行当前行defer {后半部分}的语句,而是将defer后面的语句压进栈中(可以理解为defer栈)当函数执行结束后,defer 语句出栈,遵循先入后出在defer将语句放入栈中时,也会将相关的值Copy,同时入栈defer最主要的价值在于,当函数执行完毕后,可以及时地释放函数创建的资源,如defer file.close() / defer connect.close()
示例:
func sum(num1 int, num2 int) int {
//暂时不执行,压进defer栈中
defer fmt.Println("defer1 num1=", num1)
defer fmt.Println("defer2 num2=", num2)
// 执行
res := num1 + num2
fmt.Println("resFun=", res)
return res
}
func main() {
res := sum(10,20)
// 当函数执行完毕后,defer按照先进后出的方式出栈
fmt.Println("res=",res)
}
// 运行结果:
resFun= 30
defer2 num2= 20
defer1 num1= 10
res= 30
# init函数- 每一个源文件都可以包含一个init函数,该函数在main函之前被go框架运行。通常可以在init函数中完成初始化工作。
Note:
如果一个文件同时包含全局变量定义 / init() / main()。其执行的流程是 全局变量定义 -> init() -> main()
init函数最大的作用是完成一些初始化的工作
var gVar = gTest()func gTest() string { fmt.Println("global variable:") return "global variable:"}func init() { fmt.Println("init function:")}func main() { fmt.Println("main function:")}
// 运行结果:
global variable:
init function:
main function: