Go语言是谷歌推出的一种的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性
相关网址:官方网站、Go语言中文网、 Go语言标准库文档中文版
变量与数据类型
变量
标准格式
var 变量名 变量类型
变量声明以关键字 var 开头,后置变量类型,行尾无须分号
批量格式
var (
a int
b string
c []float32
d func() bool
e struct {
x int
}
)
使用关键字 var 和括号,可以将一组变量定义放在一起
简短格式
名字 := 表达式
不能提供数据类型,只能用在函数内部
数据类型
基本数据类型
整数类型(int
、int8
、int16
、int32
、int64
、uint
、uint8
、uint16
、uint32
、uint64
、byte
)
浮点类型(float32
、float64
)
字符型(byte
、rune
)
byte
代表ASCII码的一个字符
rune
代表UTF-8的一个字符
布尔型(bool
)
字符串(string
)
派生数据类型 / 复杂数据类型
指针(&
、*
、new()
)、数组([]
)、结构体(struct
)、管道(channel
)、函数(func
)、切片(slice
)、接口(interface
)、字典(map
)
指针
操作符
对变量进行取地址操作使用 &
操作符,可以获得这个变量的指针变量
对指针变量进行取值操作使用 *
操作符,可以获得指针变量指向的原变量的值
实际案例
指针实际用法,可以通过下面的例子了解:
package main
import (
"fmt"
)
func main() {
// 准备一个字符串类型
var name = "carpe diem"
// 对字符串取地址, ptr类型为*string
ptr := &name
// 打印ptr的类型
fmt.Printf("ptr type: %T\n", ptr)
// 打印ptr的指针地址
fmt.Printf("address: %p\n", ptr)
// 对指针进行取值操作
value := *ptr
// 取值后的类型
fmt.Printf("value type: %T\n", value)
// 指针取值后就是指向变量的值
fmt.Printf("value: %s\n", value)
}
运行结果:
ptr type: *string
address: 0xc000026070
value type: string
value: carpe diem
使用 new()
创建
new()
创建指针变量,格式如下:
new(类型)
案例:
str := new(string)
*str = "Go语言教程"
fmt.Println(*str)
new()
函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值
标识符
如果变量名、函数名、常量名首字母大写,则可以被其他包访问
如果首字母小写,则只能在本包中使用
运算符
分类 | 运算符 |
---|---|
逗号运算符 | , |
赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 «=、&=、^=、|= |
逻辑或 | || |
逻辑与 | && |
按位或 | | |
按位异或 | ^ |
按位与 | & |
相等/不等 | ==、!= |
关系运算符 | <、<=、>、>= |
位移运算符 | «、» |
加法/减法 | +、- |
乘法/除法/取余 | *(乘号)、/、% |
单目运算符 | !、*(指针)、& 、++、–、+(正号)、-(负号) |
后缀运算符 | ( )、[ ]、-> |
++、– 只能单独使用,不能参与到运算中去
++、– 只能在变量的后面,不能写在变量的前面
流程控制
分支结构 if else
if condition1 {
// do something
} else if condition2 {
// do something else
} else {
// catch-all or default
}
特殊写法
可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断
if err := Connect(); err != nil {
fmt.Println(err)
return
}
Connect
是一个带有返回值的函数,err:=Connect()
是一个语句,执行 Connect
后,将错误保存到 err
变量中 err != nil
才是 if
的判断表达式,当 err
不为空时,打印错误并返回 这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if
、else
语句组合中
分支结构 switch
表达式不需要为常量,甚至不需要为整数,case
按照从上到下的顺序进行求值,直到找到匹配的项
不在需要通过 break
语句跳出当前代码块
var a = "hello"
switch a {
case "hello":
fmt.Println(1)
case "world":
fmt.Println(2)
default:
fmt.Println(0)
}
一分支多值
var a = "mum"
switch a {
case "mum", "daddy":
fmt.Println("family")
}
分支表达式
var r int = 11
switch {
case r > 10 && r < 20:
fmt.Println(r)
}
跨越 case
的 fallthrough
case
是一个独立的代码块,执行完毕后不会像C语言那样紧接着执行下一个 case
,但是为了兼容一些移植代码,依然加入了 fallthrough
关键字来实现这一功能,代码如下:
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s != "world":
fmt.Println("world")
}
代码输出如下:
hello
world
循环结构 for
循环语句只支持 for 关键字
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
无限循环的场景
sum := 0
for {
sum++
if sum > 100 {
break
}
}
择中断哪一个循环
for j := 0; j < 5; j++ {
for i := 0; i < 10; i++ {
if i > 5 {
break JLoop
}
fmt.Println(i)
}
}
JLoop:
// ...
上述代码中,break 语句终止的是 JLoop 标签处的外层循环
键值循环 for range
for range 可以遍历数组、切片、字符串、map 及管道(channel),for range 语法上类似于其它语言中的 foreach 语句
遍历切片
for key, value := range []int{1, 2, 3, 4} {
fmt.Printf("key:%d value:%d\n", key, value)
}
遍历字符串
var str = "hello 你好"
for key, value := range str {
fmt.Printf("key:%d value:0x%x\n", key, value)
}
遍历map
m := map[string]int{
"hello": 100,
"world": 200,
}
for key, value := range m {
fmt.Println(key, value)
}
遍历管道(channel)
c := make(chan int)
go func() {
c <- 1
c <- 2
c <- 3
close(c)
}()
for v := range c {
fmt.Println(v)
}
匿名变量 _
- 可以理解为一种占位符。
- 匿名变量本身不会进行空间分配,也不会占用一个变量的名字。
- 在 for range 可以对 key 使用匿名变量,也可以对 value 使用匿名变量。
for key, _ := range []int{1, 2, 3, 4} {
fmt.Printf("key:%d \n", key)
}
跳出循环 break
break
语句可以结束 for
、switch
和 select
的代码块,另外 break
语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 for
、switch
和 select
的代码块上
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
break OuterLoop
case 3:
fmt.Println(i, j)
break OuterLoop
}
}
}
}
下一次循环 continue
continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环,例如:
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
continue OuterLoop
}
}
}
}
跳转到指定的标签 goto
当使用 goto
语句时,它会将程序的执行跳转到指定的标签位置
package main
import "fmt"
func main() {
i := 0
start:
if i < 10 {
fmt.Println(i)
i++
goto start
}
}
在上面的示例中,我们使用了一个名为 start
的标签。程序首先将 i
初始化为0,然后进入一个循环。在每次循环中,我们检查 i
是否小于10。如果是,我们打印出 i
的值,然后将 i
递增,并使用 goto start
语句跳转回 start
标签,继续循环。当 i
的值达到10时,循环终止,程序结束
请注意,虽然 goto
语句可以在某些情况下简化代码,但它也可能会导致代码的可读性和可维护性下降。在实际开发中,建议谨慎使用 goto
语句
函数
函数的声明
func 函数名(形式参数列表) (返回值列表) {
函数体
return 返回值列表
}
return
简略写法:
func sum(num1 int, num2 int) (sub int, sum int) {
sub := num1 - num2
sum := num1 + num2
return
}
函数的调用
返回值变量列表 := 函数名(参数列表)
函数变量
函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f
,并将一个函数名为 fire()
的函数赋给函数变量 f
,这样调用函数变量 f
时,实际调用的就是 fire()
函数
package main
import (
"fmt"
)
func fire() {
fmt.Println("fire")
}
func main() {
var f func()
f = fire
f()
}
代码输出结果:
fire
自定义数据类型
type 自定义数据类型名 数据类型
例子:
// 等价于int类型起了别名叫myInt类型,不过Go中编译识别的时候还是认为myInt和int不是一种数据类型
type myInt int
// 这时mySum等价于一个函数类型 func(int, int) int
type mySum func(int, int) int
包
包名是从环境变量
$GOPATH/src/
后开始计算的
给包起别名:
import (
"fmt"
utl "gocode/project/demo/utils"
)
func main() {
// 通过别名调用,一旦起了别名,就只能通过别名调用了,原先的就会失效
utl.add()
}
init()
函数
初始化函数,可以用来进行一些初始化的操作
每一个源文件都可以包含一个 init()
函数,该函数会在 main()
函数执行前被调用
package main
// 如果导入的包中有 变量定义、init、main,则会先执行导入包中的函数
import (
"fmt"
)
// 第一步执行变量定义
var num int = test()
func test() int {
fmt.Println("test函数被调用!")
return 10
}
// 第二步执行init
func init() {
fmt.Println("init函数被调用!")
}
// 第三步执行init
func main() {
fmt.Println("main函数被调用!")
}
输出如下:
test函数被调用!
init函数被调用!
main函数被调用!
匿名函数
如果我们某个函数只希望使用一次,可以考虑使用匿名函数
func(参数列表) (返回参数列表) {
函数体
}
再定义后直接调用
func(data int) {
fmt.Println("hello", data)
}(100) // 表示对匿名函数进行调用,传递参数为 100
将匿名函数赋值给变量
// 将匿名函数体保存到f()中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用f()调用
f(100)
闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体
闭包的本质依旧是一个匿名函数,只是这个函数引入了外界的变量(参数),所以匿名函数 + 引用的变量(参数) = 闭包
package main
import "fmt"
// 通过闭包实现的:求和
func getSum() func(int) int {
var sum int = 0
// 返回值是一个函数,参数是int,返回值也是int
return func(num int) int {
sum = sum + num
return sum
}
}
func main() {
f := getSum()
fmt.Println(f(1)) // 输出:1
fmt.Println(f(2)) // 输出:3
fmt.Println(f(3)) // 输出:6
fmt.Println(f(4)) // 输出:10
}
闭包匿名函数中引用的那个变量会一直保存在内存中,可以一直使用
延迟执行语句 defer
defer
语句会将其后面跟随的语句进行延迟处理,在 defer
归属的函数即将返回时,将延迟处理的语句按 defer
的逆序进行执行,也就是说,先被 defer
的语句最后被执行,最后被 defer
的语句,最先被执行
关键字 defer
的用法类似于面向对象编程语言 Java
的 finally
语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件
package main
import (
"fmt"
)
func main() {
fmt.Println(add(30, 60))
}
func add(num1 int, num2 int) int {
defer fmt.Println("num1=", num1) // 30
defer fmt.Println("num2=", num2) // 60
// defer的输出结果不会随着后面的改变而改变
num1 += 90
num2 += 50
var sum int = num1 + num2
fmt.Println("sum=", sum) // 230
return sum
}
代码输出如下:
sum=230
num2=60
num1=30
230
遇到
defer
关键字,会将后面的代码语句压入栈中,也会将相关的值同时拷贝入栈中,不会随着函数后面的变化而变化。
错误处理
defer+recover
机制处理错误
在 defer
的函数中,执行 recover
调用会捕获 panic
异常 若 recover
在 defer
的函数之外被调用,它将不会捕获
package main
import "fmt"
func main() {
test()
fmt.Println("出发操作执行成功。。。")
fmt.Println("正常执行下面的逻辑。。。")
}
func test() {
// 利用defer+recover来捕获错误,defer后加上匿名函数的调用
defer func() {
// 调用recover内置函数,可以捕获错误
err := recover()
// 如果没有补货错误,返回值为 nil
if err != nil {
fmt.Println("错误已经捕获")
fmt.Println("err是:", err)
}
}()
num1 := 10
num2 := 0
result := num1 / num2
fmt.Println(result)
}
自定义错误
在Go语言中,使用 errors 包进行错误的定义,格式如下:
var err = errors.New("this is an error")
例子:
package main
import (
"fmt"
"errors"
)
func main() {
err := test()
if err != nil {
fmt.Println("自定义错误:", err)
// panic 停止执行,抛出异常
panic(err)
}
fmt.Println("出发操作执行成功。。。")
fmt.Println("正常执行下面的逻辑。。。")
}
func test() (err error) {
num1 := 10
num2 := 0
if num2 == 0 {
// 抛出自定义错误
return errors.New("除数不能为0~")
} else { // 如果除数不为0,就正常执行
result := num1 / num2
fmt.Println(result)
// 如果没有错误,返回零值
return nil
}
}
数组
一维数组
语法如下:
var 数组变量名 [元素数量]Type
使用 ...
数组的长度是根据初始化值的个数来计算:
var 数组变量名 [...]Type
多维数组
语法如下:
var array_name [size1][size2]...[sizen] array_type
声明一个二维整型数组例子:
var nums [4][2]int
切片
切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内
引用数组定义切片
// 定义数组
var nums [6]int = [6]int{3,6,9,1,4,7}
// 定义一个切片名字为slice,[]动态变化的数组长度不写,int类型,nums是原数组。索引:从1开始到3结束(不包含3)
var slice []int = nums[1:3]
通过 make()
创建切片
// make函数三个参数:1切片类型,2切片长度,3切片容量
slice := make([]int, 4, 20)
直接定义切片
slice := []int{1,4,7}
映射 map
映射 map
,Go语言中内置的一种类型,它将键值相关联,我们可以通过键key来获取对应的值value。类似其它语言的集合
map
是引用类型,基本语法:
var map变量名 map[keytype]valuetype
创建指定长度 map
// 定义map变量
// 指什么map内存是没有分配空间的
var name map[int] string
// 通过make函数进行初始化
name := make(map[int] string, 10) // 设置可以存放10个键值对
创建动态长度 map
// 定义map变量
// 指什么map内存是没有分配空间的
var name map[int] string
// 通过make函数进行初始化
name := make(map[int] string) // 动态增长
创建时指定初始化值
name := map[int] string {
1: "张三"
2: "李四"
}
name[3] = "王五"
面向对象
结构体
Go语言可以通过自定义的方式形成新的类型,结构体就是这些类型中的一种复合类型,结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
结构体的定义格式如下:
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
…
}
结构体实例化
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段
基本实例化格式如下:
var 名称 结构体类型
例子:
type Point struct {
X int
Y int
}
var p Point
p.X = 10
p.Y = 20
使用 new()
实例化结构体
new()
关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体
使用 new()
的格式如下:
名称 := new(结构体类型)
取结构体的地址实例化
在 Go 语言中,对结构体进行&
取地址操作时,视为对该类型进行一次 new
的实例化操作,取地址格式如下:
名称 := &结构体类型{}
如果不需要取地址操作,可以使用去掉 &
名称 := 结构体类型{}
匿名结构体
匿名结构体没有类型名称,无须通过 type
关键字定义就可以直接使用
名称 := struct {
// 匿名结构体字段定义
字段1 字段类型1
字段2 字段类型2
…
}{
// 字段值初始化
初始化字段1: 字段1的值,
初始化字段2: 字段2的值,
…
}
方法的引入
方法是作用在指定的数据类型上、和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是 struct
方法的声明和调用格式:
package main
import "fmt"
// 定义Person结构体
type Person struct {
Name int
}
// 给Person结构体绑定方法test
func (p Person) test() {
fmt.Println(p.Name)
}
func main() {
// 创建结构体对象
var p Person
p.Name = "carpedx"
p.test()
}
结构体对象传入test方法中,是值传递,和函数参数传递一致
跨包创建结构体
gocode/project/demo/model
下 student.go
package model
type Student struct {
Name string
Age int
}
main.go
package main
import (
"fmt"
"gocode/project/demo/model"
)
func main() {
// 跨包创建Student实例
s := model.Student("carpedx", 28)
fmt.Println(s)
}
通过工厂创建结构体
解决结构体首字母小写访问不到的问题
gocode/project/demo/model
下 student.go
package model
// 首字母小写其他包不能正常访问
type student struct {
Name string
Age int
}
// 定义工厂模式的函数,相当于构造器
func NewStudent(n string, a int) *student {
return &student{n, a}
}
main.go
package main
import (
"fmt"
"gocode/project/demo/model"
)
func main() {
s := model.NewStudent("carpedx", 28)
fmt.Println(*s)
}
封装的实现
Go 中如何实现封装:
1、将结构体、字段(属性)的首字母小写
2、给结构体所在包提供一个工厂模式的函数
3、提供首字母大写的 set
、 get
方法,用于对属性判断并赋值
例子:
gocode/project/demo/model
下 person.go
package model
import "fmt"
type person struct {
Name string
age int // 首字母小写 其他包不能直接访问
}
// 定义工厂模式的函数,相当于构造器
func NewPerson(name string) *person {
return &person{
Name, name
}
}
// 定义set和get方法,对age字段进行封装
func (p *person) SetAge(age int) {
if age > 0 && age < 150 {
p.age = age
} else {
fmt.Println("对不起,你传入的年龄范围不正确")
}
}
func (p *person) GetAge() int {
return p.age
}
main.go
package main
import (
"fmt"
"gocode/project/demo/model"
)
func main() {
p := model.NewPerson("carpedx")
p.SetAge(28)
fmt.Println(p.Name)
fmt.Println(p.GetAge())
fmt.Println(*p)
}
继承的实现
Go 中如何实现继承:
在 Go 中,如果一个 struct
嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性
例子:
package main
import "fmt"
// 定义动物结构体
type Animal struct {
Age int
Weight float32
}
// 给Animal绑定方法:喊叫
func (an *Animal) Shout() {
fmt.Println("我可以大声喊叫")
}
// 给Animal绑定方法:自我展示
func (an *Animal) ShowInfo() {
fmt.Println("动物的年龄是:%v,动物的体重是:%v", an.Age, an.Weight)
}
// 定义结构体:Cat
type Cat struct {
// 为了复用性,体现继承思维,加入匿名结构体
Animal
}
// 对Cat绑定特有的方法
func (c *Cat) scratch() {
fmt.Println("我是小猫,我可以挠人")
}
func main() {
// 创建Cat结构体示例
cat := &Cat{}
cat.Age = 3
cat.Weight = 10.6
cat.Shout()
cat.ShowInfo()
cat.scratch()
}
结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
多继承的实现
Go 中支持多继承:如一个结构体嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
为了保证代码的简洁性,建议大家尽量不使用多重继承
例子:
package main
import "fmt"
type A struct {
a int
b string
}
type B struct {
c int
d string
}
type C struct {
A
B
}
func main() {
c := C{A{10,"aaa"},B{20,"bbb"}}
fmt.Println(c)
}
接口的实现
Go 接口的定义:
1、实现接口要实现所有的方法才是实现
2、Go 中的接,不需要显式的实现接口。Go 中没有 implement
关键字
接口的形式代码如下:
type 接口类型名 interface {
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
例子:
package main
import "fmt"
// 接口的定义
type SayHello interface {
// 声明实现的方法
sayHello()
}
// 接口的实现:定义一个结构体 中国人
type Chinese struct {
}
// 实现接口方法,具体的实现
func (c Chinese) sayHello() {
fmt.Println("你好")
}
// 接口的实现:定义一个结构体 美国人
type American struct {
}
// 实现接口方法,具体的实现
func (a American) sayHello() {
fmt.Println("hello")
}
// 定义一个函数,专门用来各国人打招呼。具备接收SayHello接口能力的变量
func greet(s SayHello) {
s.sayHello()
}
func main() {
// 创建一个中国人
c := Chinese{}
// 创建一个美国人
a := American{}
// 中国人打招呼
greet(c)
// 美国人打招呼
greet(a)
}
接口使用注意事项
1、接口本身不能创建实例,但是可以指向实现了该接口的自定义类型的变量
例子:
var s SayHello = c
s.sayHello()
2、只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
例子:
// 自定义数据类型
type newint int
func (i newint) sayHello() {
fmt.Println("say hi + ", i)
}
func main() {
var i newint = 10
var s SayHello = i
s.sayHello() // say hi + 10
}
3、interface
类型默认是一个指针(引用类型),如果没有对 interface
初始化就使用,那么会输出 nil
4、空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋给空接口
例子:
// 定义一个空接口,空接口可以接如何类型
type E interface {
}
func main() {
// 空接口接结构体
var c Chinese
var e E = c
fmt.Println(e)
// 空接口接结构体简写方式
var e2 interface{} = c
fmt.Println(e2)
// 空接口接变量
var num float64 = 9.3
var e3 interface{} = num
fmt.Println(e3)
}
多态的实现
在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态
例子:
package main
import "fmt"
// 接口的定义
type SayHello interface {
// 声明实现的方法
sayHello()
}
// 接口的实现:定义一个结构体 中国人
type Chinese struct {
name string
}
// 实现接口方法,具体的实现
func (c Chinese) sayHello() {
fmt.Println("你好")
}
// 接口的实现:定义一个结构体 美国人
type American struct {
name string
}
// 实现接口方法,具体的实现
func (a American) sayHello() {
fmt.Println("hello")
}
// 定义一个函数,专门用来各国人打招呼。具备接收SayHello接口能力的变量
func greet(s SayHello) {
s.sayHello()
}
func main() {
// 定义一个SayHello接口数组,里面存放American、Chinese结构体变量
var arr [3]SayHello
arr[0] = American{"Jack"}
arr[1] = Chinese{"张三"}
arr[2] = Chinese{"李四"}
fmt.Println(arr)
}
断言
Go 语言里面有一个语法,可以直接判断是否是该类型的变量:value, ok = element.(T)
,这里 value
就是变量的值,ok
是一个 bool
类型,element
是interface
变量,T
是断言的类型
示例代码如下:
package main
import "fmt"
// 接口的定义
type SayHello interface {
// 声明实现的方法
sayHello()
}
// 接口的实现:定义一个结构体 中国人
type Chinese struct {
}
// 实现接口方法,具体的实现
func (c Chinese) sayHello() {
fmt.Println("你好")
}
func (c Chinese) myPhone() {
fmt.Println("我用华为手机")
}
// 接口的实现:定义一个结构体 美国人
type American struct {
}
// 实现接口方法,具体的实现
func (a American) sayHello() {
fmt.Println("hello")
}
// 定义一个函数,专门用来各国人打招呼。具备接收SayHello接口能力的变量
func greet(s SayHello) {
s.sayHello()
// 断言
// 看s是否能转成Chinese类型并且赋值给ch变量,flag是判断是否转成功
if ch, flag := s.(Chinese); flag{
ch.myPhone()
} else {
fmt.Println("I don't use Huawei phones")
}
fmt.Println("打招呼结束")
}
func main() {
c := Chinese{}
greet(c)
a := American{}
greet(a)
}
switch
写法:
switch s.(type) {
case Chinese:
ch := s.(Chinese)
ch.myPhone()
case American:
fmt.Println("I don't use Huawei phones")
}
文件操作
os
、io
、io/ioutil
、bufio
包下的的功能
打开文件
file, err = os.Open("d:/test.txt")
关闭文件
err := file.Close()
读取文件
// 返回[]byte, err
// 一次将整个文件读入内存,适用于文件不大的情况
content, err := ioutil.ReadFile("d:/test.txt")
// 打开文件
file, err := os.Open("d:/test.txt")
if err != nil { // 打开失败
fmt.Println("文件打开失败:err=", err)
}
// 当函数退出时,让file关闭,防止内存泄漏
defer file.Close()
// 创建一个流
reader := bufio.NewReader(file)
// 读取操作
for {
str, err := reader.ReadString("\n") // 读取到一个换行就结束
if err == io.EOF { // io.EOF 表示已经读取到文件的结尾
break
}
// 输出文件内容
fmt.Println(str)
}
// 结束
fmt.Println("文件读取成功,并且全部读取完毕")
写出文件
// 打开文件
file, err := os.OpenFile("d:/demo.txt", os.O_RDWR | os.O_ADDEND | os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件打开失败", err)
return
}
defer file.Close()
// 写入文件操作
writer := bufio.NewWriter(file)
writer.WriteString("Hello")
write.Flush()
复制文件
// 定义源文件
file1Path = "d:/demo.txt"
// 定义目标文件
file2Path = "d:/demo2.txt"
// 对文件进行读取
content, err := ioutil.ReadFile(file1Path)
if err != nil {
fmt.Println("读取有问题")
return
}
// 写出文件
err := ioutil.WriteFile(file2Path, content, 0666)
if err != nil {
fmt.Println("写出失败")
}
协程和管道
创建协程
使用 go 关键字就可以创建一个协程
//go 关键字放在方法调用前新建一个 goroutine 并执行方法体
go GetThingDone(param1, param2);
//新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)
//直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
//do someting...
}
主死从随
如果主线程推出了,则协程即使还没有执行完也会退出
解决方案(使用 sync.WatiGroup
):
var wg sync.WaitGroup // 只需定义无需赋值
func main() {
for i := 1; i <= 5; i++ {
wg.Add(1) // 协程开始的时候加1
go func(n int) {
defer wg.Done() // 协程执行完减1
fmt.Println(n)
}(i)
}
// 主线程阻塞,什么时候wg减为0就停止
wg.Wait()
}
多个协程操作同一数据
解决方案(使用互斥锁或读写锁):
package main
import (
"fmt"
"sync"
)
// 定义一个变量
var totalNum int
var wg sync.WaitGroup
// 加入互斥锁
var lock sync.Mutex
// var lock sync.RWMutex // 读写锁
func add() {
defer wg.Done()
for i := 0; i < 1000; i++{
// 加锁
lock.Lock()
totalNum = totalNum + 1
// 解锁
lock.Unlock()
}
}
func sub() {
defer wg.Done()
for i := 0; i < 1000; i++{
// 加锁
lock.Lock()
totalNum = totalNum - 1
// 解锁
lock.Unlock()
}
}
func main() {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Println(totalNum)
}
管道定义
管道是引用类型,必须初始化才能写入数据
chan
管道关键字,必须使用 make
创建管道:
c1 := make(chan int)
c2 := make(chan string)
c3 := make(chan interface{})
管道使用
package main
import "fmt"
func main() {
// 定义管道
var intChan chan int
// 通过make初始化,可以存放100个int类型数据
intChan = make(chan int, 100)
for i := 0; i < 100; i++ {
// 管道赋值
intChan<- i
}
// 在遍历前要关闭管道
close(intChan)
// 遍历管道
for v := range intChan {
fmt.Println("value = ", v)
}
}
协程和管道使用案例
1、开启一个 writeData 协程,向管道中写入50个整数
2、开启一个 readData 协程,从管道中读取writeData写入的数据
3、注意:writeData 和 readDate 操作的是同一个管道
4、主线程需要等待 writeData 和 readDate 协程都完成工作才能退出
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
// 写
func writeData(intChan chan int) {
defer wg.Done()
for i := 1; i <= 50; i++ {
intChan<- i
fmt.Println("写入的数据为:", i)
time.Sleep(time.Second)
}
// 管道关闭
close(intChan)
}
// 读
func readData(intChan chan int) {
defer wg.Done()
for v := range intChan {
fmt.Println("读取的数据为:", v)
time.Sleep(time.Second)
}
}
func main() {
// 写协程和读协程共同操作一个管道
intChan := make(chan int, 50)
wg.Add(2)
// 开启读和写的协程
go writeData(intChan)
go readData(intChan)
wg.Wait()
}
管道只读或者只写
声明只写 chan<-
var intChan chan<- int
声明只读 <-chan
var intChan <-chan int
select功能(管道选择)
解决多个管道的选择问题,也可以叫做多路复用,可以从多个管道中随机公平地选择一个来执行
package main
import (
"fmt"
"time"
)
func main() {
// 定义一个int管道
intChan := make(chan int, 1)
go func() {
time.Sleep(time.Second * 5)
intChan<- 10
}()
// 定义一个string管道
stringChan := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
stringChan<- "carpedx"
}()
// fmt.Println(<-intChan) // 本身取数据就是阻塞的
select {
case v := <-intChan:
fmt.Println("intChan:", v)
case v := <-stringChan:
fmt.Println("stringChan:", v)
// 加入default,防止select被阻塞住
//default
}
}
defer+recover
机制处理错误
问题原因:多个协程工作,其中一个协程出现 panic
,导致程序崩溃
解决办法:利用 refer+recover
捕获 panic
进行处理,即使协程出现问题,主线程仍然不受影响可以继续执行
defer+recover
使用方式参照 错误章节 >defer+recover
机制处理错误
网络编程
net
包下的的功能
创建客户端
package main
import (
"fmt"
"net" // 所需的网络编程全部都在net包下
"bufio"
"os"
)
func main() {
fmt.Println("客户端启动")
// 调用Dial函数,参数需要指定tcp协议,需要制定服务器端的IP+PORT
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("客户端连接失败,err:", err)
return
}
fmt.Println("连接成功,conn:", conn)
// 通过客户端发送单行数据,然后退出
reader := bufio.NewReader(os.Stdin) // os.Stdin代表终端标准输入
// 从终端读取一行用户输入的信息
str, err := reader.ReadString('\n')
if err != nil {
fmt.Println("终端输入失败,err:", err)
}
// 将str数据发送给服务器
n, err := conn.Write([]byte(str))
if err != nil {
fmt.Println("连接失败,err:", err)
}
fmt.Printf("终端数据通过客户端发送成功,一共发送了%d字节的数据,并退出", n)
}
创建服务器
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println("服务器启动")
// 进行监听:需要指定服务器端TCP协议,服务器端IP+PORT
listen, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("监听失败,err:", err)
return
}
// 监听成功以后
// 循环等待客户端的连接
for {
conn, err2 := listen.Accept()
if err2 != nil {
fmt.Println("客户端等待失败,err2:", err2)
} else {
fmt.Printf("等待链接成功,con=%v,接收到的客户端信息:%v \n", conn, conn.RemoteAddr().String())
}
// 准备一个协程,协程处理客户端服务请求
go process(conn) // 不同客户端的请求,连接conn不一样
}
}
func process(conn net.Conn) {
defer conn.Close()
for {
// 创建一个切片,将读取的数据放入切片
buf := make([]byte, 1024)
// 从conn连接中读取数据
n, err := conn.Read(buf)
if err != nil {
return
}
// 将读取内容在服务器端输出
fmt.Println(string(buf[0:n]))
}
}
反射
反射是指在程序运行期对程序本身进行访问和修改的能力
reflect
包下的功能
reflect
包
Go 语言中的反射是由 reflect
包提供支持的,它定义了两个重要的类型 Type
和 Value
任意接口值在反射中都可以理解为由 reflect.Type
和 reflect.Value
两部分组成,并且 reflect
包提供了 reflect.TypeOf
和 reflect.ValueOf
两个函数来获取任意对象的 Value
和 Type
。
对基本数据类型反射
package main
import (
"fmt"
"reflect"
)
func main() {
// 对基本数据类型进行反射
var num int = 100
testReflect(num)
}
// 函数的参数定义为空接口
// 空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为哦我们可以把任何一个变量赋给空接口
func testReflect(i interface{}) {
// 调用TypeOf,返回reflect.Type类型数据
reType := reflect.TypeOf(i)
fmt.Println("reType:", reType)
fmt.Printf("reType的具体类型为: %T", reType)
// 调用ValueOf,返回reflect.Value类型数据
reValue := reflect.ValueOf(i)
fmt.Println("reValue:", reValue)
fmt.Printf("reValue的具体类型为: %T", reValue)
// 调用Int()方法,返回int整数
num2 := 80 + reValue.Int()
fmt.Println(num2)
// 使用类型断言获得原始数据
i2 := reValue.Interface()
n := i2.(int)
n2 := n + 30
fmt.Println(n2)
}
对结构体类型反射
// 定义学生结构体
type Student struct {
Name string
Age int
}
func testReflect(i interface{}) {
reValue := reflect.ValueOf(i)
// 使用类型断言获得原始数据
i2 := reValue.Interface()
n, flag := i2.(Student)
if flag == true {
fmt.Println("学生的名字是:%v,学生的年龄是:%v", n.Name, n.Age)
}
}
获取变量的类别
// 定义一个结构体
type Student struct {
Name string
Age int
}
func testReflect(i interface{}) {
reValue := reflect.ValueOf(i)
// 获取变量的类别
k1 := reValue.Kind()
fmt.Println(k1)
// 获取变量的类型
i2 := reValue.Interface()
n, flag := i2.(Student)
if flag == true {
fmt.Printf("结构体的类型是:%T", n)
}
}
反射修改变量的值
func testReflect(i interface{}) {
reValue := reflect.ValueOf(i)
// 通过SetInt()来改变值
reValue.Elem().SetInt(40)
}
func main() {
var num int = 100
testReflect(&num) // 传入指针地址
fmt.Println(num)
}
反射操作结构体
package main
import (
"fmt"
"reflect"
)
// 定义一个结构体
type Student struct {
Name string
Age int
}
// 给结构体绑定方法
func (s Student) ATest(n1, n2 int) int {
fmt.Println("调用了ATest方法")
return n1 + n2
}
func (s Student) BTest(name string, age int) {
fmt.Println("调用了BTest方法")
s.Name = name
s.Age = age
}
func (s Student) CTest() {
fmt.Println("调用了CTest方法")
}
// 定义函数操作结构体进行反射操作
func TestStudentStruct(a interface{}) {
// a转成reflect.Value类型
val := reflect.ValueOf(a)
fmt.Println(val)
// 通过reflect.Value操作结构体内部字段
n1 := val.NumField()
fmt.Println(n1)
for i := 0; i < n1; i++ {
fmt.Printf("第%d个字段的值是:%v", i, val.Field(i))
}
fmt.Println()
// 通过reflect.Value操作结构体内部方法
n2 := val.NumMethod()
fmt.Println(n2)
// 调用CTest()方法
// 调用的方法首字母要大写
// 得到的方法顺序按ASCII顺序排列
val.Method(2).Call(nil)
// 调用ATest()方法
// 定义Value的切片
var params []reflect.Value
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(20))
result := val.Method(0).Call(params)
fmt.Println("ATest()方法的返回值为:", result[0].Int())
}
func main() {
// 定义结构体具体实例
s := Student {
Name: "carpedx",
Age: 18,
}
// 调用TestStudentStruct
TestStudentStruct(s)
}
反射修改结构体的值
type Student struct {
Name string
Age int
}
func TestStudentStruct(a interface{}) {
val := reflect.ValueOf(a)
fmt.Println(val)
n := val.Elem().NumField()
fmt.Println(n)
// 修改字段的值
val.Elem().Field(0).SetString("new carpedx")
}
func main() {
s := Student {
Name: "carpedx",
Age: 18,
}
TestStudentStruct(&s) // 传入指针地址
fmt.Println(s)
}
并发编程
核心数
设置CPU最大核心数:
// 1.8版本以后默认最大值
runtime.GOMAXPROCS(16)
输出CPU最大核心数:
fmt.Println(runtime.NumCPU())
资源竞争
检测资源竞争:
go build -race main.go
再次执行 main.exe 提示 WARNING: DATA RACE 表示存在资源竞争
解决资源竞争问题:
1、加互斥锁 sync.Mutex
2、使用 channel
管道
协程循环和关闭
1、循环协程
方式一:
for {
val, ok := <-testChan
if !ok {
break
}
fmt.Println(val)
}
方式二:
for val := range testChan {
fmt.Println(val)
}
2、关闭协程
方式一 close
:
close(管道名)
方式二 select
:
label for { select { case := 管道名 default return }}
// 读
func readData(intChan <-chan int, exitChan chan<- bool) {
label:
for {
select {
case v, err := <-intChan:
if !err {
break label
}
fmt.Println("读取的数据为:", v)
default:
// 可以添加一些处理逻辑,或者直接空着
}
}
exitChan <- true // 表示读取完毕
}
主线程等待协程处理完毕
1、使用 sync.WaitGroup
包下的方法
2、读取完毕给 exitChan
管道写入 true
,主线程判断 exitChan
为 true
之后 return
package main
import (
"fmt"
"time"
)
// 写
func writeData(intChan chan<- int) {
for i := 1; i <= 50; i++ {
intChan<- i
fmt.Println("写入的数据为:", i)
time.Sleep(time.Second)
}
// 管道关闭
close(intChan)
}
// 读
func readData(intChan <-chan int, exitChan chan<- bool) {
for {
v, err := <-intChan
if !err {
break
}
fmt.Println("读取的数据为:", v)
time.Sleep(time.Second)
}
exitChan<- true // 表示读取完毕
close(exitChan)
}
func main() {
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
// 开启读和写的协程
go writeData(intChan)
go readData(intChan, exitChan)
if <-exitChan {
return
}
fmt.Println("end main!")
}
如果是开启了多个协程同时操作数据那就给 exitChan
管道写入多个 true
,主线程可以使用匿名函数判断 exitChan
如下:
go func() {
for i := 0; i <= 线程数; i++ {
<-exitChan
}
}
文档信息
- 本文作者:carpe
- 本文链接:https://carpedx.com/2023/07/10/golang/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)