前言
本文主要介绍在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
浙公网安备 33010602011771号