GO核心编程

简介

go语言特点:

  • go具有垃圾回收机制
  • 从语言层面支持并发,goroutine,高效利用多核,基于CPS并发模型实现(重要特点)
  • 吸收了管道通信机制,实现不同goroutine之间的互相通信
  • 函数可以返回多个值
  • 切片、延时执行defer
  • 继承C语言很多思想,引入包的概念,用于组织程序结构

golang执行流程分析

第一种方式是go build编译后生成可执行文件,在运行可执行文件即可;第二种方式是直接go run源文件。两种方式的区别:

  • 如果我们先编译生成了可执行文件,那么我们可以将该可执行文件拷贝到没有 go 开发环境的机器上,仍然可以运行
  • 如果我们是直接 go run go 源代码,那么如果要在另外一个机器上这么运行,也需要 go 开发环境,否则无法执行
  • 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多

真正工作时候需要先编译在运行!!

go程序开发注意事项

  • Go每个语句后不需要分号(Go语言会在每行后自动加分号)
  • Go编译器是一行行进行编译的,一行只能写一条语句
  • 存在未使用的包或变量,编译会通不过

规范代码风格

编写完代码后可以通过gofmt -w main.go来进行格式化;Go设计者的思想:一个问题尽量只有一个解决方法。

基本语法

变量使用注意事项

  • 如果一次声明多个全局变量

    var(
    	n3 = 300
    	name = "mary"
    ) 
    //局部变量 var n3, name = 300, "mary"
    
  • //查看变量类型和占用字节
    fmt.Printf("n1 的 类型 %Tn n1占用的字节数 %d",n1,unsafe.Sizeof(n1))
    
  • /*
    byte~uint8 存储字符时候选用byte
    如果保存字符对应码值大于255,比如汉字,可以考虑使用int类型保存
    如果需要输出字符,需要格式化输出
    */
    
    //rune~int32 表示一个Unicode码
    
  • /* 
    Go中字符串是不可变的
    字符串两种表现形式:
    双引号:会识别转义字符
    反引号:以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果
    */
    
  • Go数据类型不能自动转换,需要显示转换T(v)

    //基本数据类型和string的相互转换
    //Sprintf会根据format参数生成格式化的字符串并返回该字符串
    
  • go语言不支持三元运算符

流程控制使用注意事项

  • Switch...case语句中,case后面不再需要添加break,case后面也可以有多个值,用逗号分隔开。如果想要执行下面的语句,添加fallthrough关键字,叫做switch穿透
  • 循环遍历只有一个for关键字,可以用for range语句来遍历数组。

包使用注意事项

  • 一个文件夹下的所有.go文件同属于一个包,一般和文件夹一样。在同一个包下不能有相同的函数名和变量名,即使在不同文件中也一样。
  • 跨包访问的函数或变量首字母需要大写,相当于public。
  • import实际上是import "文件夹名字",访问时候是用的包名.函数名,因为包名可以和文件夹名不一样
  • 如果要编译成一个可执行程序文件,就需要将这个包声明为main;如果是写一个库,包名可以自定义

函数使用注意事项

  • 基本数据类型和数组默认都是值传递

  • Go中,函数也是一种数据类型,可以赋给一个变量,类似于C语言的函数指针,类型为func(type1,type2)

  • C++中typedef,在Go中变为type

  • 支持对函数返回值命名

    func getSumAndSub(n1 int,n2 int)(sum int,sub int){
    	sum = n1 + n2
    	sub = n1 - n2
    	return
    }
    
  • 支持可变参数

    func sum(args... int) sum int{
    }
    func sum(n1 int,args... int) sum int{
    }
    //args是slice切片,通过args[index]可以访问到各个值,可变参数要放在形参列表最后
    
  • 每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也 就是说 init 会在 main 函数前被调用。如果一个文件同时包含全局变量定义,init 函数和 main 函数,则执行的流程全局变量定义->init函数->main 函数,如果import其他文件,则先执行其他文件的初始化!!!

  • 匿名函数

    //方式一
    res1:= func(n1 int) int{
    	return n1+1
    }(10) 
    //方式二
    fun:=func(n1 int) int{
      return n1+1
    }
    res2:=fun(10)
    
  • 闭包

    闭包就是一个函数和与其相关的引用环境组合的一个整体.可以这样理解: 闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。

    要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引

    用到的变量共同构成闭包

    func makeSuffix(suffix string) func(string) string{
      return func(name string) string{
        //如果name没有指定后缀,则加上,否则就返回原来的名字
        if !strings.HasSuffix(name,suffix){
          return name+suffix
        }
      }
    }
    f2 := makeSuffix(".jpg")
    fmt.Println(f2("winter")) //winter.jpg
    fmt.Println(f2("bird.jpg")) //bird.jpg
    

    我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每 次都传入 后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复 使用。这个makeSuffix用处有点类似于java的泛型和c++的模版类,生成特定后缀判断的函数变量

  • defer

    当执行到defer时,暂停不执行,会将defer后面的语句压入到独立的栈(defer栈),当函数执行完毕后,再从defer栈中取出语句执行,在defer语句放入到栈时,也会将相关的值拷贝同时入栈

    func sum(n1 int,n2 int) int{
      defer fmt.Println("ok1 n1=",n1)
      defer fmt.Println("ok2 n2=",n2)
      n1++
      n2++
      res:=n1+n2
      fmt.Println("ok3 res=",res)
      return res
    }
    //执行结果
    //ok3 res=32
    //ok2 n2=20
    //ok1 n1=10
    

    defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源

  • 函数传参

    值类型:基本数据类型、数组和结构体 struct,默认是值传递

    引用类型:指针、slice 切片、map、管道 chan、interface 等,默认是引用传递

  • 错误处理

    Go语言不支持传统的try...catch...finally处理,引入的处理方式为defer,panic,recover。

    这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。

    func test(){
    	defer func(){
        err := recover() //recover()内置函数,可以捕获到异常
        if err != nil{
          fmt.Println("err",err)
        }
      }()
      num1 := 10
      num2 := 0
      res := num1/num2
      fmt.Println("res=",res)
    }
    

    自定义错误:

    1.errors.New("错误说明") , 会返回一个 error 类型的值,表示一个错误

    2.panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了,相当于java的Object)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序.

数组和切片

go语言中数组的名字不在是地址了,数组的首地址为&arr或者&arr[0]。

var arr1 = [3]int{5,6,7} //var slice = []int{1,2,3} 虽然可以这样,但已经不是一个数组了,数组声明必须指定长度
var arr2 = [...]int{1,3,3}
var arr3 = [...]int{1:800,0:900,2:999}
//for range遍历方式
for index,value range arr1{
}

数组使用注意事项

  • func test(arr [3]int){ //值传递,不影响原来的
    }
    func test(arr *[3]int){//可以通过传指针
    }
    //Go语言传参有严格的限制,[3]int类型和[4]int类型不一致!!!
    
  • 二维数组定义后面的赋值必须严格的划分开,不能省略花括号!!

    arr := [2][2]int{{1,2},{3,4}}
    arr := [...][2]int{{1,2},{3,4}}
    //二维数组for-range遍历
    for i,v:= range arr{
      for j,v2:=range v{
      }
    }
    

切片是数组的一个引用,因此切片是引用类型,是一个可以动态变化的数组。

slice := ar[1:3] //左开右闭
slice := make([]int,len,[cap])
slice := []int{1,2,3}

方式一和方式二的区别

通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素。方式一直接饮用数组,这个数组事先存在,程序员可见。

切片使用注意事项

  • 切片可以继续切片,因为切片的更改会影响底层数组的更改。
  • append内置函数可以对切片追加具体元素,也可以追加slice。追加的具体元素如果不超过底层数组的长度,则会覆盖底层数组的数值;当超过底层数组的长度时候,go会创建一个新的数组,将slice原来包含的元素拷贝到新的数组然后重新引用newArr。
  • 内置函数copy(dest,source)的参数类型是切片,source长度可以闭dest大

string和slice

  • string底层是一个byte数组,因此string也可以进行切片

  • string是不可变的,str[0]='z'编译不通过

    //如果想要改变,可以现将string转成byte切片,修改完后在转为string
    arr1 := []byte(str)
    arr1[0] = 'z'
    str = string(arr1)
    //这种转换仅仅适用于string <---> byte,可以把byte当成char类型
    

    注意:当我们转成[]byte后,可以处理英文和数字,不能处理中文,因为一个汉字3个字节,会出现乱码,解决办法是将string转成[]rune即可,因为[]rune是按字符处理的,兼容汉字。

map

声明一个map是不会分配内存的,初始化需要make,分配内存后才能赋值和适用。

m := make(map[string]string,10) //容量达到后,会自动扩容
m := make(map[string]string)

新增操作:Map["key"]=value 如果key还没有就是增加,如果key存在就是修改。

删除操作:delete(map,"key"),如果一次性删除所有的则需要一个个遍历key去delete

查找操作:v,ok :=map["tom"]

Slice of map

m := make([]map[string]string,2) //类型为map[string]string的切片,大小为2,第三个map就需要先append在使用了,否则会越界
//切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了

注意:使用slice和map一定要先make

map中的key是无序的,每次遍历得到的结果可能不一样,Go没有办法对map进行排序,但是有办法根据key来顺序输出map。

/*
1. 先将map的key放到切片中
2. 对切片排序
3. 遍历切片,然后按照key来输出map值
*/

面向对象编程

结构体

type Person struct{
}
p := Person{"mary",20}
var person *Person = new(Person)
(*person).Name = "smith" //person.Name = "smith"
//go设计者为了程序员使用方便,底层会对person.Name进行处理,加上(*person).Name

结构体使用注意细节:

  • 不同结构体可以相互转换,前提是需要有完全相同的字段(名字、个数和类型)
  • struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。

方法

func (p Person) test(){
	//...
}

方法使用注意事项

  • Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct,int,floate32等都可以有方法
  • 变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝
  • 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母 大写,可以在本包和其它包访问
  • 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输 出

工厂模式

问题来了,如果首字母是小写的, 比如 是 type student struct {....} 就不不行了,怎么办---> 工厂模式来解决.

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
}

封装

在 Golang 开发中并没有特别强调封装,这点并不像 Java

  • 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
  • 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
  • 提供一个首字母大写的 Set /Get方法(类似其它语言的 public)

继承

在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性,也即匿名结构体的所有东西成为了新的结构体的一部分。

  • 结构体可以使用匿名结构体的所有字段和方法,大小写都可以,但是要在同一个包里面去访问。、
  • 匿名结构体字段访问可以简化,比如b.A.age=19可以写b.age=19。
  • 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
  • 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身 没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错
  • 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
  • 如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。尽量不要使用多重继承

接口

Go采用接口来实现多态,interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。只要一个变量,含有接口类型中的所有方法(注意:一定要是所有),那么这个变量就实现这个接口。

接口使用注意事项

  • 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口

  • 只要是自定义数据类型,就可以实现接口

    type integer int
    func (i integer) say{
    	//...
    }
    

类型断言

接口要转成具体类型就要用到类型断言

var x interface{}
var b2 float32 = 1.1
x = b2
y := x.(float32) //arg.(type)

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

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

if y,ok := x.(float32);ok{
	//convert success
}else{
  //convert fail
}

高级教程

命令行参数

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

for i,v:= range os.Args{
  fmt.Printf("args[%v]=%vn",i,v)
}//有效参数从Args[1]开始,即第二个

flag包解析命令行参数

前面的方式是比较原生的方式,对解析参数不是特别的方便,特别是带有指定参数形式的命令行。go 设计者给我们提供了 flag 包,可以方便的解析命令行参数,而且参数顺序可以随意。

	//定义几个变量,用于接受命令行参数
	var user string
	var pwd int
	flag.StringVar(&user,"u","","用户名,默认为空")
	flag.IntVar(&pwd,"pwd",0,"密码,默认为空")
	flag.Parse()
	fmt.Printf("user=%v pwd=%vn",user,pwd)

序列化和反序列化

json.Marshal(v interface{}) ([]byte,error) //序列化
type monster struct{
}
json.unMarshal([]byte(str),&monster) //序列化

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

在反序列化一个json字符串时,要确保反序列化后的数据类型和原来序列化前的数据类型一致

*单元测试

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

  • 测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go , cal 不是固定的
  • 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper
  • TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.
  • 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序,t.Logf 方法可以输出相应的日志

goroutine

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

Go可以轻轻松松的起上万个协程。

channel

这个解决的是不同的goroutine如何通信的问题。

全局变量的互斥锁

lock sync.Mutex
lock.lock
//...
lock.unlock

上面这种方法不完美,主线程在等待所有 goroutine 全部完成的时间很难确定;

如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁;

通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作

在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增加一个参数 -race 即可

  • channel本质就是一个数据结构-队列,它是有类型的,是线程安全的(多个协程操作同一个管道时,不会发生资源竞争问题)

  • channel必须初始化才能写入数据,即make后才能使用

    var intChan chan int
    intChan = make(chan int,3)
    
  • 当我们给管写入数据时,不能超过其容量,它的价值是一边放一边取

  • allChan := make(chan interface{},3)
    allChan <- Cat{Name:"tom",Age:18}
    newCat <- allChan
    fmt.Printf("%Tn%v",newCat,newCat) //正常输出
    fmt.Printf("newCat.Name=%v",newCat.Name) //编译不通过!!!
    a := newCat.(Cat) //使用类型断言!!!!
    
  • 使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然 可以从该 channel 读取数据,只有关闭后读完会自动退出

  • 在没有使用协程的情况下,如果 channel 数据取完了,再取就会报 dead lock ,写也是一样。使用协程则会阻塞。

应用实例1

一个读协程,一个写协程,操作同一管道,主线程需要等待两个协程都完成工作才能退出。

func writeData(intChan chan int){
	for i:=1;i<=50;i++ {
		//放入数据
		intChan <- i
		fmt.Println("write data",i)
	}
	close(intChan)
}

func readData(intChan chan int,exitChan chan bool){
	for{
		v,ok := <-intChan
		if !ok {
			break
		}
		fmt.Println("read data=%vn",v)
	}
	//任务完成
	exitChan<-true
	close(exitChan)
}
func main()  {
	//创建两个管道
	intChan := make(chan int,10)
	exitChan := make(chan bool,1)
	
	go writeData(intChan)
	go readData(intChan,exitChan)
	
	for{
		_,ok := <- exitChan
		if !ok {
			break
		}
	}
}

管道的阻塞机制

如果只是向管道写入数据,而没有读取数据,就会出现阻塞而dead lock。原因是intChan容量是10,而代码wirteData会写入50个数据,因此会阻塞在writeData的ch<-i。但是写管道和读管道的频率不一致,无所谓

应用实例2

统计1-8000的数字中,哪些是素数?将统计素数的任务分配给4个goroutine去完成

//向intChan放入1-8000个数
func putNum(intChan chan int){
	for i:=1;i<=8000;i++ {
		intChan <- i
	}
	close(intChan)
}
//从intChan取出数据,并判断是否为素数,如果是,就放入primeChan
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool)  {
	var flag bool
	for  {
		time.Sleep(time.Microsecond*10)
		//取一个数处理
		num,ok := <-intChan
		if !ok{
			break
		}
		flag = true
		//判断
		for i:=2;i<num;i++{
			if num%2 ==0 {
				flag=false
				break
			}
		}
		//放入
		if flag {
			primeChan <- num
		}
	}
	fmt.Println("有一个primeNum协程因为取不到数据,退出")
	//这里不能关闭primeChan
	exitChan <- true
}
func main(){
	intChan := make(chan int, 1000)
	primeChan := make(chan int,2000)
	exitChan := make(chan bool,4) //4个
	go putNum(intChan)
	//开启4个协程,从intChan取出数据判断
	for i:=0;i<4;i++{
		go primeNum(intChan,primeChan,exitChan)
	}
	go func() {
		for i:=0;i<4;i++{
			<-exitChan
		}
		//当我们从exitChan取出4个结果,就可以放心关闭primeChan
		close(primeChan)
	}()

	for {
		res,ok := <-primeChan
		if !ok{
			break
		}
		fmt.Println("%dn",res)
	}
}

channel使用注意事项

  • channel可以声明为只读或者只写,可以有效防止误操作,降低权限。

    /*
    var writeChan chan<-int //只写
    var readChan <-chan int //只读
    */
    
  • 传统方法在遍历管道时,如果不关闭后阻塞而导致deadlock。在实际开发中,可能我们不好确定什么时候关闭管道,使用select可以解决从管道取数据的阻塞问题。

    for{
      //select里面的case是并发执行的
      select{
        //这里如果intChan一直没有关闭,不会一直阻塞而deadlock,没有数据的话会自动到下一个case匹配
      case v:= <-intChan
        fmt.Printf("从intChan读取的数据%dn",v)
      case v:= <-stringChan
        fmt.Printf("从stringChan读取的数据%dn",v)
      default:
        fmt.Printf("都取不到,程序员可以加入逻辑n")
        time.Sleep(time.Second)
        return
      }
    }
    
  • goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题。

反射

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

反射常见的应用场景

  • 不知道接口调用哪个函数,根据传入参数在运行时确定调用的具体接口,这种需要对函数或方法反射。
  • 对结构体序列化时,如果结构体有指定tag,也会使用反射生成对应的字符串

概念

  • reflect.TypeOf()/reflect.ValueOf()

  • 变量、interface{}、reflect.Value是可以相互转换的

    func reflectTest(b interface{})  {
    	//通过反射获取传入变量的type,kind,值
    	rType := reflect.TypeOf(b)
    	fmt.Println("rType=",rType)
    
    	rVal := reflect.ValueOf(b)
    	n:= 2+rVal.Int()
    	fmt.Println("n=",n)
    	fmt.Printf("rVal=%v rVal type=%Tn",rVal,rVal)
    
    	iV:=rVal.Interface()
    	n2:=iV.(int)
    	fmt.Println("n2=",n2)
    }
    

反射使用注意细节

  • Reflect.Vlaue.Kind获取变量的类别,返回一个常量,type和kind有时候一样有时候不一样,stu Type是Student,Kind是struct
  • 通过反射的来修改变量, 注意当使用 SetXxx 方法来设置需要通过对应的指针类型来完成, 这样才能改变传入的变量的值, 同时需要使用到 reflect.Value.Elem()方法(相当于获取指针指向变量的值)

反射最佳实践

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

type Monster struct{
	Name string `json:"name"`
	Age int `json:"monster_age"`
	Score float32 `json:"成绩"`
	Sex string
}

func (s Monster) GetSum(n1,n2 int) int  {
	return n1+n2
}

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 (s Monster) Print(){
	fmt.Println("----start---")
	fmt.Println(s)
	fmt.Println("-----end-----")
}

func TestStruct(a interface{}){
	typ := reflect.TypeOf(a)
	rval := reflect.ValueOf(a)
	kd := rval.Kind()
	if kd != reflect.Struct{ //如果不是struct就退出
		fmt.Println("expect struct")
		return
	}
	//获取结构体有几个字段
	num := rval.NumField()
	fmt.Printf("structs has%d filedsn",num)
	for i:=0;i<num;i++{
		fmt.Printf("Filed %d值为%vn",i,rval.Field(i))
		tagVal := typ.Field(i).Tag.Get("json")
		//如果该字段有tag就显示,否则就不显示
		if tagVal !=""{
			fmt.Printf("File%d:tag为%v",i,tagVal)
		}
	}
	//获取结构体有多少个方法
	numOfMethod:=rval.NumMethod()
	fmt.Printf("struct has %d methodsn",numOfMethod)
	//方法的排序默认是按照函数名排序
	rval.Method(1).Call(nil)//获取到第二个方法即Print,调用它,因此没有参数
	//调用结构体的第一个方法Method(0)
	var params []reflect.Value
	params = append(params,reflect.ValueOf(10))
	params = append(params,reflect.ValueOf(40))
	
	res:=rval.Method(0).Call(params)//传入参数是[]reflect.Value
	fmt.Println("res=",res[0].Int())//返回结果是[]reflect.Value

}

TCP编程

端口分类:0保留端口;1-1024固定端口;1025-65525动态端口,程序员可以使用,一个端口只能被一个程序监听,服务器要尽可能少用端口。

服务端代码

func process(conn net.Conn){
	defer conn.Close()

	for{
		buf:=make([]byte,1024)
		//等待客户端conn发送信息,如果客户端没有发送,那么协程就阻塞在这里
		fmt.Printf("服务器在等待客户端%s 发送信息n",conn.RemoteAddr().String())
		n,err := conn.Read(buf)
		if err != nil{
			fmt.Printf("客户端退出 err=%v",err)
			return //!!!
		}
		//显示客户端发送的内容到服务器的终端
		fmt.Print(string(buf[:n]))
	}

}
func main()  {
	fmt.Println("服务器开始监听...")
	listen,err:= net.Listen("tcp","0.0.0.0:8888")
	if err!= nil{
		fmt.Println("listen err=",err)
		return
	}
	defer listen.Close() //延时关闭listen
	//循环等待客户端来连接我
	for{
		fmt.Println("等待客户端来连接...")
		conn,err:=listen.Accept()
		if err != nil{
			fmt.Println("Accept() err=",err)
		}else{
			fmt.Printf("Accept() success con=%v 客户端ip=%vn",conn,conn.RemoteAddr().String())
		}
		//这里准备一个协程为客户端服务
		go process(conn)
	}
}

客户端代码

func main(){
	conn,err := net.Dial("tcp","0.0.0.0:8888")
	if err != nil{
		fmt.Println("client dial err=",err)
		return
	}

	//客户端可以发送单行数据,然后就退出
	reader := bufio.NewReader(os.Stdin)
	for {
		//从终端读一行用户输入,并准备发送给服务器
		line, err := reader.ReadString('n')
		if err != nil {
			fmt.Println("readString err=", err)
		}
		//如果用户输入的是exit就退出
		line = strings.Trim(line, " rn")
		if line == "exit" {
			fmt.Println("客户端退出..")
			break
		}
		//再将line发送给服务器
		_, err = conn.Write([]byte(line + "n"))
		if err != nil {
			fmt.Println("conn Write err=", err)
		}
		//fmt.Printf("客户端发送了%d字节的数据,并退出",n)
	}
}

Redis的使用

REmote Dictionary Server(远程字典服务器),Redis性能非常高,单机能够达到15w qps,通常适合做缓存,也可以持久化。是完全开源的,高性能的k-v分布式内存数据库,基于内存运行并支持之久化的NoSQL数据库。

Redis安装好后,默认有16个数据库,初始默认使用0号库,编号0...15,select 1`切换1号数据库。

golang操作redis

  • 安装第三方开源redis库

    cd $GOPATH
    go get github.com/garyburd/redigo/redis
    
  • Set/Get接口

    func main()  {
    	//连接到redis
    	conn,err := redis.Dial("tcp","127.0.0.1:6379")
    	if err!= nil{
    		fmt.Println("redis.Dial err=",err)
    		return
    	}
    	defer conn.Close()
    	//通过go向redis写入数据string[key-val]
    	_,err = conn.Do("Set","name","tomjerry_cat")
    	if err!= nil{
    		fmt.Println("set err=",err)
    		return
    	}
    	//通过go向redis读取数据
    	r,err:=redis.String(conn.Do("Get","name"))
    	if err!= nil{
    		fmt.Println("get err=",err)
    		return
    	}
    	//因为返回r是interface{},name对应的值是string,因此我们需要转换
    	//nameString := r.(string)
    	fmt.Println("操作ok",r)
    }
    
  • redis链接池

    事先初始化一定数量的链接,放入到链接池,当 Go 需要操作 Redis 时,直接从 Redis 链接池取出链接即可,这样可以节省临时获取 Redis 链接的时间,从而提高效率。

    //定义一个全局的pool
    var pool *redis.Pool
    
    //当启动程序时,就初始化链接池
    func init()  {
    	pool = &redis.Pool{
    		MaxIdle: 8,//最大空闲链接数
    		MaxActive: 0,//表示和数据库的最大链接数,0表示没有限制
    		IdleTimeout: 100,//最大空闲时间
    		Dial: func() (redis.Conn, error) {//初始化链接代码
    			return redis.Dial("tcp","localhost:6379")
    		},
    	}
    }
    func main()  {
    	//先从pool取出一个链接
    	conn:=pool.Get()
    	defer conn.Close()
    	_,err:=conn.Do("Set","name","Tom cat!!")
    	if err!=nil{
    		fmt.Println("conn.Do err=",err)
    		return
    	}
    	
    	//...
    }
    

经典项目-海量用户即时通讯系统

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/yrxing/p/14593948.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程