随笔 - 241  文章 - 1  评论 - 58  阅读 - 85万 

前言

本文主要介绍在Go中内置的包如何使用

 

time

时间相关的包

  

ParseInLocation和Parse的区别
学个单词
absent:ver /adj
1
I was absent from the APEC In Beijing。//我缺席了北京的APEC会议

1
His wife often complains about his frequent absences.  //他老婆经常抱怨她缺少他的陪伴

  

in the absence of  短语 

1
No one can live a 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.
 
 

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 反射包

我们经常使用的序列化和反序列化是怎么实现的?               代码<------>字符串

  

Django orm里modal.py写的class是怎么对应成Mysql中的表的呢?     代码---->字符串

我们运维写得的 配置文件信如何加载到程序里面的呢?               字符串---->代码

任何接口类型(变量)都是由类型和值构成我们可以使用reflect的typeof()和reflect.valueof()获取任意变量到这2种信息。 

 

1.TypeOf func(i interface{}) Type {}

reflect.TypeOf ()函数获取接口变量当前的类型

  

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 将值以字符串类型返回

 

 

  

 

2.1获取到变量的值之后修改值

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

  

 

2.2.isNil()和isValid()

获取到值之后,判断值里面   是否包含某个方法 / 字段(是否存在)

类似于Python中 getattr(obj,"method_name " )

  

loadini文件

根据ini文件配置生成go中的结构体

  

 

strconv  

根据字符串面值 转换为Go中的数据类型

Go语言是强类型的语言:如果Go本身不支持2种数据类型直接的转换,是不能强制转换的。

   

net网络相关

 

 

golang的net包封装了实现传输层、应用层协议的API 。

我们使用net包可以写1个遵循TCP/UPD协议的socket程序,也可以写1个遵循http协议的 web应用程序。

 

1.TCP服务端和客户端实现

TCP server

TCP client

 

TCP粘包问题

TCP tcp数据传递模式是流模式(水流)

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

  1. 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去
  2. 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

 

客户端发送数据

我们可以自己定义一个协议,比如数据包的前4个字节为包头(固定大小),用来存储要发送数据的长度

如何保证数据包的包头为固定大小呢?

golang中int32数据类型的变量为4个字节!

在网络上传输的都是字节数据,golangGo里面内建仅支持UTF8字符串编码,所以我们不需要考虑编码的问题,直接使用   bytes.buffer把数据发送到网络!

 

服务端接收到数据

先获取4个字节的信息(也就是要接收数据的元数据)

根据元数据获取指定长度的数据。

  

2.UPD服务端和客户端

UDP server

UDP协议用户数据报协议(UDP,User Datagram Protocol)

UDP和TCP最大的区别就是UDP 为应用程序提供了一种无需建立连接(3次握手)就可以发送封装的 IP 数据包方法。

  

UDP client

 

web聊天室

server

  

 

client

  

 

3.HTTP协议的服务端和客户端

 

 

HTTP服务端

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

做web开发关注这块,什么Django/Flashk/Tornado(写接口)

 

    

  

HTTP客户端

做运维、测试、爬虫关注这块(接口测试、调用个API、写个爬虫)

  

设置http客户端head参数

在爬虫请求次数频繁的业务场景下:       使用共用1个固定的HTTP客户端设置keepalive, 限制服务器开启socket的数量和每次建立TCP连接的开销。

在爬虫请求次数不频繁的业务场景下: 使用多个HTTP客户端禁用 keepalive保证数据获取完毕之后socket资源及时的释放掉。

  

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

  

flag包基本使用

1
etct start --gotuineCount=10000

如果我想在支持程序时 支持这种调用方式,os.Args将会不容易实现。

 flag包支持的命令行参数类型有boolintint64uintuint64float float64stringduration

 

定义命令行flag参数

 flag.String(解析为指针类型)

 

 flag.StringVar(非指针类型)

如果以上面的方式解析命令行参数,返回的是指针类型的变量,不方便你在程序你使用它!

  

 

结果

1
2
3
4
5
6
7
8
9
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没有具体连接数据库,只是列出 第三方库需要实现连接的内容。

所以需要下载第三方的驱动。

1
2
3
func init() {
    sql.Register("mysql", &MySQLDriver{})
}

  

go-sql-driver/mysql驱动下载

1
update 所有依赖到最新<br>go get -u github.com/go-sql-driver/mysql

database/sql包原生实现了数据库连接池,并且并发安全。

 

创建连接池

go-sql-driver原生支持连接池并支持并发,所有可以同时使用多个gorutine从连接池中获取数据库连接。

  

QueryRow(查询单条记录)

 

 

Query(查询多条记录)

query一定要手动释放持有的数据库连接到连接池(否则会导致连接池连接资源马上枯竭!)

 

Exec(insert/update/remove) 

 在更新数据时一定要记得 rows.RowsAffected()使update sql语句生效。跟pymysql的commit()一样!

  

事物操作

事物就是保证 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

  

连接池

  

Get和Select查询

sqlx和go-sql-driver的差别主要体现在这里!
1
2
3
4
5
6
//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并且结构体的字段大写,才能被源码包调用到!
 

 

Exec(insert/update/remove) 

  

 namedExec

通过结构体修改数据库行

 

sqlx事物操作

  

sql注入

sql注入产生的root cause 是字符串拼接。

select name,sex,age from studentds_info where name='xxx' or 1=1 #别拼了

如果SQL是拼接而成的就意味着这条SQL出现了缝隙,有了这个缝隙hack就可以乘虚而入: 迎合sql前一部分【干自己想干的】注释掉后一部分。

真佩服第1个搞这个事情的人!~~~~~方法总比困难多!!!

  

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)
}

  

批量查询 

 

 go操作Redis

 

安装

1
go get -u github.com/go-redis/redis

  

连接

1.普通连接

1
2
3
4
5
6
7
8
9
10
11
12
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哨兵模式

  

 

3.连接Redis集群

  

3.set和get数据

Redis支持诸如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、带范围查询的排序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)。



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
复制代码

 

使用

  

 

 

 

 

 taill模块

类似于Linux中的taill命令可以检测文件内容的变化,并支持文件重新打开。

什么是文件重新打开呢?

一般日志文件切割的策略是当文件内容到达1个size阀值之后,把当前的文件(nginx.log)重命名(nginx1),然后重新打开一个新的文件(nginx.log).......。

下载

go get -u github.com/hpcloud/tail

 

基本使用

                                                   

  goini 解析ini配置文件

我们在写项目的时候使用配置文件是常见的事情,那么如何把1个配置文件转换成go程序可以识别的数据类型呢?

总不能自己reflect.TypeOf用反射实现一个,unkown实现了1个第三方包。

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()
View Code
复制代码

都是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)
}
复制代码

 

 

 

 

 

 

 

 

 

参考

1
ini.v1
posted on   Martin8866  阅读(859)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示