介绍
Go(Golang) 是谷歌开发的一种 动态强类型、编译型、并发型,并具备垃圾回收性能的编程语言。
咱们为什么要学习 Go?其实我感觉是因为公司倒退越来越快的一个必然趋势,随着倒退,很多货色是 nodejs 不肯定能很好的反对。咱们须要后端多样化,以在将来某个时段咱们能有更大的能力去面对未知的事务。
Golang 的 Hello World
还记得以前学习 C 语言的时候,老师都是从 Hello World 开始讲起,明天咱们也是从这里开始咱们的 Golang 之旅。
package main
import "fmt"
func main() {fmt.Println("Hello World")
}
和 C 语言相似,咱们的程序都是从 main
函数开始,应用如下语句进行编译:
$ go build helloworld.go
编译完了之后,会在以后文件生成一个 helloworld
可执行文件,执行改文件即可打印:
$ ./helloworld
Hello World
下载
官网下载地址(点我)
下载对应零碎的安装包
解压缩:
tar -C /usr/local -xzf go1.15.2.linux-amd64.tar.gz
增加 PATH 环境变量:
export PATH=$PATH:/usr/local/go/bin
装置好了应用上面命令测试:
$ go version
go version go1.15.2 linux/amd64
咱们能够应用 go env
查看 go 的环境变量,咱们先重点关怀这个环境变量:
GOPROXY
Go 除了本身带了很多包,社区也有很多开源我的项目的包,大部门都是在 github、google、等域名下面,如果你的 terminal 没有翻墙,拉取会很慢,这个时候,咱们能够设置这个环境变量(非零碎环境变量):
$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,direct
这样,拉取所有的包都走的是七牛云。
Golang 的语法
Golang 是一门上手十分快、并发性能十分高的语言,咱们先来介绍 Go 的根本语法。
package
每份 .go
代码,都须要在文件的第一行指定包名,一般来说,都是以后所在文件夹的名字,在同一个文件夹下的 Go 文件,包名是统一的。包名不能应用特殊字符,比方 -
等, 个别都是小写并且简短
其中有个例外,就是 package main
,main 包在一个文件夹下只能有一个。
包的导入有两种形式:
import "github.com/go-redis/redis"
import "context"
// OR
import (
"github.com/go-redis/redis"
"context"
)
咱们举荐应用第二种写法,有时候,咱们可能只想援用某个包,让其执行 init 函数,而不会用到包外面的函数,能够这样做:
import (_ "github.com/go-sql-driver/mysql" // 只会执行 mysql 包下的 init 函数)
main 函数
main 函数是咱们我的项目的入口程序,如果一个 go 代码被编译,它没有 main 函数,是无奈编译出可执行文件的。
main 函数的写法是:
func main() {// do something}
语句
一个 statement 就是一行,不须要结尾的分号
比方:
func main() {fmt.Println("Hello Go!") // 这就是一条语句
}
正文
和大部分语言的正文一样,Go 应用 //
代表单行正文,应用 /** **/
代表多行正文
比方:
func main() {
// 我是单行的正文
/**
我是
多行
的
正文
**/
}
根本类型
Go 是强类型的语言,不会像咱们目前应用的 nodejs 一样,一个变量能够随便赋任意类型的值。在 Go 中,一旦定义了某个变量的类型,则这个变量只能赋予该类型的值。
- 布尔类型 bool 布尔的值只能是
true
或者false
,和 nodejs 不同,0 不代表 false。 - 字符串类型 string 字符串必须是由双引号蕴含起来的字符汇合。
- 数字类型 见下
数字类型 能够分为 整数类型、浮点数类型和一些其它非凡意义的类型
整数类型
序号 | 类型 | 形容 |
---|---|---|
1 | uint8 | 无符号 8 位整数(0-255) |
2 | uint16 | 无符号 16 位整数(0-65535) |
3 | uint32 | 无符号 32 位整数(0-4294967295) |
4 | uint64 | 无符号 64 位整数(0-18446744073709551615) |
5 | int8 | 有符号 8 位整数(-128 到 127) |
6 | int16 | 有符号 16 位整数(-32768 到 32767) |
7 | int32 | 有符号 32 位整型 (-2147483648 到 2147483647) |
8 | int64 | 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
浮点数类型
序号 | 类型 | 形容 |
---|---|---|
1 | float32 | IEEE-754 32 位浮点型数 |
2 | float64 | IEEE-754 64 位浮点型数 |
3 | complex64 | 32 位实数和 32 位虚数 |
4 | complex128 | 64 位实数和 64 位虚数 |
其它数字类型
序号 | 类型 | 形容 |
---|---|---|
1 | byte | 相似 uint8 |
2 | rune | 相似 int32 |
3 | uint | 无符号整数,取决于零碎,零碎是 32 位则是 32 位,零碎是 64 位则是 64 位整数 |
4 | int | 有符号整数,和 uint 一样 |
5 | uintptr | 无符号整型,用于寄存一个指针 |
变量
变量名的规定和其它语言都相似, 只能由字母、数字和下划线取名,且不能以数字结尾:
var variable data_types = value
其实申明变量的形式由很多种
// 第一种 这种形式必须要指定类型
// 如果 a 没有被赋值,则 a 的值默认是该类型的零值,int->0,string->"",bool->false
var a int
a = 1
// 第二种 申明的时候间接赋值,如果没有给定类型,会主动推导进去
var a int = 1
var a = 1 // fmt.Printf("%T", a) => int
// 第三种 申明常量, 常量申明的时候必须要赋值,类型能够写能够不写,会主动推导,常量不可被批改
const a = 1
const (
a = 100
b
) // a、b 都是 int 值都是 100,只有 const 能够这样用
const (
a = iota // 0
b // 1
c // 2
d = "pdf" // iota+1
e = 100 // iota+1
f = iota // 5
g // 6
) // iota 是一个非凡常量,z 在下一行的时候会主动递增,只在一个 const()作用域无效,用作枚举很不便
// 第四种 主动推导
a := 1
a := ""
变量的申明是能够多个变量能够一起申明的:
var (
a,
b,
c int
) // a、b、c 都被申明为 int 类型
var (
a,
b,
c int
d,
e,
f string
) // a、b、c 都被申明为 int 类型,d、e、f 都被申明为字符串类型
a, b := 1, "" // a 是 int,b 是 string,这种形式最罕用,因为函数的返回值能够是多个
运算符
运算符用于程序在执行算术运算或者逻辑运算时应用。
Go 内置的运算符有:
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 其它运算符
算术运算符
假如 A =10 B=20
运算符 | 形容 | 实例 |
---|---|---|
+ | 相加 | A+B = 30 |
– | 相减 | B – A=10 |
* | 相乘 | A * B = 200 |
\ | 相除 | B / A = 2 |
% | 求余 | B % A = 0 |
++ | 自增 | A++ // A=> 11 |
— | 自减 | B– // B=> 19 |
关系运算符
假如 A =10 B=20, 关系运算符的后果时 bool 类型的值
// ==
fmt.Println(A == B) // false
// !=
fmt.Println(A != B) // true
// > < >= <=
fmt.Println(A > B) // false
fmt.Println(A < B) // true
fmt.Println(A >= B) // false
fmt.Println(A <= B) // true
逻辑运算符
假如 A = true B = false
// && 逻辑 AND
fmt.Println(A && B) // => false
// || 逻辑或
fmt.Println(A || B) // => ture
// ! 逻辑非
fmt.Println(!A, !B) // => false true
位运算符
- 按位与 &
- 按位或 |
- 按位异或 ^: 1^1=0;1^0=1;0^0=0;
- 左移 <<
- 右移 >>
赋值运算符
=、+=、-=、*=、=、%=、<<=、>>=、&=、^=、|=
其它运算符
指针取地址: &, 取值 *
条件语句
Go 语言提供上面几种条件语句:
- if 语句
- switch 语句
- select 语句
if 语句
Go 语言中,if 语句的语法:
if 布尔条件表达式 {statement} else if 布尔条件表达式 {statement} else {statement}
留神,这里的条件表达式不须要括号,举个实例:
package main
func main() {
a := 1
b := 2
if a < b {// if 语句有这样的语法糖 if a:= func(); a < 10 {}
fmt.Println("a < b")
} else if a == b {fmt.Println("a == b")
} else {fmt.Println("a > b")
}
}
switch 语句
Go 语言的 switch 语句很弱小,和 nodejs 的不太一样,Go 语言中的 Switch 语法:
switch expression {
case condition:
statement
default:
statement
}
首先,expression 能够是任何类型的值,如果没有写,则默认是 true, condition 必须要和 expression 的类型统一,否则报错,并且 condition 能够为多个,以逗号隔开,default 子句示意当 case 都没有满足的条件的时候执行,同时最大的一点不同之处,没有 break 语句,Go 程序的 switch 每一个 case 执行完了就退出 switch 而不会持续向下执行,如果须要向下执行,则须要在 case{}中最初加上一句:fallthrough
比方:
switch "2" {
case "1", "2", "3":
fmt.Println("1 2 3")
fallthrough
case "4":
fmt.Println("4")
default:
fmt.Println("default")
} // => 1 2 3
// => 4
select 语句
select 语句个别用于超时管制,是 Go 的一个控制结构,每个 case 都是一个 channel 操作,select 将随机选取一个能够运行的 case 执行,如果没有能够运行的 case,则 select 语句会阻塞,直到有能够执行的 case。默认子句总是能够执行的。
语法如下:
select {
case <- ch: // 前面会讲,这个是 channel 这一块的常识
statement
default:
statement
}
不过个别咱们都不会用 default, 就像下面讲的,咱们个别是用来做超时管制的,举个例子:
package main
import (
"fmt"
"time"
)
func sleep(n int, res chan<- string) {time.Sleep(time.Duration(n) * time.Second)
res <- "获取到了后果" // 管道外面设置数据
}
func main() {timeout := time.After(time.Duration(3) * time.Second)
res := make(chan string)
go sleep(2, res)
select {
case <- timeout:
fmt.Println("超时了")
case val := <- res: // 接管管道的数据
fmt.Println(val)
}
}
这里,咱们申请一个函数,同时设置一个 3s 的超时,如果函数 3s 内没有返回数据,则 select 会执行超时这个 case, 否则就执行了取后果的数据。当然,这个例子是没有代表性的,因为理论工程上要比这个简单一点,因为要波及到资源的开释之类的,前面会讲到。
循环语句
Go 的循环语句和 nodejs 也不太一样,咱们先看语法:
// 第一种
for init; condition; post { // 留神,这里能够省略任意一项,如果只有 condition 能够不要分号,statement
}
// 咱们常常会应用死循环 +select 来做一下事件
for {
select {case xxx}
}
// 第二种
for k,v := range mapValue { }
// 因为 Go 定义了变量,就必须要应用,所以不须要的变量能够用 _ 代替, 比方
for _, v := range mapValue {}
函数
Go 中函数的申明从 func
关键字开始,语法如下:
func funcName(v datatypes) datatypes {}
func 关键字前面接的是函数名字,函数名字个别是用驼峰命名,如果是小驼峰,则这个函数只能在以后文件被调用,如果是大驼峰,则能够被其它包引入调用。
括号外面的是形参,模式为 变量名 变量类型
。Go 的返回值也必须要定义类型,定义类返回值类型,则必须要有返回值。
举个例子:
func f1(a int, b string) string {} // 传了一个 int, 一个 string 返回值是 string
func f2(a, b int, c, d string) {} // 如果两个参数相邻并且类型统一,则只须要在最初一个变量写类型
func f3() (string, string) {} // Go 反对返回多个参数,这里返回了两个 string 类型的值
func f4() (a , b int) {} // 这里示意曾经定义好了返回值的变量,只有在代码外面对 a,b 赋值就能够,如果没有赋值,则返回对应类型的零值
!!!十分重要!!!
函数传参个别有两种:
- 值传递是指在调用函数时将理论参数复制一份传递到函数中,这样在函数中如果对参数进行批改,将不会影响到理论参数
- 援用传递是指在调用函数时将理论参数的地址传递到函数中,那么在函数中对参数所进行的批改,将影响到理论参数。
而 Golang 应用的是值传递
https://goinbigdata.com/golan…
每个 go 文件都能够有一个 init
函数,在被导入包的时候就会执行,并且只会执行一次,被屡次导入也只会执行一次。
func init() {}
数组
数组是一种具备惟一的雷同的数据类型、固定长度的数据项序列,这个类型能够是整型、字符串或者任意自定义的构造体。
定义数组必须要指定长度,语法如下:
var variables [size] variable_type
以下是例子:
var balance [10] float32 // 长度为 10 的数组,初始值是[0,0,0,0,0,0,0,0,0,0]
var balance = [3]int {1, 2, 3} // 初始化, 晓得长度
var balance = [...]int {1, 2, 3, 4, 5} // 初始化,如果不晓得长度,能够主动推断
指针
和 nodejs 不同的是,Go 是有指针的,和 C 一样,都是应用 &
进行取地址,应用 *
定义指针变量
举个例子:
var a int = 3
var p *int = &a // p := &a
在 C 语言中,指针属于比拟难的一块,什么是指针?一个指针变量,指向了一个值的地址:
比方一个变量 C 保留了字符 ‘K’,地址在 0x11A,而一个指向 c 的指针变量 p,它的值就是 C 的地址 11A。
通过一个实例来看怎么应用指针:
s := &map[string]string{"a": "a", "b": "b"}
v, ok := (*s)["a"]
构造体
构造体是 Go 中十分重要的一个模块,前面的开发肯定会用的到,不论是创立 Service 还是创立 Orm 的 model。
数组是只能存储一个类型固定长度的值,构造体呢?能够存储多个不同类型的值,简略来说,构造体就是一系列不同或者雷同类型的汇合。
构造体语法定义如下:
type struct_name struct {
field1 data_types
field2 data_types
...
}
举个例子:
type User struct {
Name string
Age uint8
ID int
Event [10]string
}
这样咱们就定义了一个构造体,申明应用如下:
// 不是很举荐的写法
user := User{"pdf", 8, 123, [10]string{"a", "b"}} // 按定义程序顺次填入
// 举荐写法
user := User{
Name: "pdf"
Age: 8,} // 能够疏忽掉不须要的字段,这样没有赋值的就是对应的零值
申明了之后,应用形式如下:
user.Name = "ghj"
fmt.Println(user.Age)
这里有一个语法糖,对于指针的,就是说,对于构造体的指针,咱们能够疏忽 *
:
u := &user
fmt.Println(u.Name, (*u).Name)
切片(slice)
数组长度是固定的,对于长度不晓得或者不固定的数据,咱们就很难应用数组去实现这种业务。
所以就有了切片(动静数组),切片长度是不固定的,切片的定义和数组的定义很相似,不过不须要长度
arr := [10]int{}
slice := []int{}
fmt.Printf("arr: %T, slice: %T", arr, slice) // arr: [10]int, slice: []int
// 还能够用 make 创立切片
slice := make([]type, len) // 指定初始长度
// 还能够指定容量
slice := make([]type, len, cap)
切片初始化和赋值:
a := []int{1, 2} // 申明的时候间接初始化
a = append(a, 1) // 追加
arr := [10]int{}
slice := arr[start:end] // 从数组外面切,从 start 算,到 end-1
当初,咱们说了数组和切片,能够开始说一下长度和容量了。
Go 提供了 len(v Type)
和 cap(v Type)
查看数组和切片的长度以及容量
数组的长度和容量都是固定的!
切片的长度示意以后切片值的个数,切片的容量是什么呢?
咱们须要从切片的实质去看:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
这个就是切片的构造体,Data 指向数组的指针,Len 示意以后切片的长度,Cap 示意 Data 数组的大小。
用一个理论的例子来说:
b := [4]int{}
d := b[0: 3]
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ① b=[0 0 0 0], d=[0 0 0], len(d)=3, cap(d)=4
d[0] = 1
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ② b=[1 0 0 0], d=[1 0 0], len(d)=3, cap(d)=4
d = append(d, 2)
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ③ b=[1 0 0 2], d=[1 0 0 2], len(d)=4, cap(d)=4
d[0] = 3
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ④ b=[3 0 0 2], d=[3 0 0 2], len(d)=4, cap(d)=4
d = append(d, 4)
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ⑤ b=[3 0 0 2], d=[3 0 0 2 4], len(d)=5, cap(d)=8
d[0] = 5
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ⑥ b=[3 0 0 2], d=[5 0 0 2 4], len(d)=5, cap(d)=8
d 是切片,这个时候 Data 指向的就是 b 这个数组,一个六个输入,顺次说一下:
- d 切了[0:3], 所以长度是 3,容量是 b 数组的长度,也就是 4,当没有赋值时,int 的默认零值是 0,所以 b =[0,0,0,0],d=[0,0,0]
- 因为切片的 Data 指向了 b,所以批改是批改 b 数组的值,所以 b = [1, 0, 0, 0],d = [1, 0, 0]
- append 函数用于切片开端追加一个值,留神,这个不是间接批改 d,必须要从新用 d 接管返回值,所以此时 d 的长度是 4,容量也是 4,因为 d 的 Data 指向 b,所以 b 和 d 的值都是 [1, 0, 0, 2]
- 因为 d 的 Data 指向 b,所以 b 和 d 的值都是[3, 0, 0, 2]
- 这个时候,d 再次追加了一个值,这个时候,b=[3,0,0,2], 和上一次打印是统一的,而 d 的值为[3, 0, 0, 2, 4],追加胜利,并且长度的确加了 1,然而容量却从 4 变为了 8. 可能看到这里大家就会纳闷了,d 的 Data 不是指向了 b 吗,为什么这一次 b 没有被批改呢?这是因为数组是长度不变的,切片是动静数组,当切片容量不够时,就会新申请一片间断的内存作为 Data 的指向,并且将原来的值复制过来,这个就是切片的扩容(扩容算法不开展讲),所以一旦产生了扩容,切片的指向就会发生变化
- 所以这个时候,再次批改 d,也不会影响 b 了,所以 b =[3, 0, 0, 2],d=[5, 0, 0, 2, 4]
切片的零值是 nil
就像下面说的,间接切片一个数组或者别的切片失去的新切片,其实 Data 指向的还是原来的,然而有时候咱们不要这个援用,因为咱们要传参的时候想要批改这个值然而不能影响实参,所以咱们就须要拷贝切片:
old := []int{1, 2, 3}
newSlice := make([]int, len(old), 2 * cap(old)) // 应用 make 函数,指定长度和容量,留神,长度肯定小于等于容量
fmt.Printf("old=%v, newSlice=%v\n", old, newSlice)
// old=[1 2 3], newSlice=[0 0 0]
copy(newSlice, old) // 应用 copy 函数复制,第一个参数是指标,第二个参数是被拷贝的值,newSlice 的长度至多都要等于 old 的长度,否则会截断
fmt.Printf("old=%v, newSlice=%v\n", old, newSlice)
// old=[1 2 3], newSlice=[1 2 3]
old[0] = 2
fmt.Printf("old=%v, newSlice=%v\n", old, newSlice)
// old=[2 2 3], newSlice=[1 2 3]
// old 批改胜利,然而 newSlice 不会被批改
Map 汇合
Go 种的 Map 汇合和咱们应用的 Nodejs 种的对象相似,它就是一组无序的键值对的汇合。
var m map[string]int
m := make(map[string]int)// 键是 string 类型,值是 int 类型
// 初始化
var m = map[string]int{"pdf": 100}
m["ghj"] = 99
fmt.Println(m) // map[ghj:99 pdf:100]
Map 值的获取就比拟有意思了,咱们后面有始终说到,一个变量定义了,它都会有本人对应的零值,所以在 map 获取值的时候,如果某个 key 是不存在的,则返回了 value 类型对应的零值,比方:
v := m["none"]
fmt.Println(v) // 0
这个时候是有问题的。大家思考一下
如果我原本就是有一个存在的 key,并且值我就是设置为 0 呢?
m["hz"] = 0
v := m["hz"]
fmt.Println(v) // 0
这个时候就没有方法辨别这个 key 的值到底是默认的零值还是咱们本人设置的零值。所以咱们须要用另外一种写法:
if v, ok := m["none"]; ok {fmt.Println(v)
}
v, ok := m["none"]
fmt.Println(v, ok) // 0 false
还记得 if 这个写法吗?忘了的同学能够往前面看看。这个时候,咱们多承受了一个 ok 参数,这个参数的类型是 bool,如果为 true 示意 key 存在,如果为 false 示意 key 不存在,获取的是零值。
map 的 key 是能够用 ==
或者 !=
作比拟的类型(map 和 interface{}这两种是我晓得的不可比拟的两种动静类型)。比方:
m := map[bool]bool{true: true}
m := map[struct{}]map[string]int
刚刚说过,key 存不存在能够用第二个返回值判断,如果说前面咱们须要删掉一个 key,能够应用 delete
函数:
m := map[string]int{"pdf": 1}
delete(m, "pdf")
v, ok := m["pdf"]
fmt.Println(v, ok)
接口 interface
Go 语言提供了另外一种数据类型 即接口,他把所有具备共性的办法定义在一起,任何其它类型只有实现了这些办法 (接口定义外面的全副) 就相当于实现了这个接口。
比方:
type UserInferface interface { // 定义了一个用户接口
GetUserInfo(userId int) (UserInfoDTO, error)
UpdateUserInfo(vo UserVo) (UserInfoDTO, error)
}
type UserService struct{} // 定义一个用于实现用户接口的构造体
func (service *UserService) GetUserInfo(userId int) (UserInfoDTO, error) { }
func (service *UserService) UpdateUserInfo(vo UserVO) (UserINfoDTO, error) { }
func main () {s := &UserService{}
s.GetUserInfo()}
这样,UserService 就实现了 UserInterface 这个接口。
错误处理
Go 语言通过内置的谬误接口提供了非常简单的错误处理机制(Go 中没有所谓 try catch)。
error 类型是一个接口类型,定义如下:
type error interface {Error() string
}
通常咱们都会应用 errors
包来返回一个谬误:
func demo() error {return errors.New("demo 产生谬误")
}
err := demo()
if err != nil {fmt.Println(err) // fmt.Println(err.Error())
}
晓得了 error 类型是一个接口,所以咱们能够自定义 error 类型:
func (us * User1Service)Error() string {return us.Name + "又错了"}
func demo() error {
return &User1Service{Name: "pdf",}
}
err := demo()
fmt.Println(err) // pdf 又错了
Go 并发
并发这里,是初识 Golang 的最初一节,咱们抉择 Go 的一个很大起因就是 Go 的并发能力很强,并且应用非常简单!
简略到什么水平?
go someFunc()
就这么简略,只须要被调用函数后面加一个 go
关键字就能够了。
go
关键字其实是开启一个叫做 goroutine
货色,这个其实叫做协程。
main 函数就是一个主协程。
举个例子:
func output(intput int) {fmt.Println(intput)
}
func main() {
for i := 0; i < 10; i++ {go output(i)
}
for i := 10; i < 20; i++ {go output(i)
}
time.Sleep(time.Duration(1) * time.Second)
}
当你运行的时候,能够看到,每一次输入的后果都是不统一的。加一个睡眠是因为,主协程运行完结,则程序就退出了,所以还没有运行完的协程就没有运行机会了。
这样来看,有没有感觉和咱们的异步是一样的,哈哈。
结束语
明天讲的货色,我全副都没有深刻去讲,心愿这一篇文章,能让咱们大家从一个 noder,从此也能够叫为 gopher。