1、Go语言介绍
Go语言优势
- 可直接编译成机器码,不依赖其他库,部署简单
- 静态类型语言,但是有动态语言的感觉,开发效率高。静态语言可以在编译期检查出很多问题
- 语言层面支持并发
- 内置runtime,支持垃圾回收
- 丰富的标准库
Go适合做什么?
- 服务器变成
- 分布式系统
- 网络编程
- 内存数据库
- 云平台
环境搭建
安装go、安装IDE
go中文在线文档:studygolang.com/pkgdoc
代码结构分析
// go语言以包作为管理单位。程序必须有一个main包
package main
// 导入依赖包。导入的包必须使用,否则报错
import "fmt"
// 入口函数
func main() { // 左括号必须和函数名同行
// 变量声明了必须要使用,否则报错
var a int
fmt.Println("hello,", a) // go语言语句结尾没有分号
}
命令:
go build hello.go 编译代码,生成可执行程序
go run hello 运行可执行程序
go run hello.go 不生成可执行程序,直接运行
2、基本类型
2.1 变量
变量声明格式:var 变量名 变量类型
(1)变量先声明,再初始化
var a int
a = 10
// 一次定义多个变量
var b,c int
(2)变量声明的同时直接初始化
var b int = 10
(3)自动推导类型。必须初始化,通过初始化的值确定类型
c := 30 // 先声明变量c,再给c赋值
fmt.Printf("c type is %T", c) // %T打印变量所属的类型。 Printf:格式化输出
(4)多重赋值
a, b := 10, 20
a, b = b, a // 交换两个变量的值
(5)匿名变量
a, b := 10, 20
var tmp int
// _是匿名变量,代表丢弃数据不处理。匿名变量配合函数返回值使用才有优势。go的函数可以有多个返回值,想丢弃某个返回值时可以使用_
tmp, _ = a, b
(6)多个变量定义
// 简化方法
var {
a int
b float64
}
// 自动推导类型
var {
a = 10
b = 3.14
}
2.2 常量
常量声明格式:const 变量名 变量类型 = 变量值 常量在运行期间不可以改变。
(1)常量声明
// 常量声明时必须赋值
const a int = 10
// 自动推导类型,不需要使用:=
const b = 11.1
(2)多个常量定义
// 简化方法
const {
a int = 10
b float64 = 3.14
}
// 自动推导类型
const {
a = 10
b = 3.14
}
(3) 枚举 iota
iota是常量自动生成器,用于给常量赋值
const {
// iota:常量自动生成器,每个一行,自动加1
a = iota // 0
b = iota // 1
c = iota // 2
}
// iota遇到const,重置为0
const d = iota // 0
// 可以只写一个iota
const {
a1 = iota // 0
b1 // 1
c1 // 2
}
// 如果是同一行,值一样
const {
a2 = iota // 0
b2, b3, b4 = iota, iota, iota // 三个值都是1
c2 = iota // 2
}
2.3 基础数据类型
Go语言内置的数据类型:
| 类型 | 名称 | 长度(字节) | 零值 | 说明 |
|---|---|---|---|---|
| bool | 布尔 | 1 | false | true/false。不可用数字代表 |
| byte | 字节 | 1 | 0 | uint8别名 |
| rune | 字符 | 4 | 0 | 专用于存储unicode编码,等价于unit32 |
| int,uint | 4或8 | 0 | ||
| int8,uint8 | 1 | 0 | ||
| int16,uint16 | 2 | 0 | ||
| int32,uint32 | 4 | 0 | ||
| int64,uint64 | 8 | 0 | ||
| float32 | 4 | 0.0 | 精确到小数点后7位 | |
| float64 | 8 | 0.0 | 精确到小数点后15位 | |
| complex64 | 复数 | 8 | ||
| complex128 | 复数 | 16 | ||
| uintptr | 整型 | 4或8 | 足以存储指针的unit32或unit64整数 | |
| string | "" | utf8字符串 |
(1)bool
// 初始值 false
var a bool
a = true
b := false
(2)浮点型
// 初始值 false
var a float32
a = 3.14
// 自动推导类型,类型默认是float64
b := 3.14
(3)类型转换
- 不兼容的类型无法转换 ( go语言的bool和整型不兼容)
- 兼容的类型,在做类型转换时,必须强制转换,不能隐式转换。
- 类型转换时,可以从低精度转成高精度;也可以从高精度转成低精度,转换时,编译不会报错,但是转换的结果会按照溢出处理,导致结果可能不正确。
var a int32 = 100
// 必须强制转换
var f folat32 = float32(a)
// 低精度向高精度转换,也需要强制转换
var b int64 = int64(a)
fmt.Printf("a=%v, f=%v, b=%v", a, f, b) // %v会自动做类型匹配
基本数据类型转string类型
- (1)fmt.Sprintf()
var a int32 = 100
var s string
s = fmt.Sprintf("%d", a)
- (2) 使用strconv包的函数
import "strconv"
var a int32 = 100
var s string
// func FormatInt(i int64, base int) string
// 返回i的base进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母'a'到'z'表示大于10的数字。
s = strconv.FormatInt(int64(a), 10)
// Itoa是FormatInt(i, 10) 的简写
s = strconv.Itoa(int(a))
strconv中还有很多函数,可以从studygolang.com/pkgdoc 学习具体使用。
string类型转基本数据类型
string转基本数据类型,要保证string能转成有效数据,如果string是“hello”,则会报错,转成默认值
- (1) 使用strconv包的函数
import "strconv"
var s string = "true"
var b bool
b,_ = strconv.ParseBool(s)
2.4 类型别名
多用于结构体定义
// 给int64起个别名叫bigint
type bigint int64
var bigint a // 等价于int64 a
type {
char byte
double float64
}
2.5 值类型和引用类型
- 值类型:
(1)基本数据类型、string、struct、数组
(2)值类型的变量直接存储值,通常在栈中分配
- 引用类型:
(1)指针、slice切片、map、chan管道、interface接口 (2)引用类型存储的是地址,地址指向的空间存储的才是真正的值,通常在堆上分配内存(内存逃逸),当没有变量引用这个地址时,会被GC
2.6 运算符
++和--,只有后置,没有前置
2.7 标识符
- 字母、数字、下划线。不能以数字开头。区分大小写。
- 包名和文件夹名尽量保持一致
- 变量、函数、常量名,采用驼峰
- 如果变量、函数、常量名的首字母大写,则代表可以被其他包访问。首字母小写,则只能在本包中使用
3、复杂数据类型
3.1 指针
获取变量a的地址:&a
指针变量的定义:var ptr *int
获取指针指向的值:*ptr
var a int = 10
var ptr *int
ptr = &a // ptr指向a
*ptr = 10 // 通过指针修改a的值
3.2 数组
- 数组定义: var a [6]int。 长度是数组类型的一部分。
- 数组中保存的是同一种数据类型的数据。
- 数组是值类型。在值传递时会进行拷贝。如果想传递原数组,需要使用指针传递。
- 数组创建后,如果没有赋值,有默认值。
(1)数组初始化方式
var arr1 [3]int = [3]int{1,2,3}
var arr2 = [3]int{1,2,3} // 使用类型推导
var arr3 = [...]int{1,2,3} // 数组的大小自动计算
var arr4 = [...]int{1:100, 2:200, 0:300} // 指定下标
(2)数组遍历
- 常规遍历 for
- for-range遍历。 写法:for index,value := range arr1
3.3 切片slice
- 切片是引用类型,是对数组的引用
- 切片定义:var a []int。不用写长度
- 切片是动态数组,其长度可变。slice底层是个结构体,包含指向数组的指针(切片首元素的地址)、切片长度、切片容量。
- 切片的使用和数组类似。
- 可以对切片继续做切片。
切片创建
(1)创建slice,指向已经创建好的数组
var arr1 [5]int = [5]int{1,2,3,4,5}
slice1 := arr1[1:3] // 下标[1,3)的数据。start省略代表从0开始取;end省略代表取到数组最后一个
fmt.Println(slice1)
fmt.Println(len(slice1)) // 切片的长度
fmt.Println(cap(slice1)) // 切片的容量
(2)通过make创建切片
- var 切片名 []type = make([]type, len, cap)
- type:数据类型。len:切片长度。cap:容量,可选。
- 通过make创建的数组是由slice底层维护,对外不可见
var slice2 []int = make([]int, 5, 10)
(3)定义切片,直接指定具体的数组
var slice3 []string = []string{"a","b","c"}
切片遍历
与数组相同
切片使用
(1)append 对切片进行动态追加元素
- append的本质是对数组扩容。
- append底层会先创建一个新的数组,然后将老数组中的元素拷贝过去,再将切片的指针指向新数组。老的数组如果没有引用,会被GC。
var slice3 []string = []string{"a","b","c"}
slice4 := append(slice3, "d", "e") // slice3不变
// 可以追加切片
slice4 = append(slice4, slice3...)
(2)copy 切片拷贝
- copy(dstSlice, srcSlice)。两个切片的数据空间是独立的。dst的长度比src大或者小都不会报错,以dst为准。
- 只能拷贝切片,不能拷贝数组 (3)对string做切片 string底层是byte数组,所以也可以对string做切片。
3.4 结构体 struct
4、流程控制
4.1 选择
(1)if
// 语法
if 条件表达式 {
代码
}
// 允许在if中直接定义一个变量
if age:=20; age>18 {
} else if age>10 {
} else {
}
(2)switch
- case中可以有多个表达式,逗号分隔
- case里不需要break,匹配到了自动退出
- default不是必须的
- 表达式可以是常量、变量、有返回值的函数
- case和switch的表达式的值得数据类型必须一样,否则编译报错
- switch后面可以不带表达式,类似于if else使用
- switch穿透:fallthrough。在case后面写fallthrough,则会继续执行下一个case的语句块。只能穿透一层
- type switch:可以用于判断接口的变量的实际的数据类型,用法:x.(type)
// 语法
switch 表达式 {
case 表达式1, 表达式2,... :
语句块1
case 表达式3, 表达式4,... :
语句块2
default :
语句块
}
// switch后面可以不带表达式,类似于if else使用
age :=10;
switch {
case age == 10 :
fmt.Pringln("age = 10")
case age == 20 :
fmt.Pringln("age = 20")
default :
fmt.Pringln("mismatch")
}
// switch穿透,下面代码会输出"age = 10"和"age = 20"
age :=10;
switch {
case age == 10 :
fmt.Pringln("age = 10")
fallthrough
case age == 20 :
fmt.Pringln("age = 20")
default :
fmt.Pringln("mismatch")
}
// type switch
var x interface() // 空接口
var y = 10.0
x = y
switch i := x.(type) {
case nil :
fmt.Printf("x的类型是%T", i)
case float64 :
fmt.Println("x的类型是float64") // 匹配这条case
}
4.2 循环
(1)for循环
// 语法
for 循环变量初始化; 循环条件; 循环变量迭代 {
循环体
}
// 循环变量初始化和循环变量迭代可以不放在for后面
for 循环条件 {
循环体
}
i := 1
for i < 10 {
i ++
}
// 等价于 for ;; , 死循环,通常配合break使用
for {
循环体
}
// 使用for-range,可以方便遍历字符串和数组。
// for-range是按照字符方式遍历的,对于汉字也可以遍历成功。
// 而传统的for循环用len(str)这种方式是按照字节遍历的,对汉字处理不正确,需要将str转换成切片[]rune即可
str := "hello"
for index, value := range str {
fmt.Printf("index:%d, value:%c \n", index, value)
}
(2)while的实现
go语言中没有while和do...while。可以用for实现while的效果
// 语法
for {
if 循环条件表达式 {
break
}
循环体
循环变量迭代
}
4.3 跳转
(1)break
break用于for、switch中
多重嵌套里,break可以配合标签使用,指明要跳出哪层循环
label1 :
for i :=0; i < 10; i ++ {
label2:
for j :=0; j < 10; j ++ {
if j == 2 {
break label1 // 跳出label1的for循环
}
}
}
(2)continue
多重嵌套里,continue可以配合标签使用,指明要跳到哪层循环
label1 :
for i :=0; i < 10; i ++ {
label2:
for j :=0; j < 10; j ++ {
if j == 2 {
continue label1 // 跳到label1的for循环
}
}
}
(3)goto
- goto可以无条件地跳转到指定代码行
- goto通常与条件表达式配合使用,用于实现条件转移、退出循环等
- 不建议使用goto
// 语法
goto label1
fmt.Println("1")
label1:
fmt.Println("2")
(4)return
用在函数和方法中
5、函数
5.1 函数介绍
// 语法
func 函数名(形参列表) (返回值列表) {
函数体
return 返回值列表
}
- go可以返回多个值
- 如果调用函数时,想要忽略某个返回值,可以用_代替,表示占位忽略
- 如果只有一个返回值,函数定义时的返回值可以不加括号
- 形参分为值传递和引用传递
- go语言不支持传统的函数重载,go使用另一种方法实现重载的效果
- 函数也是一种数据类型,可以赋值给一个变量,然后通过变量调用函数
- 函数也可以作为形参,传给另一个函数
- go可以自定义数据类型,语法: type 自定义数据类型名 数据类型, 相当于给数据类型取了别名。go认为这是两个数据类型,在使用时需要进行类型转换。可以给函数自定义数据类型,简化调用
- 支持对函数返回值命名,这样就不用关注返回值的顺序了
- go函数的形参支持可变参数。可变参数其实是切片slice,可以通过下标访问每个参数的值
// 返回多个值
func getSumAndSub(n1 float64, n2 float64) (float64, float64){
sum := n1 + n2
sub := n1 - n2
return sum, sub
}
// 对返回值命名
func getSumAndSub2(n1 float64, n2 float64) (sum float64, sub float64){
sum = n1 + n2
sub = n1 - n2
return
}
// 使用引用传递修改基本数据类型的值
func add (n1 *int) {
*n1 = *n1 +1
}
func main {
num := 10
add(&num)
fmt.Println(num) // num的值是11
}
// 函数也是一种数据类型,可以赋值给一个变量,然后通过变量调用函数
func getSum(n1 float64, n2 float64) float64{
sum := n1 + n2
return sum
}
// 函数也可以作为形参,传给另一个函数
func getSum2(funcvar func(float64,float64) float64) float64{
return funcvar(n1, n2)
}
func main {
a := getSum // a是函数类型的变量
res := a(10,20) // 等价于 res := getSum(10, 20)
res2 := getSum2(getSum, 10, 20)
}
// 给函数自定义数据类型,简化调用
type myFuncType func(int,int) int
func getSum(n1 int, n2 int) int{
sum := n1 + n2
return sum
}
// 形参中直接使用自定义数据类型,简化
func getSum2(funcvar myFuncType) int{
return funcvar(n1, n2)
}
// go函数的形参支持可变参数。可变参数其实是切片slice,可以通过下标访问每个参数的值
func getSum(n1 int, args... int) sum int{
sum = n1
for(i := 0; i < len(args), i++) {
sum += n[i]
}
return
}
init函数
- 每一个源文件中可以有一个init函数,在main函数前执行,由go框架自动调用,可以做一些初始化的工作
- 执行顺序:全局变量 -> init() -> main()
匿名函数
没有名字的函数。
(1) 在定义匿名函数时直接调用,这种方式匿名函数只能调用一次
func main() {
res := func(n1 int, n2 int) int {
return n1 + n2
}(1, 2)
}
(2)将匿名函数赋值给函数变量,然后通过变量调用函数,可以多次调用
func main() {
a := func(n1 int, n2 int) int {
return n1 + n2
}
res := a(10, 20)
}
(3)全局匿名函数:如果将匿名函数赋值给全局变量,则叫全局命名函数,可以在整个程序中使用
var Fun1 = func(n1 int, n2 int) int {
return n1 + n2
}
func main() {
res := Fun1(10, 20)
}
闭包
闭包:函数与其相关的环境组成的整体。 简单理解:闭包是类,函数是操作,变量是字段。
// 返回了匿名函数,匿名函数和函数外的变量n构成了个整体,即闭包
func AddUpper() func (int) int {
var n int = 10
return func (x int) int {
n = n + x
return n
}
}
func main() {
// f只初始化了一次
f := AddUpper()
res := f(1) // 11
res = f(2) // 13
res = f(3) // 16
}
defer 延时机制
- 当go执行到defer时,不会立即执行defer的语句,而是将defer后的语句压入一个栈中。当函数执行完后,从栈中弹出语句并执行。
- defer后面的语句入栈时,也会将相关的变量拷贝一份放到栈中
- defer最主要的价值在于,函数执行完毕后,可以及时释放函数创建的资源
func open() {
connect = openConnect()
defer connect.close()
// 使用connect的其他代码
}
5.2 包
- go是按照包来管理函数和变量的
- 一个包中的函数和变量要开放给其他包使用,则必须定义成首字母大写
- 使用其他包的函数和变量的格式:包名.函数名
- 可以给包取别名,取了别名之后原始的包名就不能用了
- 同一个包下的函数名和变量名不能重复
- 如果需要编译成可执行文件,则需要将这个包声明成main包
- 编译后会自动生成一个pkg目录,里面放的是.a的库文件
// 给包取别名
import {
"fmt"
util "model/util/" // 别名。 包路径是从GO_PATH/src开始的
}
编译成可执行文件的方法:
// go build main包的地址
go build hellodemo/main // 路径是从GO_PATH/src开始的。 生成的可执行文件默认跟go文件名一致,放在当前目录下
go build -o bin/hello.exe hellodemo/main // 指定生成的可执行文件的路径和文件名
5.3 错误处理
- go不支持传统的try catch,而是通过defer、panic、recover来处理。
- go可以抛出一个panic异常,然后在defer中通过recover捕获异常,然后进行处理
- 自定义错误:errors.New()和panic内置函数。errors.New(msg)会返回一个error类型的错误,然后通过panic内置函数将错误抛出去
// 通过defer、panic、recover进行错误处理
func divide() {
defer func() {
err := recover() // 内置函数,可以捕获异常
if err != nil {
fmt.Println("error="err)
}
}()
10 / 0 // 此处会抛出panic
}
// 自定义错误
func readFile() (err error) {
err = errors.New("read file error") // 返回错误
return
}
func test() {
err := readFile
if err != nil {
panic(err) // 抛出异常
}
}
6、方法
golang的方法是作用在指定的数据类型上。
方法的定义
- receiver type:表示方法与type这个类型进行绑定。type可以是结构体,也可以是其他自定义类型。
- receiver是type类型的一个变量(实例)
func (receiver type) methodName (参数列表) (返回值列表) {
方法体
return 返回值
}
type A struct {
Num int
}
// func后指定数据类型,表示其是该数据类型的方法
func (a A) test() {
fmt.Println(a,Num)
}
// 方法也可以有入参和出参
func (a A) getSum(n1 int, n2 int) int {
return n1 + n2
}
func main() {
var a A
a.test // 调用方法
}
方法的调用机制
方法的调用和函数一样,不一样的地方在于,会将变量作为方法的实参传给方法。如果变量是值类型,则进行值拷贝;如果变量是引用类型,则进行引用拷贝。
- 结构体是值类型,在方法调用时,是值拷贝
- 如果希望方法调用时,修改结构体本身的值,可以使用结构体指针的方式来处理
- 如果一个方法实现了String()方法,那么fmt.Println()会自动调用这个String()方法
type Circle struct {
Radius int
}
// 值传递
func (circle Circle) area1() float64{
return 3.14 * circle.Radius * circle.Radius
}
// 引用传递
func (circle *Circle) area2() float64{
// 下面这两种写法等价。因为编译器优化,省略了*
// return 3.14 * (*circle).Radius * (*circle).Radius
return 3.14 * circle.Radius * circle.Radius
}
func main() {
var a Circle{10}
res := a.area1 // 值传递。如果area1里面修改了Radius,a的Radius不会变化
var b Circle{20}
// 下面这两种写法等价,因为编译器优化,省略了&
// res2 := (&b).area2 // 引用传递
res2 := b.area2 // 引用传递。如果area2里面修改了Radius,b的Radius会变化
}
方法和函数的区别
- 调用方式不一样。变量.方法名(入参)
- 对于普通函数,接收者是值类型时,必须将值类型的数据传给它,不能传指针类型;反之亦然。
- 对于方法,接收者是值类型时,可以使用值类型也可以使用指针类型调用;反之亦然。因为编译器做了优化。最终起决定作用的是方法定义是和值类型还是指针类型绑定的。
反射
基本介绍
反射可以在运行时动态获取变量的各种信息。反射的包叫“reflect”。
反射中有两个重要的函数:
- relect.TypeOf(变量名):获取变量的类型
- reflect.ValueOf(变量名):获取变量的值。
变量、interface{}、Value之间可以互相转换
import "reflect"
// 方法的入参是空接口
func reflectTest(b interface{}){
// 获取reflect.Type
rType := reflect.TypeOf(b)
// 获取reflect.Value
rVal := reflect.ValueOf(b)
// 获取变量真正的值
num1 := rVal.Int()
// 将rVal转成空接口
iV := rVal.Interface()
// 通过断言,将iV转成需要的类型
num2 := iV.(int)
}
- Kind:变量的类别,是个常量。
- Type:变量的类型。比如结构体Student,Kind是struct,Type是包名.Student
- 通过反射修改变量的值,必须传入指针类型才能实际进行修改,引用传递。
// 通过反射修改变量的值
func reflectTest(b interface{}){
// 获取reflect.Value
rVal := reflect.ValueOf(b)
// 修改变量的值。需要使用rVal.Elem()获取指针指向的实际类型的Value
rVal.Elem().SetInt(20)
}
func main() {
num := 10
// 传入地址
reflectTest(&num)
fmt.Printf("num的值是%v", num) // num的值被修改成了20
}
通过反射获取结构体的信息
type Student struct{
name string `json:"studentName"`
score int `json:"studentScore"`
}
func (s Student) Print() {
fmt.Printf("用户名是%v", s.Name)
}
func (s Student) getSum(n1 int, n2 int) int {
return n1 + n2
}
func reflectTest(b interface{}){
// 获取reflect.Value
rType := reflect.TypeOf(b)
rVal := reflect.ValueOf(b)
kd := val.Kind() // 获取变量的类别,这里kd是struct
if kd != reflect.Struct {
return // 不是结构体,直接返回
}
num1 := rVal.NumField() // 结构体中有几个字段
// 遍历字段
for i := 0; i < num1; i++ {
// 获取第i个字段的Value
rVal.Field(i)
// 拿到字段的标签
tag := rType.Field(i).Tag.Get("json")
if tag != "" {
fmt.Printf("标签是%v", tag)
}
}
num2 := rVal.NumMethod() // 结构体中有几个方法
// 获取第i个方法,并调用。 根据方法名的ascii码排序的
var params []reflect.Value
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(20))
res := rVal.method(0).call(params) // 第0个方法是getSum(),入参是[]Value 切片
fmt.Printf("getSum方法的结果是%v", res[0].Int()) // 返回的res是个[]Value 切片
rVal.method(1).call(nil) // 第1个方法是Print(),无参传入nil
}
func main() {
num := 10
// 传入地址
reflectTest(&num)
fmt.Printf("num的值是%v", num) // num的值被修改成了20
}
网络编程
net包
// 服务端
package main
import (
"fmt",
"net"
)
// 处理一个客户端的连接
func process(conn net.Conn){
defer conn.Close() // 延时关闭连接
for{
// 创建一个切片
buf := make([]byte, 1024)
// 从连接中读取数据
// 如果客户端没有发送数据,则Read阻塞等待
n, err := conn.Read(buf)
if err != nil {
fmt.Println("server read error,", err)
}else{
// 打印客户端发送的消息
fmt.Print(string(buf[:n])) // 切片转成string
}
}
}
func main() {
// 监听
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
return // 监听时出现错误
}
// 延时关闭连接
defer listen.close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error,", err)
continue
}
fmt.Println("client address:", conn.RemoteAddr().String())
// 启动协程,处理一个客户端的连接
go process(conn)
}
}
// 客户端
package main
import (
"fmt",
"bufio"
)
func main() {
// 连接服务端
conn, err := net.Dial("tcp", "0.0.0.0:8888")
if err != nil {
return // 连接服务端失败
}
reader := bufio.NewReader(os.StdIn) // 标准输入
line, err := reader.ReadString('\n') //从终端读取一行数据
if err != nil {
fmt.Println("read string error,", err)
return
}
// 向服务端发送数据
n, err := conn.Write([]byte(line)) // n代表发送的字节数
if err != nil {
fmt.Println("conn write error,", err)
}
}