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 的 类型 %T\n 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]=%v\n",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=%v\n",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("%T\n%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=%v\n",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("%d\n",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读取的数据%d\n",v) case v:= <-stringChan fmt.Printf("从stringChan读取的数据%d\n",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=%T\n",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 fileds\n",num)
for i:=0;i<num;i++{
fmt.Printf("Filed %d值为%v\n",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 methods\n",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=%v\n",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, " \r\n")
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 } //... }
经典项目——海量用户即时通讯系统
海量用户即时通讯系统,实现用户登录、注册、显示在线用户列表、群聊等功