Go语言入门
--用更少的代码,更短的编译时间,创建运行更快的程序,享受更多的乐趣--
什么是Golang
Golang是谷歌创建的,开放源代码、编译型和静态类型的编程语言。
Golang 的主要关注点在使开发高可用和可伸缩的web应用程序更加简单和容易。
Golang最主要的特性
- 自动垃圾回收
- 更丰富的内置类型
- 函数多返回值
- 错误处理
- 匿名函数和闭包
- 类型和接口
- 并发编程
- 反射
- 语言交互性
Golang适合做什么
- 服务器编程,例如处理日志、数据打包、虚拟机处理、文件系统等。
- 分布式系统,数据库代理器等。
- 网络编程,包括Web应用、API应用、下载应用。
- 内存数据库,云平台...
安装
Golang 支持所有第三方操作系统 Mac、Windows、 Linux,你可以从以下链接下载相应平台的二进制文件 https://golang.org/dl/
Mac OS
从https://golang.org/dl/下载.pkg文件,双击开始安装,根据安装提示安装完成后,Golang被安装在 /usr/local/go 目录中,/usr/local/go/bin 会被加入到系统环境变量中。
Windows
从https://golang.org/dl/下载.msi文件,双击安装文件根据安装提示直到安装完成。Golang会被安装在 c:\Go 目录,c:\Go\bin目录会被加入到系统环境变量中,分别设置在GOROOT和Path里。在用户环境变量中GOPATH为go项目的工作空间,可以自定义。
Linux
从https://golang.org/dl/下载tar文件,使用unzip命令解压至/usr/local目录,添加/usr/local/go/bin 到系统环境变量中。
编辑器
没有专属的IDE,用什么编辑器都可以,推荐vscode
GOPATH
GOPATH:用来存放用户的Go源码,Go的可运行文件,以及相应的编译之后的包文件
src 存放源代码,使用go get下载的代码会放到这个目录
pkg 编译后生成的文件(比如:.a)
bin 编译后生成的可执行文件
Go命令
go install:编译并把编译好的结果移到$GOPATH/pkg或者$GOPATH/bin
go build :编译(项目和文件)
常用参数:
-o 指定输出的文件名,可以带上路径,例如 go build -o out main.go
-i 安装相应的包,编译+go install
-v 打印出来我们正在编译的包名
go get:安装第三方包
常用命令:
-d 只下载不安装
-u 强制使用网络去更新包和它的依赖包
-v 显示执行的命令
go clean:移除当前源码包和关联源码包里面编译生成的文件
go fmt:格式化代码
godoc:文档
godoc -http=:端口号 比如godoc -http=:8080然后打开127.0.0.1:8080可以在浏览器中查询pkg文档
godoc net/http:查看http包的文档
godoc -src fmt Printf:查看fmt.Printf的代码
go run: 编译并运行Go程序
Go语言结构
Go语言的基础组成有以下几个部分
- 包声明
- 引入包
- 函数
- 变量
- 语句&表达式
- 注释
//导入当前程序包 package main //导入其他包 import "fmt" //由main函数作为程序入口点启动 func main(){ fmt.Println("My,first Go applition") }
- package main定义了包名,你必须在源文件中非注释的第一行指明这个文件属于哪个包,每个Go应用程序都包含一个名为main的包。
- import "fmt"是告诉Go编辑器这个程序需要使用fmt包的函数,或其他元素,fmt包实现了格式化IO(输入/输出)的函数。
- func main()是程序开始执行的函数。main函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有init()函数则会先执行该函数)
- // 和 /*...*/是Go语言的注释标识
- fmt.Println(...)可以将字符串输出到控制台,并在最后自动增加换行字符 \n,使用 fmt.Print("My,first Go applition\n") 可以得到相同的结果。
- 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出;标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的。
导入多个包
import "fmt" import "io" //也可以 import { "fmt", "io" }
Go关键字(25个)
break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var
预定义标识符(36个)
append bool byte cap close complex complex64 complex128 uint16 copy false float32 float64 imag int int8 int16 uint32 int32 int64 iota len make new nil panic uint64 print println real recover string true uint uint8 uintptr
Go基础
函数
函数通过关键字func来声明,可以没有参数或接受多个参数。
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { //这里是处理逻辑代码 //返回多个值 return value1, value2 }
func add(x int, y int) int { return x + y } 等价于 func add(x, y int) int { return x + y }
Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参
func myfunc(arg ...int) {}
arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int,在函数体中,变量arg是一个int的slice
for _, n := range arg { fmt.Printf("And the number is: %d\n", n) }
Go语言的数据类型
在Go编程语言中,数据类型用于声明函数和变量。
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。
变量
Go 语言变量名用字母,数字,下划线组成。其中首字母不能使数字。
声明变量的一般形式是使用var关键字,_这个变量比较特殊,任何赋予它的值都会被丢弃。函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。
// 定义变量 var v1 int // 定义多个变量 var v1, v2, v3 string // 定义多个变量并赋值 var enabled, disabled = true, false v1 := "go" //:=在类型明确时代替了var
常量
常量可以是字符、字符串、布尔值或数值。常量不能用 := 语法声明。
const Pi = 3.14
基本类型
- bool
- string
- int int8 int16 int32 int64
- uint uint8 uint16 uint32 uint64 uintptr
- byte // uint8 的别名
- rune // int32 的别名,表示一个 Unicode 码点
- float32 float64
- complex64 complex128
同导入语句一样,变量声明也可以“分组”成一个语法块。
var ( ToBe bool = false MaxInt uint64 = 1<<64 - 1 z complex128 = cmplx.Sqrt(-5 + 12i) )
零值
没有明确初始值的变量声明会被赋予它们的 零值。
零值是:
数值类型为 0,
布尔类型为 false,
字符串为 ""(空字符串)。
类型转换
表达式 T(v) 将值 v 转换为类型 T。Go 在不同类型的项之间赋值时需要显式转换。
一些关于数值的转换:
var i int = 42 var f float64 = float64(i) var u uint = uint(f) //或者,更加简单的形式: i := 42 f := float64(i) u := uint(f)
流程控制语句
for
for循环的语法
for expression1; expression2; expression3 { //... }
for循环例子
package main import "fmt" func main(){ sum := 0; for index:=0; index < 10 ; index++ { sum += index } fmt.Println("sum is equal to ", sum) }// 输出:sum is equal to 45
有些时候如果我们忽略expression1和expression3:
sum := 1 for ; sum < 1000; { sum += sum }
其中;也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是while的功能。
sum := 1 for sum < 1000 { sum += sum }
在循环里面有两个关键操作break和continue ,break操作是跳出当前循环,continue是跳过本次循环。
if
同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。
该语句声明的变量作用域仅在 if 之内。在 if 的简短语句中声明的变量同样可以在任何对应的 else 块中使用。
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\n", v, lim) } // 这里开始就不能使用 v 了 return lim } func main() { fmt.Println( pow(3, 2, 10), pow(3, 3, 20), ) }
switch
switch 是编写一连串 if - else 语句的简便方法。实际上,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 的 case 语句从上到下顺次执行,直到匹配成功时停止。
没有条件的 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
推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
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
结构体
定义结构体和C语言一样,使用struct关键字。在结构体内部定义它们的成员变量和类型。如果成员变量的类型相同还可以把它们写到同一行。
type Person struct { age int name string }
初始化
初始化结构体需要使用一个特殊一点的语法,这就是结构体字面量。在结构体字面量中,可以按照顺序初始化结构体、也也可以按照关键字初始化结构体。如果按照关键字初始化结构体,可以只指定部分值,未指定的值将会使用默认值来初始化。
p1 := Person{24, "易天"} p2 := Person{age: 24, name: "易天"} p3 := Person{age: 24} p4 := Person{name: "张三"} fmt.Println(p1, p2, p3, p4)
访问结构体
最后要说的就是访问结构体了。结构体的成员都是公有的,所以可以直接用点号.访问。
p1.age = 26 p1.name = "王五" fmt.Println(p1)
指针
如果学习过C语言的话,对指针的概念应该会比较熟悉。在Go语言中,直接砍掉了最复杂的指针运算的部分,只留下了获取指针(&运算符)和获取对象(*运算符)的运算。
a, b := 3, 5 pa, pb := &a, &b fmt.Println(*pa, *pb)
对于一些复杂类型的指针, 如果要访问成员变量的话,需要写成类似(*p).field的形式,Go提供了隐式解引用特性,我们只需要p.field即可访问相应的成员。
p1 := &Person{name: "易天", age: 24} fmt.Println((*p1).name) fmt.Println(p1.name)
数组array
数组声明
var arr [10]int // 声明了一个int类型的数组 arr[0] = 42 // 数组下标是从0开始的 arr[1] = 13 // 赋值操作 fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42 fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0
声明并赋值
a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组 b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0 c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度
切片slice
在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要“动态数组”。
slice并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层array,slice的声明也可以像array一样,只是不需要长度。
声明
// 和声明array一样,只是少了长度 var fslice []int // 声明并赋值 slice := []byte {'a', 'b', 'c', 'd'} // 声明一个含有10个元素元素类型为byte的数组 var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} // 声明两个含有byte的slice var a, b []byte // a指向数组的第3个元素开始,并到第五个元素结束, a = ar[2:5] //现在a含有的元素: ar[2]、ar[3]和ar[4] // b是数组ar的另一个 slice b = ar[3:5] // b的元素是:ar[3]和ar[4]
也可以用make创建一个slice
var s = make([]int, 5) //长度是5,容量是5 var s = make([]int, 5, 10) //长度是5,容量是10
对于slice有几个有用的内置函数:
len 获取slice的长度
cap 获取slice的最大容量
append 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice
copy 函数copy从源slice的src中复制元素到目标dst,并且返回复制的元素的个数
slice是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的aSlice和bSlice,如果修改了aSlice中元素的值,那么bSlice相对应的值也会改变。
Range
for 循环的 range 形式可遍历切片或映射。
当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
package main import "fmt" 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) } }
可以将下标或值赋予 _ 来忽略它。
若你只需要索引,去掉 , value 的部分即可。
package main import "fmt" func main() { pow := make([]int, 10) for i := range pow { pow[i] = 1 << uint(i) // == 2**i } for _, value := range pow { fmt.Printf("%d\n", value) } }
映射map
定义map
可以使用内建函数 make 也可以使用 map 关键字来定义 map:
/* 声明变量,默认 map 是 nil */ var map_variable map[key_data_type]value_data_type /* 使用 make 函数 */ map_variable := make(map[key_data_type]value_data_type) // 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化 var numbers map[string]int // 另一种map的声明方式 numbers = make(map[string]int) numbers["one"] = 1 //赋值 numbers["ten"] = 10 //赋值 numbers["three"] = 3 //赋值 fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据 // 打印出来如:第三个数字是: 3
map也是一种引用类型,如果两个map同时指向一个底层,那么一个改变,另一个也相应的改变。
GO 中new 与 make
Go提供了两种分配原语,即new和make。它们所做的事情是不一样的,所应用的类型也不同。new(T) 返回 T 的指针 *T 并指向 T 的零值。make(T) 返回的初始化的 T,只能用于 slice,map,channel。
new用来分配内存,但与其他语言中的同名函数不同,它不会初始化内存,只会讲内存置零;也就是说,new(T)会为类型为T的新项分配已置零的内存空间,并返回他的地址,也就是一个类型为*T的值。用Go的术语来说,它返回一个指针,该指针指向新分配的,类型为T的零值。
make的目的不同于new,它只用于slice,map,channel的创建,并返回类型为T(非指针)的已初始化(非零值)的值;出现这种差异的原因在于,这三种类型本质上为引用类型,它们在使用前必须初始化。
new和make都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:slice、map 和 channel。
例子
package main import "fmt" func main() { p := new([]int) //p == nil; with len and cap 0 fmt.Println(p) v := make([]int, 10, 50) // v is initialed with len 10, cap 50 fmt.Println(v) /*********Output**************** &[] [0 0 0 0 0 0 0 0 0 0] *********************************/ (*p)[0] = 18 // panic: runtime error: index out of range // because p is a nil pointer, with len and cap 0 v[1] = 18 // ok }
方法
如果函数有接受者,这个函数就称为方法。
方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。
在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
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()) }
方法即函数,方法只是个带接收者参数的函数。这个 Abs 的写法就是个正常的函数,功能并没有什么变化。
接口interface
接口类型 是由一组方法签名定义的集合。我们通过interface来定义对象的一组行为
package main
import ( "fmt" ) // notifier是一个定义了// 通知类行为的接口 type notifier interface { notify() } // user在程序里定义一个用户类型 type user struct { name string email string } // notify是使用指针接收者实现的方法 func (u *user) notify() { fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email) } // main是应用程序的入口 func main() { // 创建一个user类型的值,并发送通知 u := user{"Bill", "bill@email.com"} // u.notify() var n notifier n = &u n.notify() //notify方法使用指针接收者声明 }
如果使用指 针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值 接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。
空interface
指定了零个方法的接口值被称为 *空接口:*
interface{}
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法)
空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。
package main import "fmt" func main() { var i interface{} describe(i) i = 42 describe(i) i = "hello" describe(i) } func describe(i interface{}) { fmt.Printf("(%v, %T)\n", i, i) }
入门资料
A Tour of Go https://tour.golang.org
How to Write Go Code https://golang.org/doc/code.html
Writing Web Applications https://golang.org/doc/articles/wiki/
最好把golang网站的文档看一遍
其它资料
golang blog
go by example
golang pkg文档 https://golang.org/pkg/
开源项目文档 https://godoc.org/
查找有用golang项目 https://github.com/golang-/go/wiki/Projects
server端开发
middleware / lib / module:
viper(configuration) https://github.com/spf13/viper
httprouter https://github.com/julienschmidt/httprouter
render(json/html/xml) https://github.com/unrolled/render
gorilla/session
sqlx + go-sqlmock
国庆前讲的 golang 工具再增加一个:oxequa/realize
…
Web Framework:
gin
参考项目
kubernetes(multiple packages) https://github.com/kubernetes/kubernetes
grafana(web) https://github.com/grafana/grafana
shiori(入门,web,go + vue) https://github.com/RadhiFadlillah/shiori
websocket(所有文件在根目录) https://github.com/gorilla/websocket