前言
本文主要介绍在Go中内置的包如何使用
time
时间相关的包
package main import ( "fmt" "time" ) func main() { //格式化时间 now := time.Now() fmt.Println(now) fmt.Println(now.Year()) fmt.Println(now.Month()) fmt.Println(now.Day()) fmt.Println(now.Hour()) fmt.Println(now.Minute()) fmt.Println(now.Second()) //时间戳 fmt.Println(now.Unix()) fmt.Println(now.UnixNano()) //纳秒时间戳 //time.Unix()时间戳转时间格式 ret := time.Unix(1587084700, 0) fmt.Print(ret.Year(), ret.Month()) //获取时间间隔 Duration类型 /* Golang的时间间隔是定义在time包中的常量 const ( //1纳秒 Nanosecond Duration = 1 //1微妙 Microsecond = 1000 * Nanosecond //1毫秒 Millisecond = 1000 * Microsecond //1秒 Second = 1000 * Millisecond //1分钟 Minute = 60 * Second //1小时 Hour = 60 * Minute ) */ // time.Sleep(time.Millisecond) // time.Sleep(5 * time.Second) // time.Sleep(time.Hour) //时间操作 //1天后 fmt.Println(now.Add(24 * time.Hour)) //一小时之后 fmt.Println(now.Add(time.Hour)) //一小时前 h, _ := time.ParseDuration("-1h") h1 := now.Add(8 * h) fmt.Println(h1) //2天前 d, _ := time.ParseDuration("-48h") DayBeforYesterday := now.Add(d) fmt.Println(DayBeforYesterday) //计算2个时间的时间差,返回duration subDay := now.Sub(DayBeforYesterday) fmt.Println(subDay) //判断2个时间是否相等,返回bool fmt.Println(now.Equal(DayBeforYesterday)) //判断2个时间点前后顺序,返回bool fmt.Println(now.Before(DayBeforYesterday)) fmt.Println(now.After(DayBeforYesterday)) //定时器 //1秒钟执行1次 // timer:=time.Tick(time.Second) // for t :=range timer{ // fmt.Println(t) // } //Go中有趣的时间格式 //以Golang诞生的时间2006 1 2 3 4 5 000替代%Y-%m-%d %H:%i:%S fmt.Println(DayBeforYesterday.Format("2006-1-2|3:4:5")) fmt.Println(now.Format("2006年1月2日3时4分5秒")) //时间格式化成字符串 精确到毫秒 fmt.Println(DayBeforYesterday.Format("2006-1-2|3:4:5.000")) fmt.Println(DayBeforYesterday.Format("2006-1-2|3:4:5.999")) stringNow := DayBeforYesterday.Format("2006-1-2|3:4:5.999") //字符串格式化成时间格式 d2, _ := time.Parse("2006-1-2|3:4:5.999", stringNow) fmt.Println(d2.Unix()) fmt.Println(d2.Year()) fmt.Println(d2.Month()) fmt.Println(d2.Day()) //ParseInLocation以指定的时区解析时间 location, err := time.LoadLocation("Asia/Shanghai") if err != nil { fmt.Println("时区解析失败") return } timeObj,err:=time.ParseInLocation("2006-01-02 15:04:05","2016-05-02 15:04:05",location) if err != nil { fmt.Println("按照时区解析时间失败",err) return } timeObj1,err:=time.Parse("2006-01-02 15:04:05","2016-05-02 15:04:05") //为什么我Parse和ParseInLocation出来的同一时间还差了8小时呢? subValue:=timeObj1.Sub(timeObj) fmt.Println(subValue) fmt.Println(timeObj.Zone()) //CST 28800 fmt.Println(timeObj1.Zone())//UTC 0 }
I was absent from the APEC In Beijing。//我缺席了北京的APEC会议
His wife often complains about his frequent absences. //他老婆经常抱怨她缺少他的陪伴
in the absence of 短语
No one can live a healthy and normal life in absence of sleep. //没有人能活的健康和正常生活缺乏睡眠。
runtime
package main import ( "fmt" "runtime" ) //runtime.Caller() //可传可以不传...可变长,可以没有,可以有1个 func f1(n ...interface{}) { pc, file, line, ok := runtime.Caller(3) //runtime.Caller(skip) //0层: main.f1 //1层:main.f2 //2层:main.f3 if !ok { fmt.Printf("runtime.Caller faild.error\n") return } fmt.Println(runtime.FuncForPC(pc).Name()) //函数名 fmt.Println(file) //哪个文件调用了我 fmt.Println(line) //文件的哪1行 } func f2() { f1() } func f3() { f2() } func main() { f3() }
github.com/docker/docker/pkg/reexec
如果想要在当前父进程中开启1个子进程;
常规的做法:分别开发出父进程和子进程2份代码,分别称为父进程代码和子进程代码;
然后先运行父进程代码,在父进程代码中运行子进程的代码,开启子进程;
reexec包可以实现在使用1份代码(父进程和子进程代码合并) 的前提下,在父进程开启子进程;
使Golang像C语言一样,在父进程fork出子进程
后,让子进程执行父进程代码中定义的子进程代码块,例如父进程中定义的1个函数。
Golang开启进程
package main import ( "github.com/docker/docker/pkg/reexec" "log" "os" ) func init() { log.Printf("init start, os.Args = %+v\n", os.Args) //reexec注册命令 reexec.Register("child_Process", childProcess) //判断是否是fork if reexec.Init() { log.Println("i am a childProcess") os.Exit(0) } log.Println("main Process initializer") } func childProcess() { log.Println("childProcess") } func main() { //打印开始 log.Printf("main start, os.Args = %+v\n", os.Args) //执行子函数 cmd := reexec.Command("child_Process") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { log.Panicf("failed to run command: %s", err) } if err := cmd.Wait(); err != nil { log.Panicf("failed to wait command: %s", err) } log.Println("main exit") }
执行流程
- go会主动先执行init方法,打印init start, os.Args = .....信息,这时,参数只有主进程文件名"/tmp/GoLand/___1go_build_main_go"
- init中reexec.Register会将childProcess注册为"child_Process"名字(特意两个名字不同)
- 执行reexec.Init判断是不是子进程,判断不是,打印"main Process initializer"
- 开始执行主函数,打印main start, os.Args = ......
- 调用reexec.Command,获取注册的child_Process方法,进行执行。
- 使用Start方法执行方法,并使用Wait方法等待子进程退出
- 子进程方面:
- 子进程执行还是会先走init方法,打印init start, os.Args = .....,此时参数则为child_Process
- reexec.Register发现方法存在不会重复注册。
- reexec.Init判断是子进程(此说法有点小问题,不影响理解,具体原因请看下文源码分析),进入
- 子进程调用,打印i am a childProcess并退出
- 子进程退出,且没有错误,主进程继续执行,打印main exit后程序全部退出
通过上述流程可以看到,也就是说docker通过reexec.Register注册命令,通过reexec.Command执行注册的方法,如果子进程与主进程冲突,比如主进程的配置更新等,可以使用reexec.Init判断是否是主进程
Register
以下是Register的源码,reexec维护了1个registeredInitializers变量,变量类型为map,key是string类型,value是func类型;
注意registeredInitializers变量不是各个进程的共享内存,会被开启的进程反复创建;
var registeredInitializers = make(map[string]func()) // Register adds an initialization func under the specified name func Register(name string, initializer func()) { if _, exists := registeredInitializers[name]; exists { panic(fmt.Sprintf("reexec func already registered under name %q", name)) } registeredInitializers[name] = initializer }
Register需要两个参数,比如刚才的程序
reexec.Register("child_Process", childProcess)
这里将"child_Process"做为键childProcess方法作为值,存储到了registeredInitializers中。
存储之前会判断"child_Process"这个键是否存在,如果存在会执行panic方法,所以Register不会注册相同键名字的方法
Command
reexec.Command()
可以修改进程的名称 os.Args[0]
,
// Self returns the path to the current process's binary. // Returns "/proc/self/exe". func Self() string { return "/proc/self/exe" } // Command returns *exec.Cmd which has Path as current binary. Also it setting // SysProcAttr.Pdeathsig to SIGTERM. // This will use the in-memory version (/proc/self/exe) of the current binary, // it is thus safe to delete or replace the on-disk binary (os.Args[0]). func Command(args ...string) *exec.Cmd { return &exec.Cmd{ Path: Self(), Args: args, SysProcAttr: &syscall.SysProcAttr{ Pdeathsig: unix.SIGTERM, }, } }
Init
Init方法比较简单,判断当前进程的名称是否在registeredInitializers中存在?
如果不存在则返回False,如果存在则执行方法,执行完成后返回true;
// Init is called as the first part of the exec process and returns true if an // initialization function was called. func Init() bool { initializer, exists := registeredInitializers[os.Args[0]] if exists { initializer() return true } return false }
reflect 反射包
我们经常使用的序列化和反序列化是怎么实现的? 代码<------>字符串
package main import ( "encoding/json" "fmt" ) //结构体 type person struct { Name string `json:"name"` Age int `json:"age"` } //字符串 func main() { str:=`{"name":"Martin","age":18}` var p person //Unmarshal做了什么?让1个字符串的值,赋值给了p结构体 json.Unmarshal([]byte(str),&p) // fmt.Println(p.Name,p.Age)//Martin 18 }
Django orm里modal.py写的class是怎么对应成Mysql中的表的呢? 代码---->字符串
我们运维写得的 配置文件信如何加载到程序里面的呢? 字符串---->代码
任何接口类型(变量)都是由类型和值构成我们可以使用reflect的typeof()和reflect.valueof()获取任意变量到这2种信息。
1.TypeOf func(i interface{}) Type {}
reflect.TypeOf ()函数获取接口变量当前的类型
package main import ( "fmt" "reflect" ) type person struct{ name string age int16 } func reflectType(arg interface{}) { v := reflect.TypeOf(arg) fmt.Printf("%v %v\n", v.Kind(),v.Name()) } func main() { var name string name="Martin" reflectType(name)//string string var age int16 age=19 //如果是struct类型如何拿到 struct种类的Person类型名称?v.Name( reflectType(age)//int16 int16 p1:=person{ name:"Martin", age:20, } reflectType(p1)//struct person //func函数类型 reflectType(reflectType)//func }
2.reflect.valueof()
reflect.ValueOf()获取接口变量当前的值
reflect.Value
与原始值之间可以互相转换。
方法 | 说明 |
---|---|
Interface() interface {} | 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以 bool 类型返回 |
Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
String() string | 将值以字符串类型返回 |
package main import ( "fmt" "reflect" ) type person struct { name string age int16 } func reflectValue(arg interface{}) { //获取变量的值 reflectValue:= reflect.ValueOf(arg) //判断变量值的早reflect包中的类型 argkind := reflectValue.Kind() switch argkind { //如果在reflect包中值的类型为reflect.Int64 case reflect.Int16: //就转换为Go内置的int64类型 fmt.Printf("type is int16, value is %d\n", int64(reflectValue.Int())) } } func main() { var name string name = "Martin" reflectValue(name) //string string var age int16 age = 19 //如果是struct类型如何拿到 struct种类的Person类型名称?v.Name( reflectValue(age) //int16 int16 p1 := person{ name: "Martin", age: 20, } reflectValue(p1) //struct person //func函数类型 reflectValue(reflectValue) //func }
2.1获取到变量的值之后修改值
Elem()
方法来获取指针对应的值。package main import ( "fmt" "reflect" ) //通过返回设置 变量的值 func setValue(arg interface{}) { //获取变量的值 reflectValue := reflect.ValueOf(arg) //设置变量的值 if reflectValue.Kind() == reflect.Int16 { //找到指针类型的变量内存地址,把变量的值设置为200 reflectValue.Elem().SetInt(200) } } func main() { var age int16 age = 19 setValue(&age) fmt.Println(age) }
2.2.isNil()和isValid()
获取到值之后,判断值里面 是否包含某个方法 / 字段(是否存在)
类似于Python中 getattr(obj,"method_name " )
package main import ( "fmt" "reflect" ) type person struct { name string age uint8 } //注意要struct的方法要大写否则找不到!哈哈哈 func(p person)Walk(){ fmt.Printf("%s is walking!",p.name) } func main() { p1 := person{ name: "Martin", age: 19, } fmt.Println("不存在的结构体成员:",reflect.ValueOf(p1).FieldByName("name").IsValid()) if reflect.ValueOf(p1).MethodByName("Walk").IsValid() { method :=reflect.ValueOf(p1).MethodByName("Walk") method.Call([]reflect.Value{})//调用方法 } }
loadini文件
根据ini文件配置生成go中的结构体
package main import ( "errors" "fmt" "io/ioutil" "reflect" "strconv" "strings" ) //Mysqlconfig 结构体 type Mysqlconfig struct { Address string `ini:"address"` Port int `ini:"port"` Username string `ini:"username"` Password string `ini:"password"` Debug bool `ini:"debug"` } //Redisconfig 结构体 type Redisconfig struct { Host string `ini:"host"` Port int `ini:"port"` Username string `ini:"username"` Database string `ini:"database"` } //配置文件 type config struct { Mysqlconfig `ini:"mysql"` Redisconfig `ini:"redis"` } func loadIni(fileName string, config interface{}) (err error) { //判断config是否为指针类型的struct t := reflect.TypeOf(config) if t.Kind() != reflect.Ptr || t.Elem().Kind() == reflect.Struct { err = fmt.Errorf("请输入1个指针类型的 config struct参数!") } fileBytes, err := ioutil.ReadFile(fileName) if err != nil { err = errors.New("请检查配置文件是否存在?") } lineSlice := strings.Split(string(fileBytes), "\r\n") var sectionName string for lineNuber, line := range lineSlice { line = strings.TrimSpace(line) //空行 if len(line) < 1 { continue } //注释 if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { continue } //加载section if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { sectionName = line[1 : len(line)-1] if len(sectionName) < 1 { err = fmt.Errorf("select:%s %d 语法错误", sectionName, lineNuber) return } //变量配置struct的字段 for i := 0; i < t.Elem().NumField(); i++ { field := t.Elem().Field(i) //获取config中的ini信息 if sectionName == field.Tag.Get("ini") { sectionName = field.Name //拿到config结构体中的 嵌套结构体信息 break } } } else { //secetion中的内容 if strings.Index(line, "=") == -1 || strings.HasPrefix(line, "=") { err = fmt.Errorf("%d语法错误", lineNuber) return } //从文件中获取==的index index := strings.Index(line, "=") //从文件中 获取属性 key := strings.TrimSpace(line[:index]) //从文件中获取值 value := strings.TrimSpace(line[index+1:]) v := reflect.ValueOf(config) structValue := v.Elem().FieldByName(sectionName) structType := structValue.Type() //config中嵌套的结构体 if structType.Kind() != reflect.Struct { err = fmt.Errorf("请设置config中的%s字段为结构体", sectionName) return } //嵌套结构体里面具体的tag获取到字段 var StructFiledName string var StructFiledType reflect.StructField for i := 0; i < structValue.NumField(); i++ { filed := structType.Field(i) StructFiledType = filed //mysql =mysql if filed.Tag.Get("ini") == key { StructFiledName = filed.Name break } } //根据StructFiledName给嵌套结构体里面具体的field获取到值 structObj := structValue.FieldByName(StructFiledName) if len(StructFiledName) < 1 { //在嵌套结构体中找不到对应的字段 err = fmt.Errorf("Warning在嵌套结构体%s中找不到对应字段%s", StructFiledName, StructFiledName) continue } switch StructFiledType.Type.Kind() { case reflect.String: structObj.SetString(value) case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: var valueInt int64 valueInt, err = strconv.ParseInt(value, 10, 64) if err != nil { err = fmt.Errorf("第%d init类型解析错误", lineNuber) return } structObj.SetInt(valueInt) case reflect.Bool: var valueBool bool valueBool, err = strconv.ParseBool(value) if err != nil { err = fmt.Errorf("第%d bool类型解析错误", lineNuber) return } structObj.SetBool(valueBool) case reflect.Float32, reflect.Float64: var valuefloat float64 //字符串解析成其他类型数据 valuefloat, err = strconv.ParseFloat(value, 64) if err != nil { err = fmt.Errorf("第%d bool类型解析错误", lineNuber) return } structObj.SetFloat(valuefloat) } } } return } func main() { var lC config err := loadIni("./log.ini", &lC) if err != nil { fmt.Println("加载配置文件失败", err) } fmt.Printf("%#v\n", lC.Mysqlconfig) fmt.Printf("%#v\n", lC.Redisconfig) }
strconv
根据字符串面值 转换为Go中的数据类型
Go语言是强类型的语言:如果Go本身不支持2种数据类型直接的转换,是不能强制转换的。
package main import ( "fmt" "strconv" ) var n1 int var n2 int32 var name string func main() { //支持相互强制转换的类型 name = "MAEJH" n1 = 250 n2 = 1280000000 //int32可以转换为init64 fmt.Println(int64(n2)) //字符串可以转换为byte类型的切片 fmt.Println([]byte(name)) //把数字转换为字符串 s1 := fmt.Sprintf("%d", 250) fmt.Println(s1) name = "250" //strconv把字符串对应的数据: //转换为10进制的int64位数字 ret, _ := strconv.ParseInt(name, 10, 64) fmt.Println(ret) //Atoi()arry(c中的字符串)to int retInt, _ := strconv.Atoi(name) fmt.Println(retInt) //Itoa:把int转换为字符串 reSrting:= strconv.Itoa(int(100)) fmt.Println(reSrting) //从字符串中解析 相应的数据类型 name="true" fmt.Println(strconv.ParseBool(name)) name="1.3456" fmt.Println(strconv.ParseFloat(name,32)) }
net网络相关
golang的net包封装了实现传输层、应用层协议的API 。
我们使用net包可以写1个遵循TCP/UPD协议的socket程序,也可以写1个遵循http协议的 web应用程序。
1.TCP服务端和客户端实现
TCP server
package main import ( "fmt" "net" ) //tcp server func processConn(conn net.Conn) { defer conn.Close() // 关闭连接 //有人来连接我!就与客户通信 var data [1024]byte //持续接收 来自每1个客户端连接通信 for { lengh, err := conn.Read(data[:]) if err != nil { fmt.Println("客户端发送数据失败", err) return } fmt.Println(string(data[:lengh])) } } func main() { //0.本地端口启动服务 listener, err := net.Listen("tcp", "127.0.0.1:8001") if err != nil { fmt.Println("start server faild", err) return } for { //2.server 接收到 来着client的 syn包,回复 syn+ack conn, err := listener.Accept() if err != nil { fmt.Println("客户端连接失败", err) } go processConn(conn) //轻松实现大并发啊 哈哈! } }
TCP client
package main import ( "bufio" "fmt" "net" "os" "strings" ) //tcp client func main() { //1.与server(127.0.0.1:8001)端建立连接(1.client 发送 syn包给 server) conn, err := net.Dial("tcp", "127.0.0.1:8001") if err != nil { fmt.Println("The socket you dialed is bussy now,pleale redial agin later!", err) } //2.发送数据 reader := bufio.NewReader(os.Stdin) for { fmt.Print("请输入内容: ") msg, _ := reader.ReadString('\n') msg = strings.TrimSpace(msg) if msg == "exit" { break } conn.Write([]byte(msg)) } conn.Close() }
TCP粘包问题
TCP tcp数据传递模式是流模式(水流)。
“粘包”可发生在发送端也可发生在接收端:
- 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
- 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
客户端发送数据
我们可以自己定义一个协议,比如数据包的前4个字节为包头(固定大小),用来存储要发送数据的长度。
如何保证数据包的包头为固定大小呢?
golang中int32数据类型的变量为4个字节!
在网络上传输的都是字节数据,golangGo里面内建仅支持UTF8字符串编码,所以我们不需要考虑编码的问题,直接使用 bytes.buffer把数据发送到网络!
服务端接收到数据
先获取4个字节的信息(也就是要接收数据的元数据)
根据元数据获取指定长度的数据。
package proto import ( "bufio" "bytes" "encoding/binary" ) // Encode 将消息编码 func Encode(message string) ([]byte, error) { // 读取消息的长度,转换成int32类型(占4个字节) var length = int32(len(message)) var pkg = new(bytes.Buffer) // 写入消息头 err := binary.Write(pkg, binary.LittleEndian, length) if err != nil { return nil, err } // 写入消息实体 err = binary.Write(pkg, binary.LittleEndian, []byte(message)) if err != nil { return nil, err } return pkg.Bytes(), nil } // Decode 解码消息 func Decode(reader *bufio.Reader) (string, error) { // 读取消息的长度 lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据 lengthBuff := bytes.NewBuffer(lengthByte) var length int32 err := binary.Read(lengthBuff, binary.LittleEndian, &length) if err != nil { return "", err } // Buffered返回缓冲中现有的可读取的字节数。 if int32(reader.Buffered()) < length+4 { return "", err } // 读取真正的消息数据 pack := make([]byte, int(4+length)) _, err = reader.Read(pack) if err != nil { return "", err } return string(pack[4:]), nil }
2.UPD服务端和客户端
UDP server
UDP协议用户数据报协议(UDP,User Datagram Protocol)
UDP和TCP最大的区别就是UDP 为应用程序提供了一种无需建立连接(3次握手)就可以发送封装的 IP 数据包的方法。
// UDP/server/main.go // UDP server端 func main() { listen, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err != nil { fmt.Println("listen failed, err:", err) return } defer listen.Close() for { var data [1024]byte n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据 if err != nil { fmt.Println("read udp failed, err:", err) continue } fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n) _, err = listen.WriteToUDP(data[:n], addr) // 发送数据 if err != nil { fmt.Println("write to udp failed, err:", err) continue } } }
UDP client
UDP 客户端 func main() { socket, err := net.DialUDP("udp", nil, &net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err != nil { fmt.Println("连接服务端失败,err:", err) return } defer socket.Close() sendData := []byte("Hello server") _, err = socket.Write(sendData) // 发送数据 if err != nil { fmt.Println("发送数据失败,err:", err) return } data := make([]byte, 4096) n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据 if err != nil { fmt.Println("接收数据失败,err:", err) return } fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n) }
web聊天室
server
package main import ( "fmt" "net" "time" ) //用户信息结构体 type client struct { name string c chan string addr string } //所有在线用户 var onlineMap map[string]client //全局消息 var massage = make(chan string) //广播msg func msgManager() { //初始化online map onlineMap = make(map[string]client) //循环全局消息channe是否有数据 for { msg := <-massage //循环所有在线用户广播消息 for _, clen := range onlineMap { clen.c <- msg } } } //处理单个request的消息 func writeMsgToClent(c client, conn net.Conn) { for msg := range c.c { conn.Write([]byte(msg + "\n")) } } //制作用户消息 func makeMsg(clnt client, msg string) (buf string) { buf = "【" + clnt.addr + "】" + ": " + clnt.name + msg return } //处理每1个request func handlerRequest(conn net.Conn) { defer conn.Close() netAddr := conn.RemoteAddr().String() c := client{ name: netAddr, c: make(chan string), addr: netAddr, } onlineMap[netAddr] = c //创建【小管家】专门从全局消息中接收数据回复客户端 go writeMsgToClent(c, conn) //向全局消息推送消息 massage <- makeMsg(c, "上线了") //判断用户退出状态 isQuit := make(chan bool) //判断用户是否活跃 isActive := make(chan bool) //创建1个匿名go程 专门处理用户发送的消息 go func() { buf := make([]byte, 4096) for { n, err := conn.Read(buf) if n == 0 { // fmt.Printf("检查到客户端:%s退出", c.name) isQuit <- true return } if err != nil { fmt.Printf("客户端%s连接出错", c.name) return } msg := string(buf[:n]) //查看在线用户 if msg == "who" { for _, user := range onlineMap { conn.Write([]byte(user.name)) } //用户输入了exit } else if msg == "exit" { isQuit <- true } else { //将读到的消息广播给在线用户 fmt.Println(msg) massage <- makeMsg(c, msg) } //keep alive isActive <- true } }() //循环检查request conn的状态 for { select { //用户退出 case <-isQuit: //为了关闭子gorutine也要关闭 close(c.c) delete(onlineMap, c.name) massage <- makeMsg(c, "退出") return //用户活跃 case <-isActive: //5钟不输入超时 case <-time.After(time.Minute * 5): close(c.c) delete(onlineMap, c.name) massage <- makeMsg(c, "退出") return } } } func main() { listener, err := net.Listen("tcp", "127.0.0.1:8001") if err != nil { fmt.Println("server listen err", err) return } defer listener.Close() //创建【大管家】msgManager负责向全局map向所有在线用户广播消息 go msgManager() //循环监控客户端请求 for { conn, err := listener.Accept() if err != nil { fmt.Println("accept error", err) return } go handlerRequest(conn) } }
client
package main import ( "bufio" "fmt" "net" "os" "strings" ) //tcp client func main() { //1.与server(127.0.0.1:8001)端建立连接(1.client 发送 syn包给 server) conn, err := net.Dial("tcp", "127.0.0.1:8001") defer conn.Close() if err != nil { fmt.Println("The socket you dialed is bussy now,pleale redial agin later!", err) } //2.发送和接收数据 var serverMsg [4096]byte reader := bufio.NewReader(os.Stdin) for { n, _ := conn.Read(serverMsg[:]) fmt.Println(string(serverMsg[:n])) fmt.Print("----->: ") msg, _ := reader.ReadString('\n') msg = strings.TrimSpace(msg) if msg == "exit" { break } conn.Write([]byte(msg)) } }
3.HTTP协议的服务端和客户端
HTTP服务端
做web开发关注这块,什么Django/Flashk/Tornado(写接口)
package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" ) //page func indexView(response http.ResponseWriter, request *http.Request) { b, err := ioutil.ReadFile("./templates/index.html") if err != nil { response.Write([]byte(fmt.Sprintf("%v\n", err))) } response.Write(b) } //api func indexAPIView(response http.ResponseWriter, request *http.Request) { //获取请求的url fmt.Println(request.URL) contentType := request.Header.Get("Content-Type") switch request.Method { case "GET": //http GET请求参数都放在请求头中,请求体里面没有数据! //获取请求方法 fmt.Println(request.Method) //GET //获取Get请求参数 quaryParam := request.URL.Query() fmt.Println(quaryParam.Get("dataType")) //文件 //request.MultipartForm //在服务端获取request请求的body fmt.Println(ioutil.ReadAll(request.Body)) response.Write([]byte("string oK")) case "POST": switch contentType { //处理form提交 case "application/x-www-form-urlencoded": if err := request.ParseForm(); err != nil { fmt.Fprintf(response, "ParseForm() err: %v", err) return } name := request.FormValue("name") age := request.FormValue("age") fmt.Println(name, age) response.Write([]byte("form oK")) //处理josn提交 case "application/json": //设置1个映射json的struct type person struct { Name string `json:"name" db:"name" ini:"name"` Age uint8 `json:"age" db:"name" ini:"name"` } var p1 person //获取[]byte body, err := ioutil.ReadAll(request.Body) if err != nil { fmt.Printf("read body err, %v\n", err) return } //解析 json.Unmarshal(body, &p1) fmt.Println(p1.Name) // type data struct { Status bool `json:"status"` Message string `json:"message"` } d := data{ Status: true, Message: "application/json", } b, _ := json.Marshal(d) response.Write(b) } } } func main() { http.HandleFunc("/index/", indexView) http.HandleFunc("/api/index/", indexAPIView) //放在路由的下面 http.ListenAndServe("127.0.0.1:8001", nil) }
HTTP客户端
做运维、测试、爬虫关注这块(接口测试、调用个API、写个爬虫)
package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "strings" ) func main() { //1.模拟浏览器向server端发起GET请求 response, err := http.Get("http://127.0.0.1:8001/api/index/?dataType=string") if err != nil { fmt.Println("请求错误") return } //把返回的数据读出来 //在Golang中不管是文件、网络IO都实现了Read方法都可以使用ioutill读取数据 b, err := ioutil.ReadAll(response.Body) if err != nil { fmt.Println("Read faild") } fmt.Println(string(b)) //2.模拟浏览器发起form Post请求 formData := make(url.Values) formData.Add("name", "张根") formData.Add("age", "18") formPayLoad := formData.Encode() formRequest, _ := http.Post( "http://127.0.0.1:8001/api/index/", "application/x-www-form-urlencoded", strings.NewReader(formPayLoad), ) formResponse, _ := ioutil.ReadAll(formRequest.Body) fmt.Println(string(formResponse)) formRequest.Body.Close() //发 json数据 josnData := struct { Name string `json:"name"` Age int `json:"age"` }{ Name: "张根", Age: 18, } josnPayLoad, _ := json.Marshal(josnData) josnRequest, _ := http.Post( "http://127.0.0.1:8001/api/index/", "application/json", bytes.NewReader(josnPayLoad), ) josnResponse, _ := ioutil.ReadAll(josnRequest.Body) fmt.Println(string(josnResponse)) josnRequest.Body.Close() }
设置http客户端head参数
在爬虫请求次数频繁的业务场景下: 使用共用1个固定的HTTP客户端设置keepalive, 限制服务器开启socket的数量和每次建立TCP连接的开销。
在爬虫请求次数不频繁的业务场景下: 使用多个HTTP客户端禁用 keepalive保证数据获取完毕之后socket资源及时的释放掉。
package main import ( // "crypto/tls" "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net/http" ) func main() { //创建1个保存所有网站CA证书的pool pool := x509.NewCertPool() //CA证书的路径 caCertPath := "ca.crt" //获取CA证书 caCrt, err := ioutil.ReadFile(caCertPath) //CA证书获取失败 if err != nil { fmt.Println("ReadFile err:", err) return } //把证书添加到证书pool pool.AppendCertsFromPEM(caCrt) tr := &http.Transport{ //支持TLS(https) TLSClientConfig: &tls.Config{RootCAs: pool}, //支持压缩包 DisableCompression: true, //禁用keepalive:根据情况 DisableKeepAlives: true, } client := &http.Client{Transport: tr} resp, _ := client.Get("https://example.com") formResponse, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(formResponse)) //记得关闭客户端连接 resp.Body.Close() }
4.WebSockt协议的服务端和客户端
websocket是1种在单个TCP连接上利用http协议发送握手信息后,客户端和服务端捂手建立全双工通信的协议。
特点:服务端也可以主动向客户端推送消息。
get -u -v github.com/gorilla/websocket
flag
flag包可以帮助我们获取解析命令行参数(用户调用执行go程序时后面指定的参数),比Python中的sys包中的sys.argv功能强大些 。写一些go脚本的时候蛮合适的!
os.Args
package main import ( "fmt" "os" ) func main() { //os.Args()获取命令行用户输入,是1个切片类型的变量 fmt.Println(os.Args) //和Python和shell一致:第一个参数是程序本身 fmt.Println(os.Args[0]) //其他参数以此类推 }
flag包基本使用
etct start --gotuineCount=10000
如果我想在支持程序时 支持这种调用方式,os.Args将会不容易实现。
flag包支持的命令行参数类型有bool
、int
、int64
、uint
、uint64
、float
float64
、string
、duration
。
定义命令行flag参数
flag.String(解析为指针类型)
package main import ( "flag" "fmt" "time" ) //flag获取并解析命令行参数 func main() { //创建1个标志位参数:参数名为name,Martin为默认值,然后是提示帮助信息 name := flag.String("name", "Martin", "请输入姓名") age := flag.Int("age", 18, "请输入年龄") married := flag.Bool("married", false, "请输入婚否?") timeOfmarriage := flag.Duration("tofm", time.Hour*1, "结婚多久了?") flag.Parse() //注意 flag返回的是指针类型的变量 fmt.Println(*name) fmt.Println(*age) fmt.Println(*married) fmt.Println(*timeOfmarriage) fmt.Printf("%T\n", *timeOfmarriage) }
flag.StringVar(非指针类型)
如果以上面的方式解析命令行参数,返回的是指针类型的变量,不方便你在程序你使用它!
package main import ( "flag" "fmt" "time" ) //flag获取并解析命令行参数 func main() { var ( name string age int married bool timeOfmarriage time.Duration ) //flag.StringVar flag.StringVar(&name, "name", "Martin", "请输入姓名") flag.IntVar(&age,"age", 18, "请输入年龄") flag.BoolVar(&married,"married", false, "请输入婚否?") flag.DurationVar(&timeOfmarriage,"tofm", time.Hour*1, "结婚多久了?") flag.Parse() fmt.Println(name) fmt.Println(age) fmt.Println(married) fmt.Println(timeOfmarriage) fmt.Printf("%T\n", timeOfmarriage) //其他方法 fmt.Println(flag.Args())//获取没有设置了命令标志(-或--)的参数 fmt.Println(flag.NArg())//获取所有参数的个数 fmt.Println(flag.NFlag())//获取设置了命令标志(-或--)的参数 使用前有-的参数个数 }
结果
D:\goproject\src\hello\os.args>main.exe -name=tom -tofm=100h s tom 18 false 100h0m0s time.Duration [s] 1 2
Go语言操作MySQL
database/sql标准库
我们要连接数据库需要使用database/sql标准库,但是golang内置的标准库databse/sql没有具体连接数据库,只是列出 第三方库需要实现连接的内容。
所以需要下载第三方的驱动。
func init() { sql.Register("mysql", &MySQLDriver{}) }
go-sql-driver/mysql驱动下载
update 所有依赖到最新
go get -u github.com/go-sql-driver/mysql
database/sql包原生实现了数据库连接池,并且并发安全。
创建连接池
go-sql-driver原生支持连接池并支持并发,所有可以同时使用多个gorutine从连接池中获取数据库连接。
package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) //全局连接池对象 var db *sql.DB func initDB() (err error) { //数据库信息 dsn := "zhanggen:123.com@tcp(192.168.56.133:3306)/golang" //验证数据库信息 db, err = sql.Open("mysql", dsn) if err != nil { return } //尝试连接数据库 err = db.Ping() if err != nil { return } //最大连接 db.SetMaxOpenConns(20) //最大空闲连接数(至少保留几个连接) db.SetMaxIdleConns(5) return }
QueryRow(查询单条记录)
package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) //全局连接池对象 var db *sql.DB func initDB() (err error) { //数据库信息 dsn := "zhanggen:123.com@tcp(192.168.56.133:3306)/golang" //验证数据库信息 db, err = sql.Open("mysql", dsn) if err != nil { return } //尝试连接数据库 err = db.Ping() if err != nil { return } //最大连接 db.SetMaxOpenConns(20) //最大空闲连接数(至少保留几个连接) db.SetMaxIdleConns(5) return } type user struct { name string sex string age int } func insertOne() { sqlStr := "insert into studentds_info(name,sex,age) values (?,?,?)" ret, err := db.Exec(sqlStr, "王五", "男", 38) if err != nil { fmt.Printf("insert failed, err:%v\n", err) return } theID, err := ret.LastInsertId() // 新插入数据的id if err != nil { fmt.Printf("get lastinsert ID failed, err:%v\n", err) return } fmt.Printf("insert success, the id is %d.\n", theID) } //QueryRow查询1条记录 func queryOne(sql string) { //从连接池中获取1个连接 rowObj := db.QueryRow(sql) //获取结果 var user1 user //获取数据库对象并释放连接 rowObj.Scan(&user1.name, &user1.sex, &user1.age) //打印结果 fmt.Printf("%#v", user1) } func main() { err := initDB() if err != nil { fmt.Println("数据库连接失败", err) return } fmt.Println("数据库连接成功!") // insertOne() sql := "select name,sex,age from studentds_info where id=1" queryOne(sql) }
Query(查询多条记录)
func queryMore(sql string) { ret := make([]user, 0, 2) //0sql的参数 rows, err := db.Query(sql, 0) if err != nil { fmt.Println("query 查询失败", err) return } //query一定要手动释放持有的数据库连接到连接池(否则连接池资源枯竭了!) defer rows.Close() //循环读取(只要有下一条) for rows.Next() { var u user err := rows.Scan(&u.name, &u.sex, &u.age) if err != nil { fmt.Println("scan失败", err) return } //封装到slince [user1,user2....] ret = append(ret, u) } fmt.Printf("%#v\n", ret) } func main() { err := initDB() if err != nil { fmt.Println("数据库连接失败", err) return } fmt.Println("数据库连接成功!") sql := "select name,sex,age from studentds_info where age >?;" queryMore(sql) }
Exec(insert/update/remove)
在更新数据时一定要记得 rows.RowsAffected()使update sql语句生效。跟pymysql的commit()一样!
package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) //全局连接池对象 var db *sql.DB func initDB() (err error) { //数据库信息 dsn := "zhanggen:123.com@tcp(192.168.56.133:3306)/golang" //验证数据库信息 db, err = sql.Open("mysql", dsn) if err != nil { return } //尝试连接数据库 err = db.Ping() if err != nil { return } //最大连接 db.SetMaxOpenConns(20) //最大空闲连接数(至少保留几个连接) db.SetMaxIdleConns(5) return } type user struct { name string sex string age int } //插入数据 func insertRow(sql string) { ret, err := db.Exec(sql, "王五1", "男", 38) if err != nil { fmt.Printf("insert failed, err:%v\n", err) return } // 新插入数据的id theID, err := ret.LastInsertId() if err != nil { fmt.Printf("get lastinsert ID failed, err:%v\n", err) return } fmt.Printf("insert success, the id is %d.\n", theID) } //update数据 func updateRow(sql string){ rows, err :=db.Exec(sql,18,"王五1") if err != nil { fmt.Printf("更新数据failed, err:%v\n", err) return } //使update sql语句生效 n, err := rows.RowsAffected() if err != nil { fmt.Printf("没有查询到 err:%v\n", err) return } fmt.Printf("更新完成:受影响的行 %d.\n", n) } //QueryRow查询1条记录 func queryOne(sql string) { //从连接池中获取1个连接 rowObj := db.QueryRow(sql) //获取结果 var user1 user //获取数据库对象并释放连接 rowObj.Scan(&user1.name, &user1.sex, &user1.age) //打印结果 fmt.Printf("%#v", user1) } func queryMore(sql string) { ret := make([]user, 0, 2) //0是sql占位符?需要替换的参数 rows, err := db.Query(sql, 0) if err != nil { fmt.Println("query 查询失败", err) return } //query一定要手动释放持有的数据库连接到连接池(否则连接池资源枯竭了!) defer rows.Close() //循环读取(只要有下一条) for rows.Next() { var u user err := rows.Scan(&u.name, &u.sex, &u.age) if err != nil { fmt.Println("scan失败", err) return } //封装到slince [user1,user2....] ret = append(ret, u) } fmt.Printf("%#v\n", ret) } func main() { err := initDB() if err != nil { fmt.Println("数据库连接失败", err) return } fmt.Println("数据库连接成功!") sql:="insert into studentds_info(name,sex,age) values (?,?,?)" insertRow(sql) sql="update studentds_info set age=? where name=?" updateRow(sql) sql= "select name,sex,age from studentds_info where age >?;" queryMore(sql) }
事物操作
事物就是保证 N个SQL组合成1组:它们要么全部执行! 要么全部不执行!它们之间是1个不可分割的工作单位。
func transaction(){ //开始事物操作 tx,err:=db.Begin() if err!=nil{ fmt.Println("事物开启失败") return } //执行1组sql操作 addSQL:="update studentds_info set age=age+1 where name=?" subSQL:="update studentds_info set age=age-1 where name=?" //执行给王五加钱操作 _,err=tx.Exec(addSQL,"王五") if err!=nil{ //加钱时断电了要回滚 tx.Rollback() fmt.Println("加钱时断电了,已经回滚") return } //执行给二狗减钱操作 _,err=tx.Exec(subSQL,"二狗") if err!=nil{ //减钱时断电了要回滚 tx.Rollback() fmt.Println("减钱时断电了,已经回滚") } //提交事物操作 tx.Commit() fmt.Println("二狗赠送王五大宝剑成功!") }
sqlx使用
sqlx也是1个golang操作MySQL的 第三方库,和go-sql-driver驱动相比 sqlx使用了反射机制,在变量赋值的时候
能够简化操作,提高开发效率。
安装
go get github.com/jmoiron/sqlx
连接池
package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) var db *sqlx.DB func initDB() (err error) { dbinfo := "zhanggen:123.com@tcp(192.168.56.133:3306)/golang" db, err = sqlx.Connect("mysql", dbinfo) if err != nil { fmt.Println("数据库连接错误!") return } db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) return }
Get和Select查询
//go-sql var u user err := rows.Scan(&u.name, &u.sex, &u.age) //sqlx var u user db.Get(&u, sql, 38)
package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) var db *sqlx.DB func initDB() (err error) { dbinfo := "zhanggen:123.com@tcp(192.168.56.133:3306)/golang" db, err = sqlx.Connect("mysql", dbinfo) if err != nil { fmt.Println("数据库连接错误!") return } db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) return } type user struct { Name string Sex string Age int } func main() { err := initDB() if err != nil { fmt.Println("sqlx初始化失败", err) return } sql := "select name,sex,age from studentds_info where age=?" /* sqlx和go-sql-driver的差别主要体现在这里! err := rows.Scan(&u.name, &u.sex, &u.age) db.Get(&u, sql, 38) 由于内部源码是 reflect实现所以,我们无需scan 结构体的所有字段 但是为了给结构体赋值,我们必须传递指结构体针保、字段大写,才能被起码包调用到! */ var u user //注意Get()必须接受指针类型struct 的变量,而且字段大写! db.Get(&u, sql, 38) fmt.Printf("%#v\n", u) var userList []user sql = "select name,sex,age from studentds_info where age>?" db.Select(&userList, sql, 18) fmt.Printf("%#v\n", userList) }
Exec(insert/update/remove)
package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) var db *sqlx.DB type user struct { Name string Sex string Age int } func initDB() (err error) { dbinfo := "zhanggen:123.com@tcp(192.168.56.133:3306)/golang" db, err = sqlx.Connect("mysql", dbinfo) if err != nil { fmt.Println("数据库连接错误!") return } db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) return } func insert(SQL string) { row, err := db.Exec(SQL, "sss", "男", 19) if err != nil { fmt.Printf("插入行失败,err:%v\n", err) return } theID, err := row.LastInsertId() // 新插入数据的id if err != nil { fmt.Println("获取插入数据id失败", err) return } fmt.Println("插入数据成功id:", theID) } func update(SQL string) { ret, err := db.Exec(SQL, "Tom","男",19,"Martin") if err != nil { fmt.Printf("update failed, err:%v\n", err) return } n, err := ret.RowsAffected() // 操作影响的行数 if err != nil { fmt.Printf("get RowsAffected failed, err:%v\n", err) return } fmt.Printf("update success, affected rows:%d\n", n) } // 删除数据 func deleteRow(SQL string) { ret, err := db.Exec(SQL, "Tom") if err != nil { fmt.Printf("delete failed, err:%v\n", err) return } n, err := ret.RowsAffected() // 操作影响的行数 if err != nil { fmt.Printf("get RowsAffected failed, err:%v\n", err) return } fmt.Printf("delete success, affected rows:%d\n", n) } func main() { err := initDB() if err != nil { fmt.Println("sqlx初始化失败", err) return } sql := "insert into studentds_info(name,sex,age)value(?,?,?)" insert(sql) sql= "update studentds_info set name=?,sex=?,age=? where name=?" update(sql) sql= "delete from studentds_info where name = ?" deleteRow(sql) }
namedExec
通过结构体修改数据库行
//name exec func namedExec(SQL string) (err error) { _, err = db.NamedExec(SQL, map[string]interface{}{ "name": "张根", "sex": "男", "age": 26, }) return } func main() { err := initDB() if err != nil { fmt.Println("sqlx初始化失败", err) return } sql := "insert into studentds_info (name,sex,age) values (:name,:sex,:age)" namedExec(sql) }
sqlx事物操作
unc transactionDemo2()(err error) { tx, err := db.Beginx() // 开启事务 if err != nil { fmt.Printf("begin trans failed, err:%v\n", err) return err } defer func() { if p := recover(); p != nil { tx.Rollback() panic(p) // re-throw panic after Rollback } else if err != nil { fmt.Println("rollback") tx.Rollback() // err is non-nil; don't change it } else { err = tx.Commit() // err is nil; if Commit returns error update err fmt.Println("commit") } }() sqlStr1 := "Update user set age=20 where id=?" rs, err := tx.Exec(sqlStr1, 1) if err!= nil{ return err } n, err := rs.RowsAffected() if err != nil { return err } if n != 1 { return errors.New("exec sqlStr1 failed") } sqlStr2 := "Update user set age=50 where i=?" rs, err = tx.Exec(sqlStr2, 5) if err!=nil{ return err } n, err = rs.RowsAffected() if err != nil { return err } if n != 1 { return errors.New("exec sqlStr1 failed") } return err }
sql注入
sql注入产生的root cause 是字符串拼接。
select name,sex,age from studentds_info where name='xxx' or 1=1 #别拼了
如果SQL是拼接而成的就意味着这条SQL出现了缝隙,有了这个缝隙hack就可以乘虚而入: 迎合sql前一部分【干自己想干的】注释掉后一部分。
真佩服第1个搞这个事情的人!~~~~~方法总比困难多!!!
func sqlInject(name string){ //自己拼接sql语句 sql:=fmt.Sprintf("select name,sex,age from studentds_info where name='%s'",name) // fmt.Println(sql) var users []user err:=db.Select(&users,sql) if err!=nil{ fmt.Println("查询失败",err) return } fmt.Println(users) } func main() { err:=initDB() if err!=nil{ fmt.Println("初始化数据库失败") } //哈哈:这个比较绝脱裤了 name:="xxx' or 1=1 # " //select name,sex,age from studentds_info where name='xxx' or 1=1 # ' //这个不好实现因为你不知道对方的数据名称 name="xxx' and (select count(*) from studentds_info) <10 #" sqlInject(name) }
sql预处理
一条完整的sql语句=MySQL规定的sql语法部分+用户参数部分
sql预处理:就是把一条完整的sql语句划分为2部分 sql语法部分、用户参数部分,sql语句部分先发送到mysqld保存, 后期每次用户发送数据,在mysqld(服务端)完成字符串和sql语句的拼接!
在重复执行多个相同sql逻辑的sql语句时,无非就是sql要替换的字符串不同而已,所有sql预处理在这种场景下既可以提供sql的执行效率,也可以 防止sql 注入。
普通SQL语句执行过程:
- 客户端对SQL语句进行占位符替换得到完整的SQL语句。
- 客户端发送完整SQL语句到MySQL服务端
- MySQL服务端执行完整的SQL语句并将结果返回给客户端。
预处理执行过程:
- 把SQL语句分成两部分,命令部分与数据部分。
- 先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。
- 然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。
- MySQL服务端执行完整的SQL语句并将结果返回给客户端。
批量插入(预处理)
//插入多条记录 func insertBatch(sql string, age int) { defer wg.Done() //先把没有字符串拼接的sql发到mysql端 stmt, err := db.Prepare(sql) if err != nil { fmt.Printf("sql预处理 failed, err:%v\n", err) return } defer stmt.Close() //再把用户输入的参数发生到mysqld ret, err := stmt.Exec("二狗", "男", age) if err != nil { fmt.Printf("insert failed, err:%v\n", err) return } fmt.Printf("insert success, the id is %d.\n", ret) }
批量查询
func queryBatch(sql string) { ret := make([]user, 0, 2) //准备sql部分 stmt, err := db.Prepare(sql) if err != nil { fmt.Println("预处理失败", err) return } defer stmt.Close() //准备用户输入部分 rows, err := stmt.Query(0) if err != nil { fmt.Printf("query failed, err:%v\n", err) return } //query一定要手动释放持有的数据库连接到连接池(否则连接池资源枯竭了!) defer rows.Close() //循环读取(只要有下一条) for rows.Next() { var u user err := rows.Scan(&u.name, &u.sex, &u.age) if err != nil { fmt.Println("scan失败", err) return } //封装到slince [user1,user2....] ret = append(ret, u) } fmt.Printf("%#v\n", ret) }
go操作Redis
安装
go get -u github.com/go-redis/redis
连接
1.普通连接
func initRedis() (err error) { //给全局变量赋值!!注意不要 := redisdb = redis.NewClient(&redis.Options{ Addr: "192.168.56.133:6379", Password: "", DB: 0, }) _, err = redisdb.Ping().Result() return }
2.连接Redis哨兵模式
func initClient()(err error){ rdb := redis.NewFailoverClient(&redis.FailoverOptions{ MasterName: "master", SentinelAddrs: []string{"x.x.x.x:26379", "xx.xx.xx.xx:26379", "xxx.xxx.xxx.xxx:26379"}, }) _, err = rdb.Ping().Result() if err != nil { return err } return nil }
3.连接Redis集群
func initClient()(err error){ rdb := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"}, }) _, err = rdb.Ping().Result() if err != nil { return err } return nil }
3.set和get数据
Redis支持诸如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、带范围查询的排序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)。
package main import ( // "encoding/json" "encoding/json" "fmt" "github.com/go-redis/redis" ) //全局连接 var redisdb *redis.Client //初始化Redis func initRedis() (err error) { //给全局变量赋值!!注意不要 := redisdb = redis.NewClient(&redis.Options{ Addr: "192.168.56.133:6379", Password: "", DB: 0, }) _, err = redisdb.Ping().Result() return } type person struct { Name string `json:"name"` Age uint8 `json:"age"` } //制造json数据 func newPerson(name string, age uint8) []byte { p1 := person{ Name: name, Age: age, } datas, _ := json.Marshal(p1) return datas } func main() { err := initRedis() //ZsetKey redis中数据类型有序集合使用于排行榜、轮班啊 var ZsetKey = "Ranking11" var items = []*redis.Z{ &redis.Z{Score: 90, Member: "William1"}, &redis.Z{Score: 91, Member: "Martin2"}, &redis.Z{Score: 92, Member: "Chris1"}, &redis.Z{Score: 93, Member: "Tom1"}} if err != nil { fmt.Println("Redis连接失败", err) return } //把所有值班人员都追加到key redisdb.ZAdd(ZsetKey, items...) redisdb.ZRem(ZsetKey) // 给某个运维值班加10分 newScore, err := redisdb.ZIncrBy(ZsetKey, 10, "Martin1").Result() if err != nil { fmt.Printf("加分失败, err:%v\n", err) return } fmt.Printf("添加成功,最新分数%v\n", newScore) //获取最高的分(2个) ret, err := redisdb.ZRevRangeWithScores(ZsetKey, 0, 0).Result() if err != nil { fmt.Printf("zrevrange failed, err:%v\n", err) return } for _, z := range ret { fmt.Println(z.Member, z.Score) } //获取95~100分的 op := &redis.ZRangeBy{ Min: "80", Max: "100", } ret, err = redisdb.ZRangeByScoreWithScores(ZsetKey, op).Result() if err != nil { fmt.Printf("zrangebyscore failed, err:%v\n", err) return } for _, z := range ret { fmt.Println(z.Member, z.Score) } for _, z := range ret { fmt.Println(z.Member, z.Score) } //存储json redisdb.Set("name", newPerson("Sally", 24), 0) //获取json val, err := redisdb.Get("name").Result() var p2 person json.Unmarshal([]byte(string(val)),&p2) fmt.Printf("%#v",p2) }
redigo包
如果你感觉go-redis的API不支持直接输入redis 命令,可以使用redigo。
Features
- A Print-like API with support for all Redis commands.
- Pipelining, including pipelined transactions.
- Publish/Subscribe.
- Connection pooling.
- Script helper type with optimistic use of EVALSHA.
- Helper functions for working with command replies.
package main import ( "fmt" "github.com/gomodule/redigo/redis" "time" ) //创建Redis 连接池 func NewRdisPool(addr, pwd string) (pool *redis.Pool) { return &redis.Pool{ MaxIdle: 60, MaxActive: 200, IdleTimeout: time.Second, Dial: func() (redis.Conn, error) { conn, err := redis.Dial("tcp", addr) if err != nil { fmt.Println("连接redis数据库失败", err) _ = conn.Close() return nil, err } //如果需要密码! if len(pwd) > 1 { if _, err := conn.Do("AUTH", pwd); err != nil { _ = conn.Close() fmt.Println("密码错误", err) return conn, err } } return conn, err }, //连接redis 连接池测试 TestOnBorrow: func(c redis.Conn, t time.Time) error { _, err := c.Do("ping") return err }, } } func main() { //初始化1个连接池 pool := NewRdisPool("192.168.56.18:6379", "123.com") //从连接池中获取连接 conn := pool.Get() //最后关闭连接、关闭连接池 defer conn.Close() pool.Close() //set值 _, err := conn.Do("set", "name", "zhanggen") if err != nil { fmt.Println("set值失败") } //获取值 value, err := conn.Do("get", "name") if err != nil { fmt.Println("获取值失败") } fmt.Println(value) }
sarama
sarama是go连接kafka的模块,由于v1.19.0之后需要gcc环境如果是windown平台要下载指定版本。
下载
D:\goproject\src\go相关模块\kafka>SET GO111MODULE=on D:\goproject\src\go相关模块\kafka>SET GOPROXY=http://goproxy.cn D:\goproject\src\go相关模块\kafka>go mod init go: creating new go.mod: module go相关模块/kafka D:\goproject\src\go相关模块\kafka>go mod download go: finding github.com/Shopify/sarama v1.19.0 D:\goproject\src\go相关模块\kafka>go build go: finding github.com/rcrowley/go-metrics latest go: finding github.com/eapache/go-xerial-snappy latest go: finding github.com/golang/snappy v0.0.1 go: downloading github.com/golang/snappy v0.0.1 D:\goproject\src\go相关模块\kafka>kafka.exe 连接成功! 分区ID:0, offset:0
使用
package main import ( "fmt" "github.com/Shopify/sarama" ) func main() { config := sarama.NewConfig() config.Producer.RequiredAcks = sarama.WaitForAll //赋值为-1:这意味着producer在follower副本确认接收到数据后才算一次发送完成。 config.Producer.Partitioner = sarama.NewRandomPartitioner //写到随机分区中,默认设置8个分区 config.Producer.Return.Successes = true msg := &sarama.ProducerMessage{} msg.Topic = `nginx_log` msg.Value = sarama.StringEncoder("this is a good test") client, err := sarama.NewSyncProducer([]string{"192.168.56.133:9092"}, config) if err != nil { fmt.Println("producer close err, ", err) return } fmt.Println("Kafka连接成功!") defer client.Close() pid, offset, err := client.SendMessage(msg) if err != nil { fmt.Println("send message failed, ", err) return } fmt.Printf("分区ID:%v, offset:%v \n", pid, offset) }
taill模块
类似于Linux中的taill命令可以检测文件内容的变化,并支持文件重新打开。
什么是文件重新打开呢?
一般日志文件切割的策略是当文件内容到达1个size阀值之后,把当前的文件(nginx.log)重命名(nginx1),然后重新打开一个新的文件(nginx.log).......。
下载
go get -u github.com/hpcloud/tail
基本使用
package main import ( "fmt" "time" "github.com/hpcloud/tail" ) func main() { fileName :="mylog.txt" config := tail.Config{ ReOpen: true, //重新打开文件 Follow: true, //跟随文件 Location: &tail.SeekInfo{Offset: 0, Whence: 2}, //从文件的哪个地方开始读 MustExist: false, //文件不存在不报错 Poll: true, } tails, err := tail.TailFile(fileName, config) if err != nil { fmt.Println("文件打开失败", err) } var ( line *tail.Line ok bool ) for { line, ok = <-tails.Lines if !ok { fmt.Printf("文件:%s关闭重新打开\n", fileName) time.Sleep(time.Second) continue } fmt.Println(line.Text) } }
goini 解析ini配置文件
我们在写项目的时候使用配置文件是常见的事情,那么如何把1个配置文件转换成go程序可以识别的数据类型呢?
总不能自己reflect.TypeOf用反射实现一个,unkown实现了1个第三方包。
$ go get gopkg.in/ini.v1
conf.ini配置文件
#服务端设置 [gin] mode=debug port=8002 #mysql相关配置 [mysql] database=web host=192.168.56.18 port=3306 username=zhanggen password=123.com
定义需要映射的结构体
package config import ( "fmt" "gopkg.in/ini.v1" ) //mysql配置 tag和ini文件保持一致 type MysqlConf struct { Database string `ini:"database"` Host string `ini:"host"` Port int `ini:"port"` Username string `ini:"username"` Password string `ini:"password"` } //gin的配置 tag和ini文件保持一致 type ServerConf struct { Mode string `ini:"mode"` Port int `ini:"post"` } //一定要配置全局对象去反射ini文件里的全部session type AllConf struct { MysqlConf MysqlConf `ini:"mysql"` GinConf ServerConf `ini:"gin"` } //单例模式 var ConfogObj = new(AllConf) //开干 func init() { err := ini.MapTo(ConfogObj, "./config/conf.ini") if err!=nil{ fmt.Println("ini配置文件解析错误",err) return } }
gorm包
使用orm可以提升我们的开发效率,他是对不同数据库sql的一层完美封装。
有了orm之后作为开发人员,并不需要去专注于不同sql的编写。
Python的orm是SQLAlchemy
当然还有Django web框架中也有自己的orm,而Gang中也有1个使用起来很方便的orm工具gorm,它限于哪个web框架。
package main import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" "time" ) type User struct { gorm.Model //自动增加 id、created_at、updated_at、deleted_at列 Username string Birthday *time.Time Password string Salary int } func main() { //配置数据连接:注意设置parseTime=true否则会报错!unsupported Scan 时间类型的字段 dbinfo := "YourUserName:YourPassWord@tcp(192.168.56.18:3306)/web?charset=utf8&parseTime=true" db, err := gorm.Open(mysql.Open(dbinfo), &gorm.Config{}) if err != nil { fmt.Println(err) //panic("failed to connect database") } //配置一下数据连接参数! mySQL, err := db.DB() if err != nil { fmt.Println(err) } defer mySQL.Close() mySQL.SetMaxIdleConns(10) mySQL.SetMaxOpenConns(100) mySQL.SetConnMaxLifetime(time.Hour) // 迁移 schema表结构,指定表 db.AutoMigrate(&User{}) //删除表 //db.Migrator().DropTable(&User{}) ////Create 1条记录 //birthday:=time.Now() //db.Create(&User{Username: "Bob",Password: "2018",Birthday:&birthday,Salary: 2018}) //var userList = []User{ // {Username: "Saly",Password: "2019",Birthday:&birthday,Salary: 2019}, // {Username: "Jinzhu",Password: "2020",Birthday:&birthday,Salary: 2020}, // {Username: "WuMa",Password: "2021",Birthday:&birthday,Salary: 2021}, //} ////创建多条记录 //db.CreateInBatches(userList, len(userList)) // First查看1条记录 var person User db.First(&person) // 根据整形主键查找 fmt.Println("----------------------", person.Username) //Find 查看全部记录 var people []User db.Find(&people) fmt.Printf("本次查询到%d人----------\n", db.Find(&people).RowsAffected) for i, user := range people { fmt.Printf("%d----%s\n", i, user.Username) } //查看部分用户= SELECT * FROM users WHERE id IN (1,2,3) db.Find(&people, []int{1, 3}) fmt.Printf("本次查询到%d人----------\n", db.Find(&people, []int{1, 3}).RowsAffected) for _, user := range people { fmt.Printf("%d----%s\n", user.ID, user.Username) } // Update var changPeson = User{} db.Model(&changPeson).Where("username = ?", "Bob").Update("Username", "Jacob") fmt.Println(changPeson.Username) // Update - 更新多个字段 db.Model(User{}).Where("id in ?", []int{1, 4}).Updates(User{Salary: 200, Password: "xxx"}) //Delete - 删除 db.Delete(&User{}, 5) }
html/template模板渲染包
在Go语言中可以使用内置的text/template包对文本文件进行变量字符串替换;
//test.tmpl和./test.tmpl名称保持一致 tmpl, err := template.New("test.tmpl").Delims("{{", "}}").ParseFiles("./test.tmpl") if err != nil { fmt.Println("生成模板错误", err) } data := TemplateData{ Message: "Hello, World!", Name1: "666666", Name2: "888888", } saveFile, err := os.OpenFile("test.txt", os.O_WRONLY|os.O_CREATE, 0644) if err != nil { fmt.Println("创建文件错误", err) } w := bufio.NewWriter(saveFile) err = tmpl.Execute(w, data) if err != nil { fmt.Println("渲染错误", err) } w.Flush()
都是web开发是数据驱动视图(前端页面显示),我们除了可以在前段JavaScript中发送ajax或者vue的axios从后端获取数据,然后赋值给前端变量。(前后、端配合完成数据驱动)
还可以在后端使用模板渲染也就是字符串替换的方式,把后端的数据赋值给前端的变量,然后将视图(html标签)和数据一起打包返回给浏览器。(在后端完成数据驱动视图)
Python的Flask使用Jia2,Django使用自带的模板引擎而Golang中的html/template包实现了数据驱动的模板。
{{.}}语法
模板语法都包含在{{ }}中间,其中{{ .}}中的点表示当前对象。
当我们把golang中的1个结构体传到模板之后就可以.字段名来获取值了.
前端
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>您好</h1> <h1>{{.Name }}</h1> <h1>{{.Age }}</h1> </body> </html>
后端
package main import ( "fmt" "html/template" "net/http" ) type Person struct { Name string Age int } func handleBook(w http.ResponseWriter, r *http.Request) { method := r.Method if method == "GET" { //fmt.Fprintf(w,"hellow") //凡是实现了file接口的类型都可以fmt.Fprintf()写入数据 //w.Write([]byte("您好!")) //data, err := ioutil.ReadFile("./home.html") //1.定义模板:{{.Age }} //2.解析模板:不要刻舟求剑,在goland中Run 'go build main.go否则找不到模板路径' t, err := template.ParseFiles("./home.html") if err != nil { fmt.Println("模板解析失败!") } userInfo := Person{Name: "张根", Age: 27} //3.渲染模板:字符串替换 t.Execute(w, userInfo) } } func main() { http.HandleFunc("/book", handleBook) err := http.ListenAndServe(":9001", nil) if err != nil { fmt.Println(err) panic("服务启动失败!") } }
后端传多个变量到模板
Django支持同时把多个变量传入模板也就是把所有变量组合成1个字典。
return render(request, "myapp/index.html", {"foo": "bar"})
html/templatet是把多个变量组合成map,需要注意的是map的key不用大写,而结构体的字段需要大小。
data:=map[string]interface{}{ "u1":person1, "u2":person2, } //3.渲染模板:字符串替换 t.Execute(w, data)
代码
package main import ( "fmt" "html/template" "net/http" ) //结构体渲染进模板时要大小写这种属性的可见性 type Person struct { Name string Age int } func handleBook(w http.ResponseWriter, r *http.Request) { method := r.Method if method == "GET" { //fmt.Fprintf(w,"hellow") //凡是实现了file接口的类型都可以fmt.Fprintf()写入数据 //w.Write([]byte("您好!")) //data, err := ioutil.ReadFile("./home.html") //1.定义模板:{{.Age }} //2.解析模板:不要刻舟求剑,在goland中Run 'go build main.go否则找不到模板路径' t, err := template.ParseFiles("./home.html") if err != nil { fmt.Println("模板解析失败!") } //map的渲染到模板语言时不需要把key进行大写! person1:= map[string]interface{}{"name": "Martin", "age": 18} person2:=Person{Name: "Bob",Age: 25} //Django的模板语言可以同时传多个参数,而template/html只能传1个,所以我对2个变量进行了打包装! data:=map[string]interface{}{ "u1":person1, "u2":person2, } //3.渲染模板:字符串替换 t.Execute(w, data) } } func main() { http.HandleFunc("/book", handleBook) err := http.ListenAndServe(":9001", nil) if err != nil { fmt.Println(err) panic("服务启动失败!") } }
{{/* 注释内容 */}}语法
注意/*和*/一定要紧贴着{{ }}
{{/* 注释内容 */}}
定义和使用变量
<h1>{{$name:=.u2.Age}}</h1> <h2>{{$name}}</h2>
去除左右两侧的空格
<h2>{{- $name -}}</h2>
管道符(pipeline)
<a href="/search?q={{. | urlquery}}">{{. | html}}</a>
比较函数
布尔函数会将任何类型的零值视为假,其余视为真。
下面是定义为函数的二元比较运算的集合:
eq 如果arg1 == arg2则返回真 ne 如果arg1 != arg2则返回真 lt 如果arg1 < arg2则返回真 le 如果arg1 <= arg2则返回真 gt 如果arg1 > arg2则返回真 ge 如果arg1 >= arg2则返回真
逻辑判断
{{$age:=.u2.Age}} {{if lt $age 18 }} 好好学习! {{else if and (ge $age 18) (lt $age 30)}} 好好谈对象! {{else if and (ge $age 30) (lt $age 50)}} 努力干活! {{else}} 享受人生! {{end}}
rang循环
后端
people:=[]Person{{Name: "张三",Age: 18},{Name: "李四",Age: 19},{Name: "王武",Age: 20}
前端
range 数组
{{ range .people}} {{/*注意 当前rang代码块中的点,就是遍历的item*/}} <li>{{.Name}}:{{.Age}}</li> {{end}}
rang 字典
<ul> {{ range $k,$v :=. }} <li>{{$k}}----{{$v}}</li> {{end}} </ul>
with
with可以开辟1个函数作用域。如果pipeline为empty不产生输出,否则将dot(点)设为pipeline的值并执行,但是不修改外面的dot。
<ul> {{with .people}} <li>{{index . 0}}</li> <li>{{index . 1}}</li> <li>{{index . 2}}</li> {{end}} </ul>
模板语言内置函数
and 函数返回它的第一个empty参数或者最后一个参数; 就是说"and x y"等价于"if x then y else x";所有参数都会执行; or 返回第一个非empty参数或者最后一个参数; 亦即"or x y"等价于"if x then x else y";所有参数都会执行; not 返回它的单个参数的布尔值的否定 len 返回它的参数的整数类型长度 index 执行结果为第一个参数以剩下的参数为索引/键指向的值; 如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。 print 即fmt.Sprint printf 即fmt.Sprintf println 即fmt.Sprintln html 返回与其参数的文本表示形式等效的转义HTML。 这个函数在html/template中不可用。 urlquery 以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。 这个函数在html/template中不可用。 js 返回与其参数的文本表示形式等效的转义JavaScript。 call 执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数; 如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2); 其中Y是函数类型的字段或者字典的值,或者其他类似情况; call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同); 该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型; 如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;
自定义函数
如果模板语言中内置的函数无法满足我们渲染数据的需求时,我们可以在后端定义函数传到前端使用。-------Django的simple tag 和filter
package main import ( "fmt" "html/template" "net/http" ) //结构体渲染进模板时要大小写这种属性的可见性 type Person struct { Name string Age int } //1.定义1个函数:必须包含2个返回值,1和是结果、1个是error类型 func PraiseSomebody(name string) (string, error) { return name + "您真是帅!", nil } func handleBook(w http.ResponseWriter, r *http.Request) { method := r.Method if method == "GET" { //fmt.Fprintf(w,"hellow") //凡是实现了file接口的类型都可以fmt.Fprintf()写入数据 //w.Write([]byte("您好!")) //data, err := ioutil.ReadFile("./home.html") //2.定义模板home.html注意不要加./: t := template.New("home.html") //3.告诉模板引擎 我扩展了自己的自定义的函数! t.Funcs(template.FuncMap{"praise": PraiseSomebody}) //4.解析模板:不要刻舟求剑,在goland中Run 'go build main.go否则找不到模板路径' _, err := t.ParseFiles("./home.html") if err != nil { fmt.Println("模板解析失败!") fmt.Println(err.Error()) } //map的渲染到模板语言时不需要把key进行大写! people := []Person{{Name: "张三", Age: 18}, {Name: "李四", Age: 19}, {Name: "王武", Age: 20}} //Django的模板语言可以同时传多个参数,而template/html只能传1个,所以我对2个变量进行了打包装! data := map[string]interface{}{ "people": people, } //5.渲染模板:字符串替换 t.Execute(w, data) } } func main() { http.HandleFunc("/book", handleBook) err := http.ListenAndServe(":9001", nil) if err != nil { fmt.Println(err) panic("服务启动失败!") } }
--------------
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <div> <h1>您好</h1> <ul> {{range .people}} <p>{{praise .Name}}</p> {{end}} </ul> </div> </body> </html>
模板嵌套
为了使我们的模板语言代码更加具有灵活扩展性,我们可在template中嵌套其他的子template。
这个子template可以是单独的template文件,也可以是通过define
定义的template。
1.通过define在当前template中声明子template
<!DOCTYPE html> <html lang="en"> {{/* 1.声明header子模板*/}} {{define "heade.html"}} <head> <meta charset="UTF-8"> <title>嵌套模板</title> </head> {{end}} {{/*2.声明body子模板*/}} {{define "body.html"}} <body> <h1>您好!</h1> </body> {{end}} {{/* 3.声明food子模板*/}} {{define "foot.html"}} <script> alert("您好!") </script> {{end}} {{/* 4.使用header子模板*/}} {{template "heade.html"}} {{/* 5.使用body子模板*/}} {{template "body.html"}} {{/*6.使用foot子模板*/}} {{template "foot.html"}} </html>
2.在其他文件声明子template
子template
./template/------
{{/* 1.声明header子模板*/}} {{define "heade.html"}} <head> <meta charset="UTF-8"> <title>嵌套模板</title> </head> {{end}}
./template/------
{{/*2.声明body子模板*/}} {{define "body.html"}} <body> <h1>您好!</h1> </body> {{end}}
./template/------
{{/* 3.声明food子模板*/}} {{define "foot.html"}} <script> alert("您好!") </script> {{end}}
./template/------
<!DOCTYPE html> <html lang="en"> {{/* 4.使用header子模板*/}} {{template "heade.html"}} {{/* 5.使用body子模板*/}} {{template "body.html"}} {{/*6.使用foot子模板*/}} {{template "foot.html"}} </html>
后端
_, err := t.ParseFiles("./templates/home.html","./templates/header.html","./templates/body.html","./templates/foot.html") if err != nil { fmt.Println("模板解析失败!") fmt.Println(err.Error()) }
模板继承
既然前面已经有了模板嵌套,为什么现在还需要模板继承呢?
取代模板继承?适应更多的开发场景?
想这个问题的时候我也深刻思考了.........面向对象的3大特性:封装、继承、多态,又想到了为什么会Python了还要学Golang呢?
其实这都是为了能够适应更多、更复杂、更丰富的开发场景!
当我们需要使用一些公用的组件时,去嵌套这些公共的template是1种不错的代码复用方案。
通常情况我们的web站点会有很多URL、很多HTML文件,它们除了有共性之外还会有一些差别。
而且这1堆HTML文件之间的差异部分远远大于相同部分。
这时我们仅仅使用模板嵌套的模式,就会陷入以下情景。
遇到新功能---》创建子template-------》组装出新页面----------》
遇到新功能---》创建子template-------》组装出新页面----------》
遇到新功能---》创建子template-------》组装出新页面.---------》
遇到新功能---》创建子template-------》组装出新页面.---------》
........................................................................................
有没有1种方法可以把这些差异部分抽象出来?把不断得组装子template的环节省略掉?
模板继承是针对一大堆HTML的差异部分远远大于相同部分的一种灵活得新功能扩展方案!
所以模板嵌套可以把公用的模板组件嵌套进来,模板继承可以支持我们更快速得进行新功能迭代。
模板嵌套和模板继承需要相互结合。
templates--------------
{{define "layout"}} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>模板的继承</title> <style> * { margin: 0; } .nav { position: fixed; background-color: aqua; width: 100%; height: 50px; top: 0; } .main { margin-top: 50px; } .menu { position: fixed; background-color: blue; width: 20%; height: 100%; left: 0; } .center { text-align: center; } </style> </head> <body> <div class="nav"> </div> <div class="main"> <div class="menu"></div> <div class="content center"> {{/*注意content后面有个点!*/}} {{block "content" .}} {{end}} </div> </div> </body> </html> {{end}}
templates--------------
{{/*继承根模板*/}} {{template "layout".}} {{/*重写根模板中的content块*/}} {{define "content" }} <h1>这是index页面1</h1> <h1>{{.}}</h1> {{end}}
templates--------------
{{/*继承根模板*/}} {{ template "layout" .}} {{/*重写根模板中的content块*/}} {{ define "content"}} <h1>这是home页面</h1> <h1>{{.}}</h1> {{end}}
后端
package main
import (
"fmt"
"html/template"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request, ) {
//1.定义模板
//2.解析模板
//indexTemplate, err := template.ParseGlob("templates/*.html")
indexTemplate,err:= template.ParseFiles("./templates/layout.html","./templates/index.html")
if err != nil {
fmt.Println(err)
return
}
//3.渲染模板
data := "Index"
//指定我具体要渲染哪1个模板?
err = indexTemplate.ExecuteTemplate(w, "index.html", data)
if err != nil {
fmt.Println("渲染模板失败, err:", err)
return
}
}
func home(w http.ResponseWriter, r *http.Request) {
//1.解析模板
homeTemplate,err:= template.ParseFiles("./templates/layout.html","./templates/home.html")
if err != nil {
fmt.Println(err)
}
//2.渲染模板
//template.Execute(w, data)
data := "Home"
err = homeTemplate.ExecuteTemplate(w,"home.html", data)
if err != nil {
fmt.Println("渲染模板失败", err)
}
}
func main() {
http.HandleFunc("/index", index)
http.HandleFunc("/home", home)
err := http.ListenAndServe(":8002", nil)
if err != nil {
fmt.Println(err)
return
}
}
修改默认的标识符
Go标准库的模板引擎使用的花括号{{
和}}
作为标识,而许多前端框架(如Vue
和 AngularJS
)也使用{{
和}}
作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:
//模板解析之前,定义自己的标识符 {[ ]} t, err := template.New("index.html").Delims("{[", "]}").ParseFiles("./templates/index.html")
模板语言防止xss跨站请求攻击
html/template和Django的模板语言一样默认情况下是对前端代码形式的字符串进行转义的!
Django的模板语言中有1个safe内置函数,可以控制后端的字符串<a href="//"></a>直接渲染到前端。
func xss(w http.ResponseWriter, r *http.Request){ tmpl,err := template.New("xss.tmpl").Funcs(template.FuncMap{ "safe": func(s string)template.HTML { return template.HTML(s) }, }).ParseFiles("./xss.tmpl") if err != nil { fmt.Println("create template failed, err:", err) return } jsStr := `<script>alert('嘿嘿嘿')</script>` err = tmpl.Execute(w, jsStr) if err != nil { fmt.Println(err) } }
模板语言使用
{{ . | safe }}
Sonyflake
在单机架构模式下我们可以使用mysql的ID或者UUID作为唯一标识(不能计算和排序)。
snowflake是一种雪花算法,如果是分布式集群环境如何在整个集群中1个全局自增的唯一数字呢?
注意:
这种算法就是基于时间戳来生成的唯一自增ID,请一定确保你集群中的服务器全部做了NTP(时间同步服务)且时间无法后置!!!!
如果你服务器时间滞后到了最近1次生成ID的时候,这个算法会一直等待你服务器时间超过最近1次生成ID时间!
package main import ( "fmt" "github.com/sony/sonyflake" ) var ( snoyFlake *sonyflake.Sonyflake machineID uint16 ) //获取机器ID的回调函数 func GetMachineID() (uint16, error) { //真正的分布式环境必须从zookeeper或者etcd去获取机器的唯一ID return machineID, nil } //初始化snowflake func Init(mID uint16) (err error) { machineID = mID //配置项 setings := sonyflake.Settings{} setings.MachineID = GetMachineID snoyFlake = sonyflake.NewSonyflake(setings) return } //获取1个全局的ID func GetID() (id uint64, err error) { if snoyFlake == nil { err = fmt.Errorf("必须调用Init函数进行初始化!") return } id, err = snoyFlake.NextID() return } func main() { //输入1个集群内机器的ID err := Init(3) if err != nil { fmt.Println(err) return } id, err := GetID() if err != nil { fmt.Println(err) } fmt.Println(id) }
ini.v1