前端er的Go语言学习日志

179 阅读24分钟

安装(以windows为例)

下载开发包官网下载地址,打开下载好的msi可执行文件,根据提示安装即可(一路next),默认安装在C:\Program Files\Go目录下,会自动添加go的环境变量,在cmd中输入go version验证是否成功,其他设备可参考go语言教程|菜鸟教程

开发工具

goland是收费的,没有免费版,所以本文使用vscode

安装工具&插件

安装前需要在cmd配置go的env,执行下面的两条命令

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct // 设置国内代理,不设置会导致安装工具包失败

vscode安装扩展,搜索go进行安装,安装完成后,按住键盘的ctrl+shift+p打开设置命令行,搜索go:install/Update Tools,点击它,全选要安装的工具,点击确定,等待安装成功,安装成功后安装常用工具包,在vscode的终端输入安装(安装前要把注释去掉)

go get -u -v github.com/nsf/gocode  // 代码自动补齐
go get -u -v github.com/rogpeppe/godef // 查找方法定义
go get -u -v github.com/golang/lint/golint // 代码规范
go get -u -v github.com/lukehoban/go-find-references // 查找引用
go get -u -v github.com/lukehoban/go-outline // 
go get -u -v sourcegraph.com/sqs/goreturns
go get -u -v golang.org/x/tools/cmd/gorename // 重命名
go get -u -v github.com/tpng/gopkgs // 可以使用的包
go get -u -v github.com/newhook/go-symbols

安装code runner运行脚本

go语言结构

// 包声明
package main
// 引入包
import "fmt"
// 主函数(入口函数)
func main() {
    // 调用fmt包里的函数打印输出(函数名首字母大写表示这是一个公共函数,可以在别的包使用)
    fmt.Println("Hello World!!")
}

// 这是单行注释
/*
这是多行注释
*/

main包的含义

Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。一个可执行程序有且仅有一个 main 包。当编译器发现某个包的名字为 main 时,它一定也会发现名为 main()的函数,否则不会创建可执行文件。 main()函数是程序的入口,所以,如果没有这个函数,程序就没有办法开始执行。程序编译时,会使用声明 main 包的代码所在的目录的目录名作为二进制可执行文件的文件名。

go常用命令

可以在终端使用go help命令查看go的所有命令,常用的有run、build、get、mod

go help

build: 编译包和依赖
clean: 移除对象文件
doc: 显示包或者符号的文档
env: 打印go的环境信息
bug: 启动错误报告
fix: 运行go tool fix
fmt: 运行gofmt进行格式化
generate: 从processing source生成go文件
get: 下载并安装包和依赖
install: 编译并安装包和依赖
list: 列出包
run: 编译并运行go程序
test: 运行测试
tool: 运行go提供的工具
version: 显示go的版本
vet: 运行go tool vet
mod:包管理

扩展-字节和字位

字位(bit):计算机存储信息的最小单位,及二进制中的一位0或1

字节(byte):计算机信息技术用于计量存储容量的一种计量单位,也表示一些计算机编程语言中的数据类型和语言字符。

换算关系:1字节=8字位,即1字节有8位的二进制数表示,根据utf-8的格式,一个英文字母大小为1字节,1个数字大小为1个字节,1个汉字大小为3~4个字节

内存地址的大小:跟机器有关,32位机器,一个地址就是32位占用4个字节,64位机器,一个地址就占用64位占用8个字节

扩展-计算机的进制

进制的图示

十进制十六进制八进制二进制
0000
1111
22210
33311
444100
88101000
10A121010
11B131011
16102010000

二进制

用0,1表示,满2进1,在go语言中不能直接使用二进制来表示一个整数,它沿用的c的特点

a := 5
// 二进制输出为101
fmt.Printf("%b\n", a)

八进制

用0-7表示,满8进1,以数字0开头表示

// 八进制011转为十进制为9
b := 011
fmt.Printf("b: %v\n", b)

十六进制

用0-9和A-F表示(不区分大小写,可以用a-f表示),满16进1,以0x或0X开头表示

// 十六进制0x11转为十进制为17
c := 0x11
fmt.Printf("c: %v\n", c)

其他进制转成十进制

1.二进制转十进制 
规则:从最低位开始(右边),将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和
案例:将二进制1011转成十进制
1011 = 1 * 1 + 1 * 2 + 0 * 2 * 2 + 1 * 2 * 2 * 2 = 1 + 2 + 0 + 8 =11

2.八进制011转十进制
规则:从最低位开始(右边),将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和
案例:将0123转成十进制
0123 = 3*1 + 2*8 + 1*8*8 = 83

3.十六进制转十进制
规则:从最低位开始(右边),将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和
案例:将0x34A转成十进制
0x34A = 10*1 + 4*16 + 3*16*16 = 842

练习:
二进制110001100 = 1*2^2 + 1*2^3 + 1*2^7 + 1*2^8 = 396
八进制02456 = 6 + 5*8+ 4*8^2 + 2*8^3 = 1326
十六进制0xA45 = 5 + 4*16 + 10*16^2 = 2629

十进制转其他进制

1.十进制转二进制
规则:将该数不断除以2,知道商为0为止,然后将每步得到的余数倒过来,就是对应的二进制
案例:将56转成二进制
56/2 28  0
28/2 14  0
14/2  7  0
7/2   3  1
3/2   1  1
1/2   0  1
将余数倒过来为111000,验算111000 = 1*2^3 + 1*2^4 + 1*2^5 = 56

2.十进制转八进制
规则:将该数不断除以8,知道商为0为止,然后将每步得到的余数倒过来,就是对应的八进制
案例:将156转成八进制,0234
156/8 19 4
19/8   2 3
2/8    0 2

3.十进制转十六进制 
规则:将该数不断除以16,知道商为0为止,然后将每步得到的余数倒过来,就是对应的十六进制
案例:将356转成十六进制,0x164
356/16 22 4
22/16   1 6
1/16    0 1

练习:
123转二进制 1111011
678转八进制 01246
8912转16进制 0x22D0

二进制转其他进制

1.二进制转八进制
规则:将二进制数每三位一组(从低位开始组合,因为二进制3位最大是7所以取每三位),转成对应的八进制数即可
案例:将11010101转成八进制
11010101 = 11 010 101 = 3 2 5 = 0325
2.二进制转十六进制
规则:将二进制数每四位一组(从低位开始组合,因为二进制4位最大是15所以取每四位),转成对应的十六进制数即可
案例:将11010101转成八进制
11010101 = 1101 0101 = 13 5 = D 5 = 0xD5
练习:
11100101转成八进制 11100101 = 11 100 101 = 3 4 5 = 0345
1110010110转成十六进制 1110010110 = 11 1001 0110 = 3 9 6 = 0x396

其他进制转二进制

1.八进制转二进制
规则:将八进制每一位转成对应的一个3位的二进制即可
案例:将0237转成二进制
0237 = 2 3 7 = 010 011 111 = 10011111

2.十六进制转二进制
规则:将十六进制每一位转成对应的一个4位的二进制即可
案例:将0x237转成二进制
0x237 = 2 3 7 = 0010 0011 0111 = 001000110111 = 1000110111

扩展-位运算

在计算机的内部,进行运算时都是以二进制的方式运行的

原码、反码、补码

  • 对于有符号的而言
    1. 二进制的最高位是符号位:0表示正数,1表示负数(1 --> [0000 0001], -1 --> [1000 0001])
    2. 正数的原码、反码和补码都是一样的
    3. 负数的反码=它的原码符号位不变,其他位取反(0->1 1->0)
    4. 负数的补码=它的反码+1
    5. 0的反码,补码都是0
    6. 在计算机运行时,都是以补码的方式来运算的

go语言有5个位运算符

1.按位与&
表示两位全为1结果为1,否则为0
案例:2&3=2
2的补码:0000 0010
3的补码:0000 0011
按位或: 0000 0010 = 2(得到的是补码,正数的原码、反码和补码是一样的)

2.按位或|
表示两位有一位为1结果为1,否则为0
案例:2|3=3
2的补码:0000 0010
3的补码:0000 0011
按位或: 0000 0011 = 3(得到的是补码,正数的原码、反码和补码是一样的)

3.按位异或^ 
表示两位有一位为1一位为0结果为1,否则为0
案例:2^3=1
2的补码: 0000 0010
3的补码: 0000 0011
按位异或:0000 0001 = 1
案例:-2^2
-2的补码:先算原码1000 0010  反码1111 1101 补码1111 1110
2的补码:0000 0010
按位异或:1111 1100(得到的是补码,要转换成原码,因为最高位是1是负数,先转成反码1111 1011,再转成原码1000 0100=-4)

4.左移<<
符号位不变,低位补0
案例:1<<2
1的补码:0000 0001 => 0000 01xx => 0000 0100 = 4,4是正数,原码和补码一致
                       00
案例:-3<<3
-3的补码:1000 0011 => 1111 1100 => 1111 1101(补码)
进行左移:1110 1000 => 1110 0111 => 1001 1000=-24

5.右移>>
低位溢出,符号位不变,并用符号位补溢出的高位
案例:1>>2
1的补码:0000 0001  => 0xx0 0000 => 0000 0000 = 0,0是正数,原码和补码一致
案例:-3>>2
-3的补码:1000 0011 => 1111 1100 => 1111 1101(补码)
进行右移: 1xx1 1111 => 1111 1111 => 1111 1110 => 1000 0001=-1

数字类型

有符号整型

  • int8:有符号 8 位整型,计算机中由8位二进制数表示,最高位表示正负,后7位表示值,范围是-128 到 127
  • int16:有符号 16 位整型,计算机中由16位二进制数表示,最高位表示正负,后15位表示值,范围是-32768 到 32767
  • int32:有符号 32 位整型,计算机中由32位二进制数表示,最高位表示正负,后31位表示值,范围是-2147483648 到 2147483647
  • int64:有符号 64 位整型,计算机中由64位二进制数表示,最高位表示正负,后63位表示值,范围是-9223372036854775808 到 9223372036854775807

无符号整型

  • uint8:无符号 8 位整型,计算机中由8位二进制数表示,8位二进制数表示值,范围是0 到 255
  • uint16:无符号 16 位整型,计算机中由16位二进制数表示,16位二进制数表示值,范围是0 到 65535
  • uint32:无符号 32 位整型,计算机中由32位二进制数表示,32位二进制数表示值,范围是0 到 4294967295
  • uint64:无符号 64 位整型,计算机中由64位二进制数表示,64位二进制数表示值,范围是0 到 18446744073709551615

浮点型(小数类型)

字符串类型

布尔类型

数组类型

指针

  • 基本数据类型,变量存的就是值本身,也叫值类型
  • 加载数据,会在内存中开辟一块内存空间,每块内存空间有自己的唯一地址用来标识,这个地址就是指针
  • 指针变量存的是一个地址,这个地址指向的内存空间存的才是值, 使用&获取变量的地址
  • 获取指针变量指向的值使用*,*ptr -> 先取到ptr对应的内存空间里的值(地址),再用这个地址去找到对应内存空间的值
  • 值类型都有对应的指针类型,形式为 *数据类型 例如int对应的指针类型是 *int float32对应的指针类型是 *float32
  • 值类型包括:基本数据类型int系列,float系列,bool,string;数组和结构体(struct)
  • 函数传参都是值拷贝

值类型和引用类型

  • 值类型包括:基本数据类型int系列,float系列,bool,string;数组和结构体(struct)
  • 引用类型包括:指针,slice切片,map,管道channel,接口interface等
  • 应用程序的内存载体,我们可以简单地将其分为堆和栈
  • 值类型:变量直接存储值,数据通常在栈区分配,
  • 引用类型:变量存储的是一个地址,这个地址对应的空间的值才是真正存储的值,数据通常在堆区分配,当没有变量引用这个地址时,该地址对应的空间就成为一个垃圾,被GC回收
  • 编译器有逃逸分析,编译器会根据实际情况将数据分配到栈区或堆区

切片

基本介绍

  1. 切片是数组的一个引用,因此切片是引用类型,遵循引用传递的机制
  2. 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度都是一样的
  3. 切片的长度是可以变化的,因此切片是一个可以动态变化的数组
  4. 定义切片的基本语法
var a []int

快速入门

var intArr [5]int = [...]int{11, 22, 33, 44, 55}

// 声明/定义一个切片
/*
    slice := intArr[1:3]解析
    slice切片名
    intArr[1:3]表示slice引用到intArr这个数组
    [1:3]表示引用这个数组的起始下标是1,终止下标是3(不包含3)
*/
slice := intArr[1:3]
fmt.Println("slice的元素是", slice)
fmt.Println("slice的元素个数是", len(slice))
fmt.Println("slice的容量是", cap(slice))

内存分析

从底层来看,slice其实是一个数据结构(struct结构体)
// 以上面的分析
type slice struce {
    ptr &[2]int // 值是intArr[1]的指针
    len int
    cap int
}

切片的使用

  • 方式一

    定义一个切片,然后让切片去引用一个已经创建好的数组,上面的例子

  • 方式二

    通过make来创建切片,底层这个slice还是指向一个数组,这个数组有make底层维护,这个数组对外不可见,只能通过slice去访问操作

    /*
    内建函数make分配并初始化一个类型为切片、映射、或通道的对象。其第一个实参为类型,而非值。make的返回类型与其参数相同,而非指向它的指针。其具体结果取决于具体的类型
    切片:size指定了其长度。该切片的容量等于其长度。切片支持第二个整数实参可用来指定不同的容量;它必须不小于其长度,因此 make([]int, 0, 10) 会分配一个长度为0,容量为10的切片。
    */
    var slice1 []int = make([]int, 4, 10)
    fmt.Println("slice1=", slice1)
    fmt.Println("slice1 len=", len(slice1))
    fmt.Println("slice1 cap=", cap(slice1))
    slice1[0] = 10
    slice1[3] = 50
    fmt.Println("slice1=", slice1)
    
  • 方式三

    定义一个切片,直接就指定具体数组,使用原理类似make的方式

    var slice2 []int = []int{1, 3, 5}
    fmt.Println("slice2=", slice2)
    fmt.Println("slice2 cap=", cap(slice2)) // 3
    

切片的遍历

  • 常规的for循环遍历
var arr [5]int = [5]int{1, 3, 5, 7, 9}
var slice []int = arr[:]
for i := 0; i < len(slice); i++ {
    fmt.Printf("slice[%v]=%v\n", i, slice[i])
}
  • for range遍历
for index, value := range slice {
    fmt.Printf("index=%v, value=%v\n", index, value)
}

使用细节和注意事项

  • 切片可以继续切片
var arr [5]int = [5]int{1, 3, 5, 7, 9}
var slice []int = arr[:]
for i := 0; i < len(slice); i++ {
    fmt.Printf("slice[%v]=%v\n", i, slice[i])
}

for index, value := range slice {
    fmt.Printf("index=%v, value=%v\n", index, value)
}

// arr slice1 slice2指向的是同一块数据空间,所以改变其中的一个其他的也改变
slice2 := slice[1:2]

fmt.Println("slice2=", slice2)
  • 使用内建函数append可以对切片进行动态追加

    append操作的底层原理分析

    1. 切片append的操作本质就是对数组的扩容
    2. append调用会检查slice是否有足够容量存储新的元素。如果容量足够,它会定义一个新的slice(仍然引用原始底层数组),然后将新的元素复制到新的位置,并返回这个新的slice,会改变底层数组
    3. 如果容量不足以容量新增元素,append函数会创建一个拥有足够容量的新的底层数组newArr来存储这些元素,然后将元素从 slice x复制到这个数组,再将新元素y追加到数组后面
    4. 新创建的数组newArr是由底层来维护的,对外不可见,旧的数组没有引用就会被垃圾回收
var slice []int = []int{100, 200, 300}
// 通过append追加具体元素
slice := append(slice, 400, 500, 600)
// 通过append追加切片
slice = append(slice, slice...)

// append操作的底层原理分析代码演示
var intArr [5]int = [5]int{0, 1, 2, 3, 4}
var intSlice []int = intArr[0:3]
fmt.Println("intArr=", intArr) // [0 1 2 3 4]

// 容量足够存储新的元素,引用的还是原始的底层数组,会改变这个数组
// intSlice = append(intSlice, 10, 20)
// fmt.Println("intArr=", intArr) // [0 1 2 10 20]

// 容量不足够存储新的元素,append函数会创建一个新的足够容量的底层数组
intSlice = append(intSlice, 20, 30, 40)
fmt.Println("intArr=", intArr) // [0 1 2 3 4]
  • 使用内建函数copy进行切片的拷贝操作,copy的两个参数类型都是切片,按照实例代码,a和slice的数据空间是独立的,相互不影响
var a []int = []int{1, 2, 3, 4, 5}
var slice []int = make([]int, 10)
fmt.Println("slice=", slice) // [0 0 0 0 0 0 0 0 0 0]
copy(slice, a)
fmt.Println("slice=", slice) // [1 2 3 4 5 0 0 0 0 0]

string和slice的区别

  • string底层是一个byte数组,因此string也可以进行切片处理
str := "hello world"
// 使用切片获取world
slice := str[6:]
fmt.Println(slice)
  • string是不可变的,不能通过str[0]=xxx修改
  • 如果需要修改字符串,可以将 string -> []byte 或者 string -> []rune,修改后再重写成string,如果要修改成中文就要转成[]rune
str := "hello world"
arr := []byte(str)
arr[0] = 'H'
str = string(arr)
fmt.Println(str)

数组排序

排序是将一组数据,依指定顺序进行排列的过程

排序的分类

  1. 内部排序

    将需要处理的所有数据都加载到内部存储器中进行排序,如:交换式排序法、选择式排序法和插入排序法

  2. 外部排序

    数据量过大,无法全部加载到内存中,需要借助外部存储进行排序,如:合并排序法和直接合并排序法

交换式排序法

  1. 冒泡排序法(bubble sort)
  2. 快速排序法(quick sort)

面向对象编程(oop)

go中也有面向对象的概念,但是与其他语言有很多不同的地方,go中使用结构体struct来实现面向对象

封装

将一类事物抽象出来的属性和操作这些属性的方法封装在一起,属性不对外暴露(在go中指首字母小写,属于私有变量,只能本包使用,外部包不能访问),对外提供操作和访问这些属性的方法(对外可访问的方法必须首字母大写)

继承

go中的继承属于伪继承,使用组合的方式来实现继承的特性即在一个结构体中嵌套一个匿名结构体,这样就可以使用这个匿名结构体中定义的属性和方法了,这样就可以简单的实现多重继承

// 定义一个学生结构体
type Student struct {
	Name  string
	Age   int
	Score float32
}

func (s *Student) ShowInfo() {
	fmt.Printf("学生名:=%v,年龄:%v,成绩:%v", s.Name, s.Age, s.Score)
}

func (s *Student) SetScore(score float32) {
	// 业务判断。。。。
	s.Score = score
}
// 定义一个小学生结构体
type Pupil struct {
	Student // 匿名结构体
}

func main() {
	p := Pupil{}
	p.Student.Name = "tom"
	p.Student.Age = 8
	p.SetScore(98.5)
	p.ShowInfo()
}

多态

go通过接口来实现多态的特性,如果一个类型实现了接口中声明的所有方法,那它就实现了该接口。调用同一个函数,使用的是各自结构体的实现的方法,处理逻辑各自区分

package main

import (
	"fmt"
)

// 声明一个收入接口
type Income interface {
	// 工资方法
	Sala(num int) float64
}

// 声明销售员结构体
type Salesman struct {
	Name       string
	OrderPrice float64 // 完成一单的价格
	BasicSala  float64 // 底薪
}

// 销售员结构体实现Income接口
func (s Salesman) Sala(num int) float64 {
	return s.BasicSala + s.OrderPrice*float64(num)
}

// 声明水电工结构体
type Plumber struct {
	Name       string
	OrderPrice float64 // 完成一单的价格
}

// 水电工结构体实现Income接口
func (p Plumber) Sala(num int) float64 {
	return p.OrderPrice * float64(num)
}

// 统计收入的函数
func CalcIncome(ic Income, num int) {
	fmt.Println(ic.Sala(num))
}

func main() {
	// 声明销售员实例
	s := Salesman{Name: "tom", OrderPrice: 100.0, BasicSala: 2000.0}
	// 声明水电工实例
	p := Plumber{Name: "jery", OrderPrice: 130.0}

	// 统计两个人的收入
	CalcIncome(s, 100)
	CalcIncome(p, 100)
}

文件的操作

打开关闭文件

使用os包的Open函数打开文件,返回文件句柄file和错误err,如果打开成功则err为nil,打开文件后使用defer调用文件句柄的关闭方法file.Close(),及时关闭,防止内存泄露

package main

import (
	"fmt"
	"os"
)

func main() {
	filename := "hello.txt"
    // file也称为文件对象、文件指针、文件句柄
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println("open file err is", err)
	}
	defer file.Close()              // 及时关闭file句柄,防止内存泄露
}

带缓冲的读文件

使用bufio包的NewReader函数创建一个具有默认大小缓冲、从r读取的*Reader,默认的缓冲的大小是4096,适用于大文件的读取

package main

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

func main() {
	filename := "hello.txt"
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println("open file err is", err)
	}
	defer file.Close()              // 及时关闭file句柄,防止内存泄露
	reader := bufio.NewReader(file) // 带缓冲的方式,适用于读取大文件
	// 循环读取文件的内容
	for {
		str, err := reader.ReadString('\n') // 读到一个换行符就结束
		if err == io.EOF {                  // io.EOF表示文件的末尾
			break
		}
		fmt.Print(str)
	}
}

一次性读取文件

使用io/ioutil包的ReadFile函数从filename指定的文件中读取数据并返回文件的内容,适用于读取小文件,这个函数把文件的打开和关闭都封装到内部了,所以会更加的便捷

package main

import (
	"fmt"
	"io/ioutil"
)

func DisposableRead() {
	// filename := "E:/learn/gocode/README.md"
	filename := "hello.txt"
	content, err := ioutil.ReadFile(filename)
	if err != nil {
		fmt.Println("err=", err)
	}
	// 返回的contents是字节切片, 要显示为具体的内容就是转成string
	fmt.Println(content)
	fmt.Printf("%v", string(content))
}

func main() {
	DisposableRead()
}

单元测试

go语言自带一个轻量级的测试框架testing和自带测试命令go test来实现单元测试和性能测试。单元测试是为了尽早发现程序设计或实现上的逻辑错误,性能测试是为了发现程序设计的问题,让程序在高并发的情况下还能保持稳定

快速入门

新建cal_test.go和cal.go文件,编辑如下代码,在命令行输入go test -v执行就可以看到测试代码执行(带 -v 运行正确或者错误都会输出日志, 不带运行正确无日志,错误输出日志)

  1. 如果只想运行一个测试文件里的测试用例,只需要在命令行带上测试文件名和被测试的文件名
go test -v cal_test.go cal.go
  1. 测试单个方法
go test -v -test.run TestAddUpper

Q:为什么go test可以执行没有入口函数main的代码呢?

A:go test命令会遍历所有的*_test.go文件中符合TestXxx命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

// cal.go
package main

// 被测试的函数
func addUpper(n int) int {
	res := 0
	for i := 1; i <= n; i++ {
		res += i
	}
	return res
}

// cal_test.go
package main

import (
	"testing"
)

// 编写测试用例
func TestAddUpper(t *testing.T) {

	// 调用
	res := addUpper(10)
	if res != 55 {
		// fmt.Println("addUpper函数执行错误,期望值=%v 实际值=%v", 55, res)
		t.Fatalf("addUpper函数执行错误,期望值=%v 实际值=%v", 55, res)
	} else {
		t.Logf("执行正确")
	}
}

并发编程

goroutine-协程

并发和并行

  • 并发:多线程程序在单核cpu上运行,同一段时间内执行多个任务(轮询时间片的方式),从人的视角看,是同时进行的,但实际上同一时刻只进行一个任务
  • 并行:多线程程序在多核cpu上运行,同一时刻执行多个任务(将任务分配到单独的一个线程),从人的视角看,是同时进行的,实际上也是同一时刻进行多个任务

channel-管道

同步与锁

反射

注意事项

  1. reflect.Value.Kind,获取变量的类别,返回的是一个常量
const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)
  1. Type是类型,Kind是类别,Type和Kind可能是相同的也可能是不相同的
    • var num int = 10 Type和Kind都是int
    • var std Student Type是包名.Student Kind是结构体struct
  2. 通过反射可以让变量在interface{}和Reflect.Value之间相互转换
// 变量转换成Reflect.Value
rVal := reflect.ValueOf(b)
// Reflect.Value转换成变量
val := rVal.Int()

// 变量转换成Reflect.Value再转成interface{}
iv := rVal.Interface()
// 使用断言将interface{}转成变量
num := iv.(int)
  1. 使用反射的方法来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int,那么久应该使用reflect.Value(x).Int(),而不能使用其他的,否则会报错(编译阶段不会提示,运行时报错)
  2. 通过反射来修改变量,注意当使用Setxxx方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量的值,需要搭配reflect.Value.Elem()方法使用
rVal := reflect.ValueOf(b)
rVal.Elem().SetInt(20)
  1. reflect.Value.Elem的理解,Elem()用于获取指针指向的值
var num = 10
var b *int = &num
// Elem()类似
*b

最佳实践

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

package main

import (
	"fmt"
	"reflect"
)

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

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

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

// 方法3:给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类型
	rType := reflect.TypeOf(a)
	// 获取reflect.Value类型
	rVal := reflect.ValueOf(a)
	// rVal.Elem().Field(0).SetString("青牛精")
	rVal.Elem().FieldByName("Name").SetString("111")
	// 获取a对应的类别
	kd := rVal.Kind()
	// 判断传入的值是不是一个结构体
	if kd != reflect.Struct {
		fmt.Println("expect struct")
		return
	}
	// 获取该结构体有多少个字段
	num := rVal.NumField()
	fmt.Printf("struct has %d fields\n", num)
	for i := 0; i < num; i++ {
		fmt.Printf("Field %d的值是%v\n", i, rVal.Field(i))
		// 获取struct标签
		tagVal := rType.Field(i).Tag.Get("json")
		// 如果该字段有tag标签就打印
		if tagVal != "" {
			fmt.Printf("Field %d的tag是%v\n", i, tagVal)
		}

	}

	// 获取该结构体有多少个方法
	numOfMethod := rVal.NumMethod()
	fmt.Printf("struct has %d methods\n", numOfMethod)

	// 获取到第二个方法并调用(方法的排序是以方法名的首字母的acii码进行排序的)
	rVal.Method(1).Call(nil)

	// 调用第二个方法
	var params []reflect.Value
	params = append(params, reflect.ValueOf(10))
	params = append(params, reflect.ValueOf(40))

	res := rVal.Method(0).Call(params)
	fmt.Println("res=", res[0].Int())
}

type Cal struct {
	Num1 int
	Num2 int
}

func (c Cal) GetSub(name string) {
	fmt.Printf("%v 完成了减法运算,%d - %d = %d", name, c.Num1, c.Num2, c.Num1-c.Num2)
}

func TestCal(a interface{}) {
	rType := reflect.TypeOf(a)
	rVal := reflect.ValueOf(a)

	num := rType.NumField()
	// fmt.Println("num=", num)
	for i := 0; i < num; i++ {
		fmt.Println(rType.Field(i))
	}
	var params []reflect.Value
	params = append(params, reflect.ValueOf("tom"))
	rVal.Method(0).Call(params)

}

func main() {
	// var a Monster = Monster{
	// 	Name:  "牛魔王",
	// 	Age:   500,
	// 	Score: 99.9,
	// }
	// TestStruct(&a)
	// fmt.Println(a.Name)

	var a Cal = Cal{Num1: 8, Num2: 3}
	TestCal(a)
}

网络编程

基本介绍

  • c/s架构的TCP socket编程,是网络编程的主流,之所以叫TCP socket编程,是因为底层是基于Tcp/ip协议的,比如:QQ聊天
  • b/s架构的http编程,我们使用浏览器去访问服务器时,使用的就是http协议,而http底层依旧是用tcp socket实现的,比如京东商城

基础知识

  1. 网线、网卡、无线网卡

    计算机之间相互通信必须要求网线、网卡或者是无线网卡

  2. 协议(tcp/ip)

    中文译名为传输控制协议/因特网互联协议,又叫做网络通信协议,这个协议是internet最基本的协议、internet国际互联网络的基础,简单的说,就是由网络层的ip协议和传输层的tcp协议组成的

  3. OSI与TCP/IP参考模型

  • OSI模型(理论):分为7层,应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
  • TCP/IP模型(现实):分为4层,应用层、传输层、网络层、链路层
    • 应用层:smtp、ftp、telnet、http
    • 传输层:解释数据
    • 网络层:定位ip地址和确定连接路径
    • 链路层:与硬件驱动对话