前言

本文主要介绍在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
	

}

  

ParseInLocation和Parse的区别
学个单词
absent:ver /adj
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 healthy and normal life in absence of sleep. //没有健康正常生活缺乏睡眠

  

 
ParseInLocation is like Parse but differs in two important ways.
paseInLocation和Pase类似但有2个非常不同的地方:
First, in the absence of time zone information, Parse interprets a time as UTC;ParseInLocation interprets the time as in the given location.
1.Parse解析出来的时间 以UTC作为时区信息,而PaseInlaction解析出来时间格式时区信息为你指定的时区。
Second, when given a zone offset or abbreviation, Parse tries to match it against the Local location; ParseInLocation uses the given location.
2.当你指定的时区偏差或省略时,Parse会尝试和本地时间进行匹配,而ParseInLocation使用指定的参数。
说白了这2个方法解析出来的的时间 1个是英国时间(UTC) 1个是中国时间(CST)两个时间不一样,所以不要求 时间差!
 

runtime

Caller reports file and line number information about function invocations on the calling goroutine's stack. The argument skip is the number of stack frames to ascend, 
Caller记录了1个函数在goroutine's 栈中被调用的信息,信息有调用这个函数的函数的文件、行号信息(Go中调用1个函数就相当于开辟了1个新的栈)1层包着1层。skip参数就是1层层栈的编号。
with 0 identifying the caller of Caller.  (For historical reasons the meaning of skip differs between Caller and Callers.) 
0代表调用caller自己这个函数栈。
The return values report the program counter, file name, and line number within the file of the corresponding call. The boolean ok is false if it was not possible to recover the information.
返回值记录了相关调用文件的 程序计算器、文件名、行号。如果没有Reover代码执行中出现的Panic,返回布尔值false.
 
 
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")
}

执行流程

  1. go会主动先执行init方法,打印init start, os.Args = .....信息,这时,参数只有主进程文件名"/tmp/GoLand/___1go_build_main_go"
  2. init中reexec.Register会将childProcess注册为"child_Process"名字(特意两个名字不同)
  3. 执行reexec.Init判断是不是子进程,判断不是,打印"main Process initializer"
  4. 开始执行主函数,打印main start, os.Args = ......
  5. 调用reexec.Command,获取注册的child_Process方法,进行执行。
  6. 使用Start方法执行方法,并使用Wait方法等待子进程退出
  7. 子进程方面:
    1. 子进程执行还是会先走init方法,打印init start, os.Args = .....,此时参数则为child_Process
    2. reexec.Register发现方法存在不会重复注册。
    3. reexec.Init判断是子进程(此说法有点小问题,不影响理解,具体原因请看下文源码分析),进入
    4. 子进程调用,打印i am a childProcess并退出
  8. 子进程退出,且没有错误,主进程继续执行,打印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获取到变量的值之后修改值

reflectValue.Elem().SetInt(200)
想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。
反射中使用专有的Elem()方法来获取指针对应的值
Elem()通过指针获取变量对应的值相当于*pointer
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数据传递模式是流模式(水流)

“粘包”可发生在发送端也可发生在接收端:

  1. 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去
  2. 接收端接收不及时造成的接收端粘包: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服务端

ps:net/http源码内部使用了gorutine,每个request都会有对应gorutine去处理,所以性能比较强悍。

做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

等同于Python中的sys.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包支持的命令行参数类型有boolintint64uintuint64float float64stringduration

 

定义命令行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(查询多条记录)

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查询

sqlx和go-sql-driver的差别主要体现在这里!
//go-sql
var u user
err := rows.Scan(&u.name, &u.sex, &u.age)
//sqlx
var u user
db.Get(&u, sql, 38)
由于sqlx内部源码需要使用reflect给你传入的struct赋值,所以我们无需scan struct的所有字段。
我们必须传递指针struct并且结构体的字段大写,才能被源码包调用到!
 
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语句执行过程:

  1. 客户端对SQL语句进行占位符替换得到完整的SQL语句。
  2. 客户端发送完整SQL语句到MySQL服务端
  3. MySQL服务端执行完整的SQL语句并将结果返回给客户端。 

预处理执行过程:

  1. 把SQL语句分成两部分,命令部分与数据部分。
  2. 先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。
  3. 然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。
  4. 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

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模板渲染包

都是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}}
heade.html

./template/------

{{/*2.声明body子模板*/}}
{{define "body.html"}}
<body>
<h1>您好!</h1>
</body>
{{end}}
body.html

./template/------

{{/* 3.声明food子模板*/}}
{{define "foot.html"}}
<script>
    alert("您好!")
</script>
{{end}}
foot.html"

./template/------

<!DOCTYPE html>
<html lang="en">
{{/* 4.使用header子模板*/}}
{{template "heade.html"}}
{{/* 5.使用body子模板*/}}
{{template "body.html"}}
{{/*6.使用foot子模板*/}}
{{template "foot.html"}}
</html>
home.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}}
layout.html

templates--------------

{{/*继承根模板*/}}
{{template "layout".}}
{{/*重写根模板中的content块*/}}
{{define "content" }}
    <h1>这是index页面1</h1>
    <h1>{{.}}</h1>
{{end}}
index.html

templates--------------

{{/*继承根模板*/}}
{{ template "layout" .}}
{{/*重写根模板中的content块*/}}
{{ define "content"}}
    <h1>这是home页面</h1>
    <h1>{{.}}</h1>
{{end}}
home.html

后端

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
    }
}
main.go

 

 

修改默认的标识符

 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
posted on 2020-05-18 18:18  Martin8866  阅读(816)  评论(0编辑  收藏  举报