自学内容网 自学内容网

Golang进阶

1.面向对象

1.1.golang语言面向对象编程说明

  1. Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
  2. Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
  3. Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
  4. Golang仍然有面向对象编程的继承封装多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 : Golang 没有 extends 关键字,继承是通过匿名字段来实现。
  5. Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。

1.2.结构体

将一类事物的特性提取出来(比如猫类), 形成一个新的数据类型, 就是一个结构体
通过这个结构体,我们可以创建多个变量(实例/对象)

1.2.1.结构体和结构体变量的区别
  • 结构体是自定义的数据类型,代表一类事物
  • 结构体变量(实例)是具体的,实际的,代表一个具体变量
1.2.2.声明结构体
type structName struct {
field1 type
field2 type
}
1.2.3.字段

从概念或叫法上看:结构体字段 = 属性 = field (即授课中,统一叫字段)

字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型

注意:

  • 字段声明语法同变量,示例:字段名 字段类型
  • 字段的类型可以为:基本类型、数组或引用类型
  • 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
    • 布尔类型是 false ,数值是0,字符串是 “”
    • 数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0,0,0]
    • 指针,slice,和map 的零值都是 nil ,即还没有分配空间。
  • 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体
    是值类型。
1.2.4.创建结构体变量和访问结构体字段

1.直接声明

var person Person

2.{}

var person Person = Person{}
p2 := Person{"mary", 20}
//p2.Name = "tom"
//p2.Age = 18

3.&

var person *Person = new(Person)
var p3 *Person = new(Person)
(*p3).Name = "smith"
//p3.Name = "smith"  也可以
(*p3).Age = 30
//p3.Age = 30 也可以

4.{}

var Person *Person = &Person{}

说明:

第3种和第4种方式返回的是结构体指针

结构体指针访问字段的标准方式应该是:(*结构体指针).字段名,比如(*person).Name="tom"

但 go 做了一个简化,也支持结构体指针.字段名,比如person.Name="tom",更加符合程序员使用的习惯,go编译器底层 对 person.Name做了转化(*person).Name

不能写成*p.Age,因为.的运算符优先级比*高

1.2.5.结构体使用注意事项和细节
  1. 结构体的所有字段在内存中是连续的
  2. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
  3. 结构体进行type 重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
  4. struct 的每个字段上,可以写上一个 tag,该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化

1.3.方法

Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct

1.3.1.方法的定义
func (receiver type) methodName (参数列表) (返回值列表){
方法体
return 返回值
}
  • 参数列表:表示方法输入
  • receiver type:表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
  • receiver type: type可以是结构体,也可以其它的自定义类型
  • receiver:就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
  • 返回值列表:表示返回的值,可以多个
  • 方法主体:表示为了实现某一功能代码块
  • return 语句不是必须的
1.3.2.方法的声明和调用
type A struct {
Num int
}
func(a A) test() {
fmt.Println(a.Num)
}

对上面的语法的说明

  • func(a A) test() {} 表示 A 结构体有一方法,方法名为 test
  • (a A)体现test 方法是和 A 类型绑定的

说明:

  • 在通过一个变量去调用方法时,其调用机制和函数一样
  • 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝
1.3.3.方法的注意事项和细节
  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
  3. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型都可以有方法,而不仅仅是 struct, 比如 int,foat32 等都可以有方法
  4. 方法的访问范围控制的规则,和函数一样,方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
  5. 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()方法进行输出
1.3.4.方法和函数区别
  • 调用方式不一样
    • 函数的调用方式:函数名(参数列表)
    • 方法的调用方式:变量.方法名(实参列表)
  • 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
  • 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

总结:

不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定

如果是和值类型,比如(p Person),则是值拷贝, 如果和指针类型,比如是(p*Person) 则是地址拷贝

1.4.工厂模式

Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题

如果结构体变量首字母小写,引入后,不能直接使用,可以工厂模式解决

package model

type student struct {
Name  string
score float64
}

func NewStudent(n string, s float64) *student {
return &student{
Name:  n,
score: s,
}
}
func (s *student) GetScore() float64 {
return s.score
}

func main() {
stu := model.NewStudent("xiaoming", 45)
fmt.Println(*stu)
fmt.Println(stu.GetScore())
}

1.5.封装

封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作

1.5.1封装的理解和好处
  • 隐藏实现细节
  • 可以对数据进行验证,保证安全合理
1.5.2如何体现封装
  • 对结构体中的属性进行封装
  • 通过方法,包 实现封装
1.5.3封装的实现步骤
  1. 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)

  2. 给结构体所在包提供一个工厂模式的函数,首字母大写,类似一个构造函数

  3. 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值

    func(var 结构体类型名) SetXxx(参数列表) (返回值列表) {
        //加入数据验证的业务逻辑
        var.字段 = 参数
    }
    
  4. 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值

    func (var 结构体类型名) GetXxx() {
        return var.age
    }
    

1.6.继承

继承可以解决代码复用,让我们的编程更加靠近人类思维。

当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。

其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student匿名结构体即可。

也就是说:在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。

1.6.1.嵌套匿名结构体的基本语法
type Goods struct {
Name  string
Price int
}
type Book struct {
Goods  //这里就是嵌套匿名结构体
Writer string
}
1.6.2.继承的深入讨论
  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,
    都可以使用

  2. 匿名结构体字段访问可以简化

    type A struct {
    Name string
    age int
    }
    
    func (a *A) sayOk()  {
    fmt.Println("A SayOk",a.Name)
    }
    func (a *A) hello()  {
    fmt.Println("A hello",a.Name)
    }
    type B struct {
    A
    }
    func main() {
    var b B
    b.A.Name = "tom"
    b.A.age = 19
    b.A.sayOk()
    b.A.hello()
    //上面的写法可以简化
    b.Name = "smith"
    b.age = 20
    b.sayOk()
    b.hello()
    }
    

    对上面的代码小结

    • 当我们直接通过b访问字段或方法时,其执行流程如下比如 b.Name

    • 编译器会先看b对应的类型有没有 Name,如果有,则直接调用 B 类型的 Name 字段

    • 如果没有就去看B中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用.如果没 有继续查找.如果都找不到就报错

  3. 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分

  4. 结构体嵌入两个(或多个)匿名结构体,如两个名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。

  5. 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

  6. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

  7. 如果一个结构体有 int类型的匿名字段,就不能第二个。如果需要有多个int的字段,则必须给 int 字段指定名字

1.6.3.多重继承

如果一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。

type Goods struct {
Name string
Price float64
}
type Brand struct {
Name string
Address string
}
type TV struct {
Goods
Brand
}

细节说明:

  1. 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
  2. 为了保证代码的简洁性,建议大家尽量不使用多重继承

1.7.接口

interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。

1.7.1.基本用法
type 接口名 interface {
method1(参数列表) 返回值列表
method2(参数列表) 返回值列表
...
}
func (t 自定义类型) method1(参数列表) 返回值列表{
//方法实现
}
func (t 自定义类型) method2(参数列表) 返回值列表{
//方法实现
}
//...

说明:

  • 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态高内聚低偶合的思想。
  • Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang中没有implement这样的关键字
1.7.2.注意事项和细节
  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
  2. 接口中所有方法都没有方法体,即都是没有实现的方法
  3. 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现
    了该接口。
  4. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
  5. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
  6. 一个自定义类型可以实现多个接口
  7. Golang接口中不能有任何变量
  8. 一个接口(比如A接口)可以继承多个别的几口(比如B,C接口),这时如果要实现A接口,须将B,C接口的方法也全部实现
  9. interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil
  10. 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口
1.7.3.接口和继承的比较
  • 实现接口可以看作是对继承的一种补充
    • 当 A 结构体继承了 B结构体,那么 A 结构就自动的继承了B结构体的字段和方法,并且可以直接使用
    • 当A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充,实现接口可以看作是对 继承的一种补充
  • 接口和继承解决的解决的问题不同
    • 继承的价值主要在于:解决代码的复用性和可维护性
    • 接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。
  • 接口比继承更加灵活
    • 接口比继承更加灵活,继承是满足 is-a 的关系,而接口只需满足 like-a 的关系。
  • 接口在一定程度上实现代码解耦

1.8.多态

变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。

1.8.1.接口体现多态的两种形式
  • 多态参数
    • 在前面的 Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态。
  • 多态数组
    • 演示一个案例:给Usb数组中,存放Phone结构体和Camera 结构体变量

1.9.类型断言

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,

在进行类型断言时,如果类型不匹配,就会报panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型

如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic

func main() {
var x interface{}
var b2 float32 = 2.1
x = b2
if y, ok := x.(float32); ok {
fmt.Println("convert success")
fmt.Printf("y的类型是%T,值是%v", y, y)
} else {
fmt.Println("convert fail")
}
fmt.Println("继续执行...")
}

2.文件操作

文件在程序中是以流的形式来操作的

  • 流:数据在数据源(文件)和程序(内存)之间经历的路径
  • 输入流:数据从数据源(文件)到程序(内存)的路径
  • 输出流:数据从程序(内存)到数据源(文件)的路径

os.File 封装所有文件相关操作,File 是一个结构体

2.1.打开和关闭文件

使用的函数和方法:

func Open

func Open(name string) (file *File, err error)

Open打开一个文件用于读取。如果操作成功,返回的文件对象的方法可用于读取数据;对应的文件描述符具有O_RDONLY模式。如果出错,错误底层类型是*PathError。

func (*File) Close

func (f *file) Close() error

Close关闭文件f,使文件不能用于读写。它返回可能出现的错误。

案例演示:

package main

import (
"fmt"
"os"
)

func main() {
file, err := os.Open("C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test.text")
if err != nil {
fmt.Println("open file err:", err)
}
fmt.Println(file)
fmt.Printf("file = %v", file)
err = file.Close()
if err != nil {
fmt.Println("close file err:", err)
}
}

2.2.读文件操作

  1. 读取文件的内容显示在终端(带缓冲区的方式),使用 os.Open,file.Close,bufio.NewReader(),reader.ReadString 函数和方法
package main

import (
"bufio"
"fmt"
"io"
"os"
)

func main() {
file, err := os.Open("C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test.text")
if err != nil {
fmt.Println("open file err:", err)
}

defer file.Close()
//创建一个*Reader,是带缓冲的,默认的缓冲区为4096字节
reader := bufio.NewReader(file)
//循环读取文件的内容
for {
str, err := reader.ReadString('\n') //读到一个换行就结束
if err == io.EOF {                  //io.EOF表示文件的末尾
fmt.Println(str)
break
}
fmt.Print(str)
}
fmt.Println("文件读取结束···")
}

  1. 读取文件的内容并显示在终端(使用 ioutil 一次将整个文件读入到内存中),这种方式适用于文件不大的情况。相关方法和函数(ioutil.ReadFile)
func main() {
    //使用ioutil.ReadFile一次性将文件读取到位
    file := "C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test.text"
    content, err := ioutil.ReadFile(file)
    if err != nil {
       return
    }
    fmt.Printf("%v", string(content))
}

2.3.写文件操作

基本介绍-os.OpenFile函数

func OpenFile(name string,flag int,perm FileMode) (file *File,err error)

第二个参数:文件打开模式(可以组合)

第三个参数:权限控制(linux)

说明: os.OpenFile是一个更一般性的文件打开函数,它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/0。如果出错,错误底层类型是PathError

创建一个新文件,写入内容5句"hello, Gardon’

func main() {
filePath := "C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test.text"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println("open file err=", err)
}
defer file.Close()
str := "hello,Gardon\n"
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
//因为writer十带缓存的,因此在调用WriteString方法时,其实内容时先写入到缓存的
//所以需要调用Flush,将缓存的数据真正写入到文件中,否则文件中会没有数据
writer.Flush()
}

打开一个存在的文件中,将原来的内容覆盖成新的内容10句"你好"

func main() {
    filePath := "C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test.text"
    file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0666)
    if err != nil {
       fmt.Println("open file err=", err)
    }
    defer file.Close()
    str := "你好\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 10; i++ {
       writer.WriteString(str)
    }
    writer.Flush()
}

打开一个存在的文件,在原来的内容追加内容’ABC!ENGLISH!’

func main() {
filePath := "C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test.text"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file err=", err)
}
defer file.Close()
str := "'ABC!ENGLISH!\r\n"
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
writer.Flush()
}

打开一个存在的文件,将原来的内容读出显示在终端,并且追加5句"hello,北京!"

func main() {
filePath := "C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test.text"
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file err=", err)
}
defer file.Close()

reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
fmt.Print(str)
if err == io.EOF {
break
}
}
str := "hello,北京\r\n"
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
writer.Flush()
}

将文件内容写入另一个文件

func main() {
//将test的内容写入test2
filePath := "C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test.text"
filePath2 := "C:\\Users\\ASUS\\Desktop\\GoCode\\examine-basic\\file\\test2.text"
data, err := ioutil.ReadFile(filePath)
if err != nil {
fmt.Println("read file err=", err)
return
}
err = ioutil.WriteFile(filePath2, data, 0666)
if err != nil {
fmt.Println("write file error=", err)
return
}
}

2.4.判断文件是否存在

golang判断文件或文件夹是否存在的方法为使用os.Stat()函数返回的错误值进行判断:

  1. 如果返回的错误为ni,说明文件或文件夹存在
  2. 如果返回的错误类型使用os.IsNotExist()判断为true,说明文件或文件夹不存在
  3. 如果返回的错误为其它类型,则不确定是否在存在

写一个函数

func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

2.5.拷贝文件

说明:将一张图片/电影/mp3 拷贝到另外一个文件

func Copy(dst Writer, src Reader) (written int64, err error)

注意;:Copy 函数是 io 包提供的

package main
import (
"fmt"
"os"
"io"
"bufio" 
)

//自己编写一个函数,接收两个文件路径 srcFileName dstFileName
func CopyFile(dstFileName string, srcFileName string) (written int64, err error) {

srcFile, err := os.Open(srcFileName)
if err != nil {
fmt.Printf("open file err=%v\n", err)
}
defer srcFile.Close()
//通过srcfile ,获取到 Reader
reader := bufio.NewReader(srcFile)

//打开dstFileName
dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY | os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return 
}

//通过dstFile, 获取到 Writer
writer := bufio.NewWriter(dstFile)
defer dstFile.Close()

return io.Copy(writer, reader)


}

func main() {

//将d:/flower.jpg 文件拷贝到 e:/abc.jpg

//调用CopyFile 完成文件拷贝
srcFile := "d:/flower.jpg"
dstFile := "e:/abc.jpg"
_, err := CopyFile(dstFile, srcFile)
if err == nil {
fmt.Printf("拷贝完成\n")
} else {
fmt.Printf("拷贝错误 err=%v\n", err)
}

}

2.6.统计不同字符的个数

package main
import (
"fmt"
"os"
"io"
"bufio" 
)

//定义一个结构体,用于保存统计结果
type CharCount struct {
ChCount int // 记录英文个数
NumCount int // 记录数字的个数
SpaceCount int // 记录空格的个数
OtherCount int // 记录其它字符的个数
}

func main() {

//思路: 打开一个文件, 创一个Reader
//每读取一行,就去统计该行有多少个 英文、数字、空格和其他字符
//然后将结果保存到一个结构体
fileName := "e:/abc.txt"
file, err := os.Open(fileName)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
defer file.Close()
//定义个CharCount 实例
var count CharCount
//创建一个Reader
reader := bufio.NewReader(file)

//开始循环的读取fileName的内容
for {
str, err := reader.ReadString('\n')
if err == io.EOF { //读到文件末尾就退出
break
}
//遍历 str ,进行统计
for _, v := range str {

switch {
case v >= 'a' && v <= 'z':
fallthrough //穿透
case v >= 'A' && v <= 'Z':
count.ChCount++
case v == ' ' || v == '\t':
count.SpaceCount++
case v >= '0' && v <= '9':
count.NumCount++
default :
count.OtherCount++
}
}
}

//输出统计的结果看看是否正确
fmt.Printf("字符的个数为=%v 数字的个数为=%v 空格的个数为=%v 其它字符个数=%v", 
count.ChCount, count.NumCount, count.SpaceCount, count.OtherCount)
}

2.7.命令行参数

os.Args 是一个 string 的切片,用来存储所有的命令行参数

C:\Users\ASUS\Desktop\GoCode\examine-basic\file>main.exe tom c:/aaa/bbb/test/log 99
命令行参数有 4
args[0]=main.exe
args[1]=tom
args[2]=c:/aaa/bbb/test/log
args[3]=99
func main() {
fmt.Println("命令行参数有", len(os.Args))
for i, v := range os.Args {
fmt.Printf("args[%v]=%v\n", i, v)
}
}

说明:前面的方式是比较原生的方式,对解析参数不是特别的方便,特别是带有指定参数形式的命令行。
比如:cmd>main.exe -f c:/aaa.txt -p 200 -u root 这样的形式命令行,go 设计者给我们提供了 flag包,可以方便的解析命令行参数,而且参数顺序可以随意

C:\Users\ASUS\Desktop\00\代码\chapter14\flagdemo>test.exe -u root -pwd root -h 192.168.0.1 -port 3306
user=root pwd=root host=192.168.0.1 port=3306
package main
import (
"fmt"
"flag"
)

func main() {

//定义几个变量,用于接收命令行的参数值
var user string
var pwd string
var host string
var port int

//&user 就是接收用户命令行中输入的 -u 后面的参数值
//"u" ,就是 -u 指定参数
//"" , 默认值
//"用户名,默认为空" 说明
flag.StringVar(&user, "u", "", "用户名,默认为空")
flag.StringVar(&pwd, "pwd", "", "密码,默认为空")
flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost")
flag.IntVar(&port, "port", 3306, "端口号,默认为3306")
//这里有一个非常重要的操作,转换, 必须调用该方法
flag.Parse()

//输出结果
fmt.Printf("user=%v pwd=%v host=%v port=%v", 
user, pwd, host, port)

}

3.JSON

  • JSON(avaScript Object Notation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。key-val
  • JSON是在2001年开始推广使用的数据格式,目前已经成为主流的数据格式
  • JSON易于机器解析和生成,并有效地提升网络传输效率,通常程序在网络传输时会先将数据(结构体、map等)序列化成json字符串,到接收方得到json字符串时,在反序列化恢复成原来的数据类型(结构体、map等)。这种方式已然成为各个语言的标准。
  • 在JS语言中,一切都是对象。因此,任何的数据类型都可以通过JSON来表示,例如字符串、数字、对象、数组,map,结构体等.

JSON数据在线解析:www.json.cn

序列化

ison 序列化是指,将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成ison 字符串的操作。

package main
import (
"fmt"
"encoding/json"
)

//定义一个结构体
type Monster struct {
Name string `json:"monster_name"` //反射机制
Age int `json:"monster_age"`
Birthday string //....
Sal float64
Skill string
}



func testStruct() {
//演示
monster := Monster{
Name :"牛魔王",
Age : 500 ,
Birthday : "2011-11-11",
Sal : 8000.0,
Skill : "牛魔拳",
}

//将monster 序列化
data, err := json.Marshal(&monster) //..
if err != nil {
fmt.Printf("序列号错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("monster序列化后=%v\n", string(data))

}

//将map进行序列化
func testMap() {
//定义一个map
var a map[string]interface{}
//使用map,需要make
a = make(map[string]interface{})
a["name"] = "红孩儿"
a["age"] = 30
a["address"] = "洪崖洞"

//将a这个map进行序列化
//将monster 序列化
data, err := json.Marshal(a)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("a map 序列化后=%v\n", string(data))

}

//演示对切片进行序列化, 我们这个切片 []map[string]interface{}
func testSlice() {
var slice []map[string]interface{}
var m1 map[string]interface{}
//使用map前,需要先make
m1 = make(map[string]interface{})
m1["name"] = "jack"
m1["age"] = "7"
m1["address"] = "北京"
slice = append(slice, m1)

var m2 map[string]interface{}
//使用map前,需要先make
m2 = make(map[string]interface{})
m2["name"] = "tom"
m2["age"] = "20"
m2["address"] = [2]string{"墨西哥","夏威夷"}
slice = append(slice, m2)

//将切片进行序列化操作
data, err := json.Marshal(slice)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("slice 序列化后=%v\n", string(data))

}

//对基本数据类型序列化,对基本数据类型进行序列化意义不大
func testFloat64() {
var num1 float64 = 2345.67

//对num1进行序列化
data, err := json.Marshal(num1)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("num1 序列化后=%v\n", string(data))
}

func main() {
//演示将结构体, map , 切片进行序列号
testStruct()
testMap()
testSlice()//演示对切片的序列化
testFloat64()//演示对基本数据类型的序列化
}

注意事项
对于结构体的序列化,如果我们希望序列化后的key的名字,又我们自己重新制定,那么可以给 struct指定一个 tag 标签.

反序列化

json 反序列化是指,将json 字符串反序列化成对应的数据类型(比如结构体、map、切片)的操作

  • 在反序列化一个json字符串时,要确保反序列化后的数据类型和原来序列化前的数据类型一致。
  • 如果 json 字符串是通过程序获取到的,则不需要再对“ 转义处理。
package main
import (
"fmt"
"encoding/json"
)

//定义一个结构体
type Monster struct {
Name string  
Age int 
Birthday string //....
Sal float64
Skill string
}


//演示将json字符串,反序列化成struct
func unmarshalStruct() {
//说明str 在项目开发中,是通过网络传输获取到.. 或者是读取文件获取到
str := "{\"Name\":\"牛魔王~~~\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"牛魔拳\"}"

//定义一个Monster实例
var monster Monster

err := json.Unmarshal([]byte(str), &monster)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 monster=%v monster.Name=%v \n", monster, monster.Name)

}
//将map进行序列化
func testMap() string {
//定义一个map
var a map[string]interface{}
//使用map,需要make
a = make(map[string]interface{})
a["name"] = "红孩儿~~~~~~"
a["age"] = 30
a["address"] = "洪崖洞"

//将a这个map进行序列化
//将monster 序列化
data, err := json.Marshal(a)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
//fmt.Printf("a map 序列化后=%v\n", string(data))
return string(data)

}

//演示将json字符串,反序列化成map
func unmarshalMap() {
//str := "{\"address\":\"洪崖洞\",\"age\":30,\"name\":\"红孩儿\"}"
str := testMap()
//定义一个map
var a map[string]interface{} 

//反序列化
//注意:反序列化map,不需要make,因为make操作被封装到 Unmarshal函数
err := json.Unmarshal([]byte(str), &a)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 a=%v\n", a)

}

//演示将json字符串,反序列化成切片
func unmarshalSlice() {
str := "[{\"address\":\"北京\",\"age\":\"7\",\"name\":\"jack\"}," + 
"{\"address\":[\"墨西哥\",\"夏威夷\"],\"age\":\"20\",\"name\":\"tom\"}]"

//定义一个slice
var slice []map[string]interface{}
//反序列化,不需要make,因为make操作被封装到 Unmarshal函数
err := json.Unmarshal([]byte(str), &slice)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 slice=%v\n", slice)
}

func main() {

unmarshalStruct()
unmarshalMap()
unmarshalSlice()
}

4.单元测试

Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:

  • 确保每个函数是可运行并且运行结果是正确的
  • 确保写出来的代码性能是好的
  • 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

案例:

cal.go

package cal

func addNums(a int, b int) int {
return a + b
}

cal_test.go

package cal

import "testing"

func TestAddNums(t *testing.T) {
res := addNums(3, 4)
if res != 7 {
t.Fatalf("addNums(3,4) error")
}
t.Logf("test addNums success")
}

单元测试快速入门总结

\1) 测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go , cal 不是固定的。

\2) 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper

\3) TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T 【看一下手册】

\4) 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub

\5) 运行测试用例指令

​ (1) cmd>go test [如果运行正确,无日志,错误时,会输出日志]

(2) cmd>go test -v [运行正确或是错误,都输出日志]

\6) 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序

\7) t.Logf 方法可以输出相应的日志

\8) 测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图].

\9) PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败

\10) 测试单个文件,一定要带上被测试的原文件

go test -v cal_test.go cal.go

\11) 测试单个方法

go test -v -test.run TestAddUpper

5.协程和管道

5.1.进程和线程

  • 进程就是程序程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位

  • 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。

  • 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。

  • 一个程序至少有一个进程,一个进程至少有一个线程

5.2.并发和并行

  • 多线程程序在单核上运行,就是并发
  • 多线程程序在多核上运行,就是并行

5.3.go协程和主线程

Go 主线程(有程序员直接称为线程/也可以理解成进程):一个Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程「编译器做优化]。

Go 协程的特点

  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程

5.4.goroutine快速入门

案例:

func main() {
go test() //开启了一个协程
for i := 1; i <= 10; i++ {
fmt.Println("main() ", i)
time.Sleep(time.Second / 4)
}
}
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() ", i)
time.Sleep(time.Second / 4)
}
}

如果主线程退出了,则协程即使还没有执行完毕,也会退出

当然协程也可以再主线程没有退出前,就自己结束了,比如完成了自己的任务

5.5.快速入门小结

  • 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
  • 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  • Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了

5.6.MPG 模式

  • M: 操作系统的主线程(是物理线程)
  • P:协程执行需要的上下文
  • G: 协程
MPG模式运行的状态1

在这里插入图片描述

MPG模式运行的状态2

在这里插入图片描述

设置Golang运行的cpu数
import (
    "fmt"
    "runtime"
)

func main() {
    cpuNum := runtime.NumCPU()
    fmt.Println("cpuNum=", cpuNum)
    //可以自己设置使用多少个cpu
    runtime.GOMAXPROCS(cpuNum)
}

g01.8后,默认让程序运行在多个核上,可以不用设置了

go1.8前,还是要设置一下,可以更高效的利益cpu

5.7.channel(管道)

5.7.1.使用全局变量加锁
package main

import (
"fmt"
"sync"
"time"
)

var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//sync是包, synchornized 同步
//Mutex 互斥
lock sync.Mutex
)

func main() {
for i := 1; i <= 200; i++ {
go test(i)
}
time.Sleep(time.Second * 10)
lock.Lock()
for i, v := range myMap {
fmt.Printf("myMap[%v]=%v", i, v)
}
lock.Unlock()
}
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
}
5.7.2.为什么需要channel
  1. 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
  2. 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5. 上面种种分析都在呼唤一个新的通讯机制-channel
5.7.3.channel 的基本介绍
  1. channle 本质就是一个数据结构-队列
  2. 数据是先进先出【FIFO : first in first out】
  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
  4. channel 有类型的,一个 string 的 channel 只能存放 string 类型数据

channel是线程安全的,多个协程操作一个管道时,不会发生资源竞争问题

5.7.4.声明channel

var 变量名 chan 数据类型

举例:

varintChanchan int //intChan 用于存放 int 数据
varmapChan chan map[int]string //mapChan用于存放map[int]string类型
varperChanchan Person
varperChan2 chan *Person
...

说明:

  • channel 是引用类型
  • channel 必须初始化才能写入数据, 即 make 后才能使用管道是有类型的,intChan 只能写入 整数 int

使用案例:

package main

import "fmt"

func main() {
    //创建一个可以存放 3 个 int 类型的管道
var intChan chan int
intChan = make(chan int, 3)
    
    //2. 看看 intChan 是什么
fmt.Printf("intChan 的值=%v intChan 本身的地址=%p\n", intChan, &intChan)
    
//向管道写入数据
intChan <- 10
num := 211
intChan <- num

fmt.Printf("channel len=%v, cap=%v \n", len(intChan), cap(intChan))

num2 := <-intChan
fmt.Println(num2)
num3 := <-intChan
fmt.Println(num3)
    //在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
num4 := <-intChan
fmt.Println(num4)
fmt.Println("num3=", num3, "num4=", num4)
}

5.7.5.channel 使用的注意事项
  • channel 中只能存放指定的数据类型
  • channle 的数据放满后,就不能再放入了
  • 如果从 channel 取出数据后,可以继续放入
  • 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

在这里插入图片描述

5.7.6.channel的关闭

使用内置函数close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据

func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
close(intChan)
//这时不能够再写入数到channel
//intChan<-300
fmt.Println("ok")
//当管道关闭后,读取数据时可以的
n1 := <-intChan
fmt.Println(n1)
}

5.7.7.channel 的遍历

channel 支持 for–range 的方式进行遍历,注意两个细节

  1. 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
func main() {
intChan := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan <- i * 2
}
//遍历管道不能使用普通的for循环
//在遍历时,如果channel没有关闭,则会出现deadlock的错误
//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
close(intChan)
for v := range intChan {
fmt.Println("v=", v)
}
}

5.8.应用实例

在这里插入图片描述

在这里插入图片描述

package main

import "fmt"

func main() {
    intChan := make(chan int, 50)
    exitChan := make(chan bool, 1)
    go writeData(intChan)
    go readData(intChan, exitChan)
    for {
       _, ok := <-exitChan
       if !ok {
          break
       }
    }
}

// write Data
func writeData(intChan chan int) {
    for i := 1; i <= 50; i++ {
       intChan <- i
       fmt.Printf("readData写入数据=%v\n", i)
    }
    close(intChan)
}
func readData(intChan chan int, exitChan chan bool) {
    for {
       v, ok := <-intChan
       if !ok {
          break
       }
       fmt.Printf("readData读到数据=%v\n", v)
    }
    //读取数据后,即任务完成
    exitChan <- true
    close(exitChan)
}
阻塞

在这里插入图片描述

求素数
package main

import (
"fmt"
"time"
)

func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
exitChan := make(chan bool, 4)
go putNum(intChan)
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
close(primeChan)
}()
for {
res, ok := <-primeChan
if !ok {
break
}
fmt.Printf("素数=%d\n", res)
}
fmt.Println("main线程退出")
}
func putNum(intChan chan int) {
for i := 1; i <= 8000; i++ {
intChan <- i
}
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
time.Sleep(time.Millisecond * 10)
num, ok := <-intChan
if !ok {
break
}
flag = true
for i := 2; i < num; i++ {
if num%i == 0 {
flag = false
break
}
}
if flag {
primeChan <- num
}
}
fmt.Println("有一个primeNum协程因为取不到数据退出")
exitChan <- true
}

5.9.channel 使用细节和注意事项

1.channel 可以声明为只读,或者只写性质

//只写
var chan2 chan<- int
chan2 = make(chan<- int, 3)
chan2 <- 20
//只读
var chan3 <-chan int
num := <-chan3
fmt.Println(num)

2.只读和只写的最佳实践案例

在这里插入图片描述

3.使用 select 可以解决从管道取数据的阻塞问题

func main() {
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
for {
select {
//注意:这里如果intChan一直没有关闭,不会一直阻塞而deadlock
//会自动到下一个case匹配
case v := <-intChan:
fmt.Printf("%d\n", v)
case v := <-stringChan:
fmt.Printf("%s\n", v)
default:
fmt.Printf("都取不到了")
return
}
}
}

4.goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题

在这里插入图片描述

package main

import (
"fmt"
"time"
)

// 函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}

// 函 数
func test() {
//这里我们可以使用 defer + recover
defer func() {
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
//定义了一个 map
var myMap map[int]string
myMap[0] = "golang" //error
}

func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}

6.反射

6.1.反射的基本介绍

  1. 反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind)
  2. 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段方法)
  3. 通过反射,可以修改变量的值,可以调用关联的方法。
  4. 使用反射,需要 import (“reflect”)

示意图:

在这里插入图片描述

6.2.反射的应用场景

在这里插入图片描述

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/imag在这里插入图片描述
es/20230724024159.png?origin_url=Go%E8%BF%9B%E9%98%B6%2Fimage-20241009193451064.png&pos_id=img-kbADoGEh-1731135619287)

6.3.反射重要的函数和概念

在这里插入图片描述

3)变量、interface{} 和 reflect.Value 是可以相互转换的,这点在实际开发中,会经常使用到。画出示意图

在这里插入图片描述

在这里插入图片描述

6.4.快速入门

请编写一个案例,演示对(基本数据类型、interface{}、reflect.Value)进行反射的基本操作代码演示

package main

import (
"fmt"
"reflect"
)

func main() {
var num int = 100
reflectTest01(num)
}
func reflectTest01(b interface{}) {
tTyp := reflect.TypeOf(b)
fmt.Println(tTyp)

//获取reflect.Value
rVal := reflect.ValueOf(b)
n2 := 2 + rVal.Int()
fmt.Println("n2=", n2)
fmt.Printf("rVal=%v rVal type=%T\n", rVal, rVal)

//下面将rVal转成interface{}
iV := rVal.Interface()
//将interface{}通过断言转成需要的类型
num2 := iV.(int)
fmt.Println("num2=", num2)
}

请编写一个案例,演示对(结构体类型、interface{}、reflect.Value)进行反射的基本操作代码演示

func reflectTest02(b interface{}) {
tTyp := reflect.TypeOf(b)
fmt.Println(tTyp)

//获取reflect.Value
rVal := reflect.ValueOf(b)

//下面将rVal转成interface{}
iV := rVal.Interface()
fmt.Printf("%v,%T\n", iV, iV)
stu, ok := iV.(Student)
if ok {
fmt.Println(stu.Name)
}
}

6.5.反射的注意事项和细节

  1. reflect.Value.Kind,获取变量的类别,返回的是一个常量

在这里插入图片描述

  1. Type 和 Kind 的区别
  • Type 是类型, Kind 是类别, Type 和 Kind 可能是相同的,也可能是不同的
  • 比如: var num int = 10 num 的 Type 是 int , Kind 也是 int
  • 比如: var stu Student stu 的 Type 是 pkg1.Student , Kind 是 struct

在这里插入图片描述

  1. 通过反射的来修改变量, 注意当使用 SetXxx 方法来设置需要通过对应的指针类型来完成, 这样才能改变传入的变量的值, 同时需要使用到 reflect.Value.Elem()方法

在这里插入图片描述

6.reflect.Value.Elem() 应该如何理解?

在这里插入图片描述

6.6.反射最佳实践

1.使用反射来遍历结构体的字段调用结构体的方法,并获取结构体标签的值

package main

import (
    "fmt"
    "reflect"
)

// Monster 定义了一个 Monster 结构体
type Monster struct {
    Name  string  `json:"name"`
    Age   int     `json:"monster_age"`
    Score float32 `json:"成绩"`
    Sex   string
}

// Print 方法,显示 s 的值
func (s Monster) Print() {
    fmt.Println("---start~ ")
    fmt.Println(s)
    fmt.Println("---end~   ")
}

// GetSum 方法,返回两个数的和
func (s Monster) GetSum(n1, n2 int) int {
    return n1 + n2
}

// Set 方法, 接收四个值,给 s 赋值
func (s Monster) Set(name string, age int, score float32, sex string) {
    s.Name = name
    s.Age = age
    s.Score = score
    s.Sex = sex
}

func TestStruct(a interface{}) {
    //获取 reflect.Type 类型
    typ := reflect.TypeOf(a)
    //获取 reflect.Value 类型
    val := reflect.ValueOf(a)
    //获取到 a 对应的类别
    kd := val.Kind()
    //如果传入的不是 struct,就退出
    if kd != reflect.Struct {
       fmt.Println("expect struct")
       return
    }
    //获取到该结构体有几个字段
    num := val.NumField()

    fmt.Printf("struct has %d fields\n", num) //4
    //变量结构体的所有字段
    for i := 0; i < num; i++ {
       fmt.Printf("Field %d:  值为=%v\n", i, val.Field(i))
       //获取到 struct 标签, 注意需要通过 reflect.Type 来获取 tag 标签的值
       tagVal := typ.Field(i).Tag.Get("json")
       //如果该字段于 tag 标签就显示,否则就不显示
       if tagVal != "" {
          fmt.Printf("Field %d: tag 为=%v\n", i, tagVal)
       }
    }
    //获取到该结构体有多少个方法
    numOfMethod := val.NumMethod()
    fmt.Printf("struct has %d methods\n", numOfMethod)

    //var params []reflect.Value
    //方法的排序默认是按照 函数名的排序(ASCII 码)
    val.Method(1).Call(nil) //获取到第二个方法。调用它

    //调用结构体的第 1 个方法 Method(0)
    var params []reflect.Value //声明了 []reflect.Value
    params = append(params, reflect.ValueOf(10))
    params = append(params, reflect.ValueOf(40))
    res := val.Method(0).Call(params) //传入的参数是 []reflect.Value, 返回[]reflect.Value
    fmt.Println("res=", res[0].Int()) //返回结果, 返回的结果是 []reflect.Value*/
}
func main() {
    //创建了一个 Monster 实例
    var a Monster = Monster{
       Name: "黄鼠狼精", Age: 400,
       Score: 30.8,
    }
    //将 Monster 实例传递给 TestStruct 函数
    TestStruct(a)
}

7.TCP编程

18.1 网络编程基本介绍

Golang 的主要设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程序必不可少也是至关重要的一部分。

网络编程有两种:

  1. TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协议的. 比如: QQ 聊天 [示意图]
  2. b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议,而 http 底层依旧是用 tcp socket 实现的。[示意图] 比如: 京东商城 【这属于 go web 开发范畴 】

原文地址:https://blog.csdn.net/qq_43000128/article/details/143645362

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!