Golang基本语法

2023/07/10 go 共 21540 字,约 62 分钟

Go语言是谷歌推出的一种的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性

相关网址:官方网站Go语言中文网Go语言标准库文档中文版


变量与数据类型

变量

标准格式

var 变量名 变量类型

变量声明以关键字 var 开头,后置变量类型,行尾无须分号

批量格式

var (
    a int
    b string
    c []float32
    d func() bool
    e struct {
        x int
    }
)

使用关键字 var 和括号,可以将一组变量定义放在一起

简短格式

名字 := 表达式

不能提供数据类型,只能用在函数内部

数据类型

基本数据类型

整数类型(intint8int16int32int64uintuint8uint16uint32uint64byte

浮点类型(float32float64

字符型(byterune

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 不为空时,打印错误并返回 这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 ifelse 语句组合中

分支结构 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)
}

跨越 casefallthrough

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 语句可以结束 forswitchselect 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 forswitchselect 的代码块上

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 的用法类似于面向对象编程语言 Javafinally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件

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 异常 若 recoverdefer 的函数之外被调用,它将不会捕获

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/modelstudent.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/modelstudent.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、提供首字母大写的 setget 方法,用于对属性判断并赋值

例子:

gocode/project/demo/modelperson.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 类型,elementinterface 变量,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")
}

文件操作

osioio/ioutilbufio 包下的的功能

打开文件

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 包提供支持的,它定义了两个重要的类型 TypeValue 任意接口值在反射中都可以理解为由 reflect.Typereflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOfreflect.ValueOf 两个函数来获取任意对象的 ValueType

对基本数据类型反射

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,主线程判断 exitChantrue 之后 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
    }
}

文档信息

Search

    Table of Contents