Golang中的unsafe标准库包

tttlll 发表了文章 • 2 个评论 • 1288 次浏览 • 2016-11-13 19:22 • 来自相关话题

2016/10/22, 老獏

unsafe标准库包是Golang中一个比较特殊的包,为什么这么说?本问将详细解释其中缘由。

Go官方文档中的警告

unsafe包的文档查看全部

2016/10/22, 老獏


unsafe标准库包是Golang中一个比较特殊的包,为什么这么说?本问将详细解释其中缘由。


Go官方文档中的警告


unsafe包的文档这么说:



Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.



Go 1 兼容性指南这么说:



Packages that import unsafe may depend on internal properties of the Go implementation. We reserve the right to make changes to the implementation that may break such programs.



两段话大同小异,简而言之就是使用unsafe包是危险的,Golang不保证此包的跨平台和跨版本兼容性。听上去挺慎人,但是此包究竟有多危险?先让我们看看此包在Golang中发挥的角色是什么。


unsafe包的角色


到目前为止(Go1.7),unsafe包包含以下资源:



  • 3个函数:

    • func Alignof(variable ArbitraryType) uintptr

    • func Offsetof(selector ArbitraryType) uintptr

    • func Sizeof(variable ArbitraryType) uintptr


  • 和一个类型


这里ArbitraryType不是一个真正的类型,它仅仅是一个占位符。


不像绝大多数的函数,在Golang中,以上3个函数的调用将在编译时而不是运行时被估值,因此这3个函数的返回值可以被赋给常量。换句话说,这3个函数是为编译器服务的。


除了这3个函数,unsafe包中唯一的类型unsafe.Pointer也是为编译器服务的。


由于安全原因,Golang不允许下列类型的值互相进行转换:



  • 两个不同指针类型,比如*int64 and *float64

  • 任何普通指针类型*Tuintptr


但是在unsafe.Pointer的帮助下,我们可以打破Golang类型系统和内存管理的安全机制,从而使得以上的转换成为可能。这是怎么办到的呢?让我们看看unsafe包中列出的和unsafe.Pointer有关的规则



  • 任何类型的指针都可以被转换为unsafe.Pointer类型;

  • 一个unsafe.Pointer值都可以被转换为任何指针类型;

  • 一个uintptr值可以被转换为unsafe.Pointer类型;

  • 一个unsafe.Pointer值可以被转换为uintptr类型。


这些规则和Go白皮书是一致的:



Any pointer or value of underlying type uintptr can be converted to a Pointer type and vice versa.



这些规则表明unsafe.Pointer如同c语言中的void*。是的,c语言中的void*很危险!


按照这些规则,对两个不同的类型T1T2,指针类型*T1的值可以被转换为unsafe.Pointer类型,然后转换后的unsafe.Pointer类型的值可以继续被转换为*T2类型(或者uintptr类型);反之也是可以的。通过这种方式,Golang类型系统和内存管理的安全机制被绕过了。显然,滥用这种方式是危险的。


看个例子:


package main

import (
"fmt"
"unsafe"
)
func main() {
var n int64 = 5
var pn = &n
var pf = (*float64)(unsafe.Pointer(pn))
// 到这里,pn和pf指向同一个内存地址。
fmt.Println(*pf) // 2.5e-323
*pf = 3.14159
fmt.Println(n) // 4614256650576692846
}

此例中的转换或许并没有太大实际意义,但此转换是安全和合法的。至于为什么安全,见下文。


更多关于unsafe.Pointer和uintptr的一些事实


下面是关于unsafe.Pointeruintptr的一些事实:



  • uintptr 是一个整数类型,

    • 即使一个uintptr值依然被程序使用中,此uintptr值所表示的指针所指的数据可能将被垃圾回收。


  • unsafe.Pointer是一个指针类型,

    • 但是unsafe.Pointer值不能被解引用;

    • 和普通指针一样,如果一个unsafe.Pointer 值依然被程序使用中,此unsafe.Pointer 值所表示的指针所指的数据将确保不会被垃圾回收。


  • *unsafe.Pointer是一个普通指针,和*int类似。


既然uintptr是一个整数类型,那么当然可以对uintptr值进行算术运算。利用这一点,我们可以绕开Golang中指针不能进行偏移运算的限制:


package main

import (
"fmt"
"unsafe"
)

func main() {
a := [4]int{0, 1, 2, 3}
p1 := unsafe.Pointer(&a[1])
p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0]))
*(*int)(p3) = 6
fmt.Println("a =", a) // a = [0 1 2 6]

// ...

type Person struct {
name string
age int
gender bool
}

who := Person{"John", 30, true}
pp := unsafe.Pointer(&who)
pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
*pname = "Alice"
*page = 28
*pgender = false
fmt.Println(who) // {Alice 28 false}
}

unsafe包究竟有多危险?


关于unsafe包,Go团队的主力开发之一Ian已经确认:



  • unsafe包中的函数原型今后保证不变;

  • 类型unsafe.Pointer将永远存在。


所以,看上去unsafe包中的函数并不怎么危险。Go团队的甚至想把它们放到别的包中。使用这些函数唯一的不安全性是这些函数的调用在今后的版本中可能会返回不同的结果值。但这种不安全性很难说是一种危险。


这样我们可以的得出结论:使用unsafe包的危险都是和使用unsafe.Pointer类型密切相关的。unsafe包的文档列出了一些合法和非法使用unsafe.Pointer类型的例子,这里仅仅列出部分非法的情形:


package main

import (
"fmt"
"unsafe"
)

// 情形A:unsafe.Pointer和uintptr之间的来回转换
// 未在同一个表达式中完成。
func illegalUseA() {
fmt.Println("===================== illegalUseA")

pa := new([4]int)

// 将下面这个合法的使用
// p1 := unsafe.Pointer(uintptr(unsafe.Pointer(pa)) + unsafe.Sizeof(pa[0]))
// 分成两个表达式(不合法的使用):
ptr := uintptr(unsafe.Pointer(pa))
p1 := unsafe.Pointer(ptr + unsafe.Sizeof(pa[0]))
// "go vet"将对上一行给出一个警告:
// possible misuse of unsafe.Pointer

// unsafe包的文档https://golang.org/pkg/unsafe/#Pointer,
// 认为上面分成两行是非法的,
// 但是目前的Go编译器和运行时(1.7.3)
// 没有侦测到这个非法使用。
//
// 然而,为了保证你的Go程序绝对安全的运行,
// 最好还是请遵守unsafe包的文档中定的规则。

*(*int)(p1) = 123
fmt.Println("*(*int)(p1) :", *(*int)(p1))
}

// 情形B:指针指向了非法地址
func illegalUseB() {
fmt.Println("===================== illegalUseB")

a := [4]int{0, 1, 2, 3}
p := unsafe.Pointer(&a)
p = unsafe.Pointer(uintptr(p) + uintptr(len(a)) * unsafe.Sizeof(a[0]))
// 现在p指向了a值内存块的结尾,
// 因为我们不清楚a值内存块的结尾存出的是什么,
// 所以p的当前值是非法的,虽然到目前为止也没什么危险。
// 但是如果我们改变了p所指的值,程序有可能运行错乱。
*(*int)(p) = 123
fmt.Println("*(*int)(p) :", *(*int)(p)) // 123 or not 123
// 当前的Go编译器和运行时(1.7.3)以及"go vet"
// 都没有检测到上面这个非法操作。

// 但是,当前的Go运行时(1.7.3)将侦测到下面的
// 非法操作(其实和上面的非法操作是一样的)并panic。
p = unsafe.Pointer(&a)
for i := 0; i <= len(a); i++ {
*(*int)(p) = 123 // Go运行时(1.7.3)不会在此panic

fmt.Println(i, ":", *(*int)(p))
// 当i==4的时候,Go运行时(1.7.3)将在上一行panic
// runtime error: invalid memory address or nil pointer dereference

p = unsafe.Pointer(uintptr(p) + unsafe.Sizeof(a[0]))
}
}

func main() {
illegalUseA()
illegalUseB()
}

编译器很难检测到非法的unsafe.Pointer使用。虽然"go vet"能发现一些潜在的非法的unsafe.Pointer使用,但也不能检测到所有的非法使用。同样,Go运行时也不能检测到所有的非法unsafe.Pointer使用。非法unsafe.Pointer使用可能会是程序崩溃,或者使程序运行诡异(比如有时候表现正常,有时候却很反常)。这就是为什么说使用unsafe包是危险的。


*T1*T2之间的转换


对于将*T1值转换为unsafe.Pointer类型,继而再转换为*T2类型,unsafe包是这么说的:



Provided that T2 is no larger than T1 and that the two share an equivalent memory layout, this conversion allows reinterpreting data of one type as data of another type.



直译过来:如果类型T2的大小不比T1大并且这两个类型有着等价的内存布局,这种转换允许其中一个类型的数据用另一个类型的数据来表示。(反正我是没看太明白)


这里对equivalent memory layout(内存布局)的定义很含糊,而且好像Go团队故意将其定义得很含糊。 这使得使用unsafe包更加的危险。


既然Go团队不愿意定义一个精确的规则,本文也不会做这个尝试。这里仅仅列出部分合法的例子。


合法使用1:在切片[]T[]MyT之间转换


在下例中,我们使用int类型表示T


type MyInt int

在Golang的类型系统中,类型[]int[]MyInt底层类型均为它们自身。Golang中,底层类型不一样的两个非接口类型之间是不支持转换的。但是在unsafe.Pointer的帮助下,这种转换成为可能:


package main

import (
"fmt"
"unsafe"
)

func main() {
type MyInt int

a := []MyInt{0, 1, 2}
// b := ([]int)(a) // error: cannot convert a (type []MyInt) to type []int
b := *(*[]int)(unsafe.Pointer(&a))

b[0]= 3

fmt.Println("a =", a) // a = [3 1 2]
fmt.Println("b =", b) // b = [3 1 2]

a[2] = 9

fmt.Println("a =", a) // a = [3 1 9]
fmt.Println("b =", b) // b = [3 1 9]
}

合法使用2:调用sync/atomic包中指针相关的函数


sync/atomic包中和指针相关的函数的参数和返回值类型大多都是unsafe.Pointer或者 *unsafe.Pointer



  • func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

  • func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

  • func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

  • func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)


为了调用这些函数,必须引入unsafe包。


注意:*unsafe.Pointer属于普通类型,所以*unsafe.Pointer值可以被转换为unsafe.Pointer类型,反之亦然。


package main

import (
"fmt"
"log"
"time"
"unsafe"
"sync/atomic"
"sync"
"math/rand"
)

var data *string

// get data atomically
func Data() string {
p := (*string)(atomic.LoadPointer(
(*unsafe.Pointer)(unsafe.Pointer(&data)),
))
if p == nil {
return ""
} else {
return *p
}
}

// set data atomically
func SetData(d string) {
atomic.StorePointer(
(*unsafe.Pointer)(unsafe.Pointer(&data)),
unsafe.Pointer(&d),
)
}

func main() {
var wg sync.WaitGroup
wg.Add(200)

for range [100]struct{}{} {
go func() {
time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)

log.Println(Data())
wg.Done()
}()
}

for i := range [100]struct{}{} {
go func(i int) {
time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)
s := fmt.Sprint("#", i)
log.Println("====", s)

SetData(s)
wg.Done()
}(i)
}

wg.Wait()

fmt.Println("final data = ", *data)
}

结论



  • unsafe包是为编译器而不是运行时服务的;

  • 使用unsafe做为包名是为了让程序员小心使用此包;

  • 使用unsafe包并非总是不推荐的,有时我们必须使用它;

  • Golang的类型系统设计同时兼顾安全性和效率,但安全性要优先于效率。所以有时候为了普遍安全性将牺牲一些效率。unsafe包使得经验丰富的程序员在某些时候不影响安全性的同时绕开Golang的安全机制,从而避免一些正常使用中的效率牺牲。

  • 当然,再重申一遍,unsafe包很容易被滥用,因此使用之是危险的。




原文:http://www.tapirgames.com/blog/golang-unsafe

golang利用模板生成数据库表对应的模型及操作函数

changjixiong 发表了文章 • 0 个评论 • 1024 次浏览 • 2016-11-13 19:05 • 来自相关话题

起因


  很多年以前,当我第一次接触到ORM的时候,我就有一点疑惑:这玩意用起来倒是方便,就是模型结构得一个字段一个字段的写,非常枯燥也非常累人,而且如果表结构修改了,比如增加、减少或者修改了一个字段,就得修改模型文件。那... 查看全部

起因




  很多年以前,当我第一次接触到ORM的时候,我就有一点疑惑:这玩意用起来倒是方便,就是模型结构得一个字段一个字段的写,非常枯燥也非常累人,而且如果表结构修改了,比如增加、减少或者修改了一个字段,就得修改模型文件。那个时候也没有想到可以从数据库中读取到目标表的表结构数据自动生成ORM需要的模型结构。直到有一天我看到一个根据模板自动生成ORM的模型文件的代码,然后我就用golang也写了这么一个玩意。完整的代码在这里
  


生成过程


准备工作




  假设我在mysql中创建了一个名为dbnote的库,并且创建了一个名为msg的表,创建语句如下:


CREATE TABLE msg (
id int(11) NOT NULL AUTO_INCREMENT,
sender_id int(11) NOT NULL COMMENT '发送者',
receiver_id int(11) NOT NULL COMMENT '接收者',
content varchar(256) NOT NULL COMMENT '内容',
status tinyint(4) NOT NULL,
createtime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);

那么对应的ORM模型则是


type Msg struct {
ID int `db:"id" json:"id"` //
SenderID int `db:"sender_id" json:"sender_id"` // 发送者
ReceiverID int `db:"receiver_id" json:"receiver_id"` // 接收者
Content string `db:"content" json:"content"` // 内容
Status int8 `db:"status" json:"status"` //
Createtime *time.Time `db:"createtime" json:"createtime"` //
}

  


获取表结构信息




  为了生成这个struct以及相关的增删改查代码,我需要先获得这个表的结果信息,以及编写对应的模板文件用于代码生成。
  通过查询语句


SELECT table_name from tables where table_schema='dbnote'

可以获取到这个库中所有的表名(当然,也可以增加过滤条件筛选目标表)。


  用目标表的名称通过查询语句


SELECT COLUMN_NAME,DATA_TYPE,COLUMN_KEY,COLUMN_COMMENT from COLUMNS 
where TABLE_NAME='msg' and table_schema = 'dbnote'

可以获取这个表的结构信息。
表结构的信息用这样一个struct存储


type TABLE_SCHEMA struct {
COLUMN_NAME string `db:"COLUMN_NAME" json:"column_name"`
DATA_TYPE string `db:"DATA_TYPE" json:"data_type"`
COLUMN_KEY string `db:"COLUMN_KEY" json:"column_key"`
COLUMN_COMMENT string `db:"COLUMN_COMMENT" json:"COLUMN_COMMENT"`
}

生成模型及操作函数需要的全部信息,则用这样一个struct存储


type ModelInfo struct {
BDName string
DBConnection string
TableName string
PackageName string
ModelName string
TableSchema *[]TABLE_SCHEMA
}

  


生成struct




  初始化模板对象的代码是这样的


data, _ := ioutil.ReadFile("../modeltool/model.tpl")
render := template.Must(template.New("model").
Funcs(template.FuncMap{
"FirstCharUpper": modeltool.FirstCharUpper,
"TypeConvert": modeltool.TypeConvert,
"Tags": modeltool.Tags,
"ExportColumn": modeltool.ExportColumn,
"Join": modeltool.Join,
"MakeQuestionMarkList": modeltool.MakeQuestionMarkList,
"ColumnAndType": modeltool.ColumnAndType,
"ColumnWithPostfix": modeltool.ColumnWithPostfix,
}).
Parse(string(data)))

函数的定义参见代码


  填充好一个ModelInfo对象后,就可以生成代码文件了


if err := render.Execute(f, model); err != nil {
log.Fatal(err)
}
fmt.Println(fileName)
cmd := exec.Command("goimports", "-w", fileName)
cmd.Run()

最后用goimports添加需要import的package,并且goimports竟然连format工作都做了,简直太爽。
  


相关模板语法说明





  • 以下语句将ModelName的值用函数FirstCharUpper处理为首字面大写然后赋值给$exportModelName变量
    {{$exportModelName := .ModelName | FirstCharUpper}}

  • 以下语句使用变量$exportModelName
    type {{$exportModelName}} struct


  • 以下语句通过函数ExportColumn将字段COLUMN_NAME的值中的下画线去掉并将单词的首字面大写,并且将id处理成ID。ExportColumn内容参见代码,可以根据实际需要调整。


    {{.COLUMN_NAME | ExportColumn}}


  • 以下语句循环处理ModelInfo.TableSchema中的元素
    {{range .TableSchema}} {{.COLUMN_NAME | ExportColumn}} {{.DATA_TYPE | TypeConvert}} {{.COLUMN_NAME | Tags}} // {{.COLUMN_COMMENT}}
    {{end}}}

  • 以下语句用3个参数 .PkColumns,=?, and调用函数ColumnWithPostfix
    {{ColumnWithPostfix .PkColumns "=?" " and "}}"

    函数的定义是


    func ColumnWithPostfix(columns []string, Postfix, sep string) string {
    result := make([]string, 0, len(columns))
    for _, t := range columns {
    result = append(result, t+Postfix)
    }
    return strings.Join(result, sep)
    }

  • 以下语句是另外一种风格的循环
    {{range $K:=.PkColumns}}{{$K}},
    {{end}}

    增删改查代码的自动生成没有什么需要特别说明的,具体参见代码



一点说明


  代码的目录结构是这样的


|____dbnotes
| |____dbhelper
| | |____dbhelper.go
| |____dbnote.go
| |____init.go
| |____model
| | |____mail.go
| | |____msg.go
| | |____notice.go
| |____modelgenerator
| | |____modelgenerator.go
| |____modeltool
| | |____model.tpl
| | |____modeltool.go

  modelgenerator.go 编译后,运行modelgenerator可根据model.tpl将struct及操作函数生成源码文件,存放在model目录下mail.go、msg.go、notice.go的这3个源码文件就是自动生成的。3个表的创建命令在init.go的注释代码中。
  数据库IP地址,用户名及密码在dbhelper.go的init()函数中,DB实例用于连接mail、msg、notice所在的库,SYSDB实例用于连接information_schema库获取表结构信息。
  dbnote.go是示例代码,示范对mail、msg、notice3个表数据的增删改查。


一点问题


  由于golang的基本类型都有一个默认的初始值,不存在定义后没有初始化的变量。所以对于数据库中的NULL就没有一个比较好的直接处理的方式,如果将struct中的数据类型定义为类似这样


Content    *string 

倒是可以接收有内容的值以及NULL,但是这样以来,对于Content的取值和赋值就没那么方便了,当然也可以用NullString。鉴于golang的基本类型都有一个默认的初始值,我个人觉得表里面还是设定为不接受NULL值比较好。

[译]Go里面的unsafe包详解

回复

astaxie 发起了问题 • 7 人关注 • 0 个回复 • 3739 次浏览 • 2016-11-13 09:07 • 来自相关话题

google/gops:一个列出和诊断系统中正在运行Go 进程的命令行工具。

回复

astaxie 发起了问题 • 5 人关注 • 0 个回复 • 1968 次浏览 • 2016-11-12 20:45 • 来自相关话题

Go自动化生成test case

andylau004 回复了问题 • 13 人关注 • 4 个回复 • 1722 次浏览 • 2016-11-11 11:14 • 来自相关话题

[译]Go 1.8 新特性

astaxie 发表了文章 • 8 个评论 • 3731 次浏览 • 2016-11-11 09:59 • 来自相关话题

原文 http://colobu.com/2016/11/05/golang-18-whats-coming/


译自 tylerchr 的 What's Coming in Go 1.8


随着Go 1.8 新特性的开发工作已经冻结,Go 1.8 将在2017年2月左右发布,现在让我们看一些在Go 1.8更有趣的API的改变。


HTTP server connection draining


Brad Fitzpatrick 最近关闭了一个将近四年的issue, 这个issue请求实现http.Server的连接耗尽(draining)的功能。现在可以调用srv.Close可以立即停止http.Server,也可以调用srv.Shutdown(ctx)等待已有的连接处理完毕(耗尽,draining, github.com/tylerb/graceful 的用户应该熟悉这个特性)。


下面这个例子中,服务器当收到SIGINT信号后(^C)会优雅地关闭。


package main
import (
"context"
"io"
"log"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
// subscribe to SIGINT signals
stopChan := make(chan os.Signal)
signal.Notify(stopChan, os.Interrupt)
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
io.WriteString(w, "Finished!")
}))
srv := &http.Server{Addr: ":8081", Handler: mux}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil {
log.Printf("listen: %s\n", err)
}
}()
<-stopChan // wait for SIGINT
log.Println("Shutting down server...")
// shut down gracefully, but wait no longer than 5 seconds before halting
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
srv.Shutdown(ctx)
log.Println("Server gracefully stopped")
}

一旦收到SIGINT信号,服务器会立即停止接受新的连接,srv.ListenAndServe()会返回http.ErrServerClosedsrv.Shutdown会一直阻塞,直到所有未完成的request都被处理完以及它们底层的连接被关闭。


更复杂的处理可以通过context实现,例如使用context.Timeout实现最大的关闭等待时间。你可以尝试复制 https://github.com/tylerchr/examples/tree/master/draining 中的例子并实现它。


通过 http.Pusher 实现 HTTP/2.0 server push


HTTP/2.0 包含 Server Push 特性, 允许 HTTP/2 服务器主动地发送额外的 HTTP response 给客户端,即使客户端没有发送请求。目标是在客户端无需请求的情况下,服务器可以及时地将客户端所需的资源推送给客户端。可以查看wiki HTTP/2 Server Push看具体的例子。


如果一个服务器支持 HTTP/2, 提供给 handlerhttp.ResponseWriter 会实现 http.Pusher 接口。Handler 可以使用这个功能区触发Server Push, 虚拟的请求(synthetic request)可以被注册的 http.Server Handler所处理。


下面的程序处理/index.html, 可以push一个/static/gopher.png:


package main
import "net/http"
func main() {
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
http.Handle("/index.html", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// server push is available if w implements http.Pusher
if p, ok := w.(http.Pusher); ok {
p.Push("/static/gopher.png", nil}
}
// load the main page
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<img src="/static/gopher.png" />`))
}))
http.ListenAndServeTLS(":4430", "cert.pem", "key.pem", nil)
}

你可以从 https://github.com/tylerchr/examples/serverpush 克隆这个例子,下面是在 Chrome 54 访问的结果:



明显地可以在 Initiator 那一列中看到 gopher.png被 Push,你也可以看到标蓝色的gopher.png先于/index.html被接收过来,这表明这个资源先于请求之前被推送到客户端。HTML下载完后可以显示。


有人可能会问如何写一个测试用来校验实现 Server Push的 Handler。因为http.NewTLSServer没有启动 HTTP/2 服务器,httptest.ResponseRecorder也没有实现http.Pusher。我的解决方案是包装httptest.ResponseRecorder实现Pusher接口,这里有个例子。


database/sql


database/sql包有几个主要的改变,可以让用户更好的控制数据库查询,允许用户更好的利用数据库的特性。



  • 查询可以使用context.Context取消查询

  • 纯数据库列类型可以通过sql.ColumnType得到

  • 如果底层数据库支持,查询可以使用命名参数


更多的细节可以阅读Daniel Theophanes的文章 What is new in database/sql?,他实现了大部分的改变。


plugin包实现动态插件


新增加的标准库plugin提供了初步的插件支持,它允许程序可以在运行的时候动态的加载插件。


但是这个库看起来还是bug多多,我甚至不能写一个正常的程序来测试它,但是假定它的使用应该如下面的样子:


// hexify.go
package main
import "encoding/hex"
func Hexify(in string) string {
return hex.EncodeToString([]byte(in))
}
$ go build -buildmode=shared hexify.go
// produces hexify.so

// main.go
package main
import "plugin"
func main() {
p, _ = plugin.Open("hexify.so")
f := p.Lookup("Hexify")
fmt.Println(f.(func(string) string)("gopher"))
// 676f70686572
}

在这个例子中,hexify.go实现了Hexify函数,它被编译成一个共享库,第二个程序动态加载它。这允许Go程序可以不在编译的时候也能调用其它的库。


别名



别名(aliasing)曾被增加到 Go 1.8 的语言规范中,但是现在又被移除了,看这个说明: this post from Russ Cox,有可能会出现在 Go 1.9中。
这个特性也引起了很多的争议,


指示符别名(Identifier aliasing)用来定义多个类型为同一个类型的语法。一个用处用来重构复杂的代码的时候,允许重新划分包而不必带来类型的不一致。 Ian Lance Taylor举了一个例子
举个具体的例子,将扩展包golang.org/x/net/context移动到标准库context的过程。因为context已经被广泛地使用,将所有的用户的代码统一转换很困难,因此允许这两个包通用很有必要。



别名的定义如下:


type Foo => pkg.Bar

这个语法定义 Foo 是 pkg.Bar别名。Foo可以用在任何pkg.Bar出现的地方。以上个例子为例,任何需要类型 golang.org/x/net/context 的地方都可以用标准库context代替,它们是等价的。


别名也可以用在常量、变量、函数等类型上。


这是一个很有争议的特性,可以参考issue 16339golang-dev post 看大家的讨论。因为它从Go 1.8中移除了,大家可以暂时不用关注这个特性了。


新的slice排序API


统一的slice排序由新的 sort.Slice 函数实现。它允许任意的slice都可以被排序,只需提供一个回调比较函数即可,而不是像以前要提供一个特定的sort.Interface的实现。这个函数没有返回值。想其它的排序函数一样,它提供了原地的排序。


下面的例子根据海拔高度排序知名山峰的slice。


type Peak struct {
Name string
Elevation int // in feet
}
peaks := []Peak{
{"Aconcagua", 22838},
{"Denali", 20322},
{"Kilimanjaro", 19341},
{"Mount Elbrus", 18510},
{"Mount Everest", 29029},
{"Mount Kosciuszko", 7310},
{"Mount Vinson", 16050},
{"Puncak Jaya", 16024},
}
// does an in-place sort on the peaks slice, with tallest peak first
sort.Slice(peaks, func(i, j int) bool {
return peaks[i].Elevation >= peaks[j].Elevation
})
// peaks is now sorted

通过sort.Interface类型的Len()Swap(i, j int)提供了抽象的排序类型,这是以前的排序方法,而Less(i, j int)作为一个比较回调函数,可以简单地传递给sort.Slice进行排序。


其它


87b1aaa encoding/base64 encoder现在有了严格模式.
6ba5b32 expvar暴露出来,可以用在其它的mux中.
003a598 伪随机码可以通过rand.Uint64()产生 (先前仅支持uint32).
67ea710 增加了一个新的time.Until函数,和time.Since对应.
net/http故意只实现了使用TLS的HTTP/2,你可以查看[issue 14141]https://github.com/golang/go/issues/14141()了解细节
sort.SliceStable提供了稳定的slice排序,就像以前的sort.Stable一样。


译者增加的内容


Go 1.8 一个很大的特性就是性能的提升,包括二进制文件的大小、编译速度和运行速度。
并且非常大的提升就是提供小于100us GC暂停。


net/http提供了更多的超时设置,比如ReadHeaderTimeoutIdleTimeout


一个完整的改动列表:Go 1.8

Go 7岁了

astaxie 发表了文章 • 2 个评论 • 800 次浏览 • 2016-11-11 09:21 • 来自相关话题

Go 开源至今已经七年了,Go 最近公布的语言排名也是一下子串到了13位,可以说应用 Go 的用户也是越来越多。... 查看全部


Go 开源至今已经七年了,Go 最近公布的语言排名也是一下子串到了13位,可以说应用 Go 的用户也是越来越多。


这一路过来感觉 Go 确实发展非常快,从最早的r58版本使用过来,可以看到 Go Team 也是一直在不断的改进内核,特别是 GC 的改进,简直可以用 amazing 来形容。即将发布的1.8版本更加是缩短到 100us 之内。


当然也是期望我们国内的社区发展的越来越好,回顾很多开源的 Go 开源项目,或多或少都有我们国人的身影,这样让我更加的坚信在未来的几年 Go 会成为一门主流的开发语言。


官方也发了文庆祝 Go 7岁了
https://blog.golang.org/7years

RobotGo v0.30.0,Golang跨平台控制鼠标键盘位图屏幕,全局事件监听,增加窗口句柄

回复

veni 发起了问题 • 1 人关注 • 0 个回复 • 1135 次浏览 • 2016-11-10 20:45 • 来自相关话题

Go1.7里面的BCE(跳跃检测排除)(译)

astaxie 发表了文章 • 0 个评论 • 762 次浏览 • 2016-11-10 19:10 • 来自相关话题

最近发布的 Go 1.7 里面使用了一个新的基于 SSA 的编译后端(目前只有 amd64 可用)。SSA 使得编译出来的代码更加高效 SSA,主要是里面有包含了 查看全部

最近发布的 Go 1.7 里面使用了一个新的基于 SSA 的编译后端(目前只有 amd64 可用)。SSA 使得编译出来的代码更加高效 SSA,主要是里面有包含了 BCE公共子表达式消除


这篇文章将会通过一些例子给大家展示 Go 1.7 里面BCE是如何工作的。


在 Go 1.7 里面我们可以通过这个命令 go build -gcflags="-d=ssa/check_bce/debug=1" 来展示我们的代码哪一行需要进行越界检查。


例子1


// example1.go
package main

func f1(s []int) {
_ = s[0] // line 5: bounds check
_ = s[1] // line 6: bounds check
_ = s[2] // line 7: bounds check
}

func f2(s []int) {
_ = s[2] // line 11: bounds check
_ = s[1] // line 12: bounds check eliminatd!
_ = s[0] // line 13: bounds check eliminatd!
}

func f3(s []int, index int) {
_ = s[index] // line 17: bounds check
_ = s[index] // line 18: bounds check eliminatd!
}

func f4(a [5]int) {
_ = a[4] // line 22: bounds check eliminatd!
}

func main() {}

$ go build -gcflags="-d=ssa/check_bce/debug=1" example1.go
# command-line-arguments
./11.go:5: Found IsInBounds
./11.go:6: Found IsInBounds
./11.go:7: Found IsInBounds
./11.go:11: Found IsInBounds
./11.go:17: Found IsInBounds

我们可以看到这个f2函数里面的12行和13行不需要进行越界检查,因为在11行里面的越界检查可以保证12,13行的代码是不会越界的。


但是在f1里面必须每一行都逐一检查越界,因为第5行没办法保证第6、第7行是安全的,第6行没办法保证第7行是安全的。


函数f3里面,Go 1.7 编译器知道如果第一行是安全的情况下,那么第二行的s[index]是完全安全的


Go 1.7 的编辑器也正确的分析出来了f4函数里面的唯一的一行(22行)也是安全的。


例子2


// example2.go
package main

func f5(s []int) {
for i := range s {
_ = s[i]
_ = s[i:len(s)]
_ = s[:i+1]
}
}

func f6(s []int) {
for i := 0; i < len(s); i ++ {
_ = s[i]
_ = s[i:len(s)]
_ = s[:i+1]
}
}

func f7(s []int) {
for i := len(s) - 1; i >= 0; i -- {
_ = s[i] // line 22: bounds check
_ = s[i:len(s)]
}
}

func f8(s []int, index int) {
if index >= 0 && index < len(s) {
_ = s[index]
_ = s[index:len(s)]
}
}

func f9(s []int) {
if len(s) > 2 {
_, _, _ = s[0], s[1], s[2]
}
}

func main() {}

$ go build -gcflags="-d=ssa/check_bce/debug=1" example2.go
# command-line-arguments
./11.go:22: Found IsInBounds

我们可以看到在例子2里面只有一行需要进行越界检查


Go 1.7 编译器是如此的聪明,它精确的做出决定说:在f5和f6里面所有的代码都是安全的


但是好像还是没有我们人类聪明,看上去22行也是安全的


例子3


// example3.go
package main

import "math/rand"

func fa() {
s := []int{0, 1, 2, 3, 4, 5, 6}
index := rand.Intn(7)
_ = s[:index] // line 9: bounds check
_ = s[index:] // line 10: bounds check eliminatd!
}

func fb(s []int, index int) {
_ = s[:index] // line 14: bounds check
_ = s[index:] // line 15: bounds check // not smart enough or a bug?
}

func fc() {
s := []int{0, 1, 2, 3, 4, 5, 6}
s = s[:4]
index := rand.Intn(7)
_ = s[:index] // line 22: bounds check
_ = s[index:] // line 23: bounds check
}

func main() {}

$ go build -gcflags="-d=ssa/check_bce/debug=1" example3.go
# command-line-arguments
./11.go:9: Found IsSliceInBounds
./11.go:14: Found IsSliceInBounds
./11.go:15: Found IsSliceInBounds
./11.go:22: Found IsSliceInBounds
./11.go:23: Found IsSliceInBounds

啊!那么多地方需要进行越界检查


但是等等,为什么 Go 1.7 的编译器会认为第10行是安全的,但是15行和23行不安全?是因为编译器不够聪明还是这是一个bug?


实际上,编译器在这里是对的!为什么?原因是子slice可能大于原始的slice,看下面这个例子:


package main

func main() {
s0 := make([]int, 5, 10) // len(s0) == 5, cap(s0) == 10

index := 8

// In golang, for the subslice syntax s[a:b],
// the valid rage for a is [0, len(s)],
// the valid rage for b is [a, cap(s)].

// So, this line is no problem.
_ = s0[:index]
// But, above line is safe can't assure the following line is also safe.
// In fact, it will panic.
_ = s0[index:] // panic: runtime error: slice bounds out of range
}

所以如果s[:index]是安全的话,也就当len(s) == cap(s)的时候,s[index:]才是安全的
。这就是为什么在例子3里面fb和fc里面的代码还需要进行越界检查。


Go 1.7 编译器在函数fa里面成功的检测到了 len(s) == cap(s)。太棒了!Golang Team!


然而,如果s[index:]是安全的话,那么s[:index]就一直是安全的。


例子4


// example4.go
package main

import "math/rand"

func fa2() {
s := []int{0, 1, 2, 3, 4, 5, 6}
index := rand.Intn(7)
_ = s[index:] // line 9: bounds check
_ = s[:index] // line 10: bounds check eliminatd!
}

func fb2(s []int, index int) {
_ = s[index:] // line 14: bounds check
_ = s[:index] // line 15: bounds check // not smart enough?
}

func fc2() {
s := []int{0, 1, 2, 3, 4, 5, 6}
s = s[:4]
index := rand.Intn(7)
_ = s[index:] // line 22: bounds check
_ = s[:index] // line 23: bounds check eliminatd!
}

func main() {}

$ go build -gcflags="-d=ssa/check_bce/debug=1" example4.go
# command-line-arguments
./11.go:9: Found IsSliceInBounds
./11.go:14: Found IsSliceInBounds
./11.go:15: Found IsSliceInBounds
./11.go:22: Found IsSliceInBounds

在这个例子中, Go 1.7 编译器成功的计算出来在fc2函数里面如果22行安全的情况下那么23行也是安全的,但是在fb2里面没办法计算出来如果14行安全的情况下15行也是安全的情况。


例子5


尽管当前的编译器(Go1.7.1 amd64) 还是不够智能的排除一些不需要检测的地方,但是我们可以做一些暗示来帮助编译器排除这些不必要的越界检查。


// example5.go
package main

func fd(is []int, bs []byte) {
if len(is) >= 256 {
for _, n := range bs {
_ = is[n] // line 7: bounds check, not smart enough.
}
}
}

func fd2(is []int, bs []byte) {
if len(is) >= 256 {
is = is[:256] // line 14: bounds check. A hint for the compiler.
for _, n := range bs {
_ = is[n] // line 16: bounds check eliminatd!
}
}
}

func fe(isa []int, isb []int) {
if len(isa) > 0xFFF {
for _, n := range isb {
_ = isa[n & 0xFFF] // line 24: bounds check, not smart enough.
}
}
}

func fe2(isa []int, isb []int) {
if len(isa) > 0xFFF {
isa = isa[:0xFFF+1] // line 31: bounds check. A hint for the compiler.
for _, n := range isb {
_ = isa[n & 0xFFF] // line 33: bounds check eliminatd!
}
}
}

func ff(s []int) []int {
s2 := make([]int, len(s))
for i := range s {
s2[i] = -s[i] // line 41: bounds check, not smart enough.
}
return s2
}

func ff2(s []int) []int {
s2 := make([]int, len(s))
s2 = s2[:len(s)] // line 48: bounds check. A hint for the compiler.
for i := range s {
s2[i] = -s[i] // line 50: bounds check eliminatd!
}
return s2
}

func main() {}

$ go build -gcflags="-d=ssa/check_bce/debug=1" example5.go
# command-line-arguments
./11.go:7: Found IsInBounds
./11.go:14: Found IsSliceInBounds
./11.go:24: Found IsInBounds
./11.go:31: Found IsSliceInBounds
./11.go:41: Found IsInBounds
./11.go:48: Found IsSliceInBounds

总结


尽管当前1.7版本里面的 BCE 特性还不够完美,但是在大多数情况下还是表现的非常好。毫无疑问Go的编译器将会在后续版本里面做的更好,感谢 Go 团队增加了如此帮的特性


参考文献



  1. gBounds Checking Elimination

  2. Utilizing the Go 1.7 SSA Compiler


原文:http://www.tapirgames.com/blog/golang-1.7-bce

Go依赖委员会成立了,起草了一份包管理的草案

lgn21st 回复了问题 • 11 人关注 • 4 个回复 • 2376 次浏览 • 2016-11-10 15:21 • 来自相关话题

golang内存检测工具

sheepbao 回复了问题 • 8 人关注 • 4 个回复 • 2241 次浏览 • 2016-11-09 19:22 • 来自相关话题

Go发布至今发现的安全漏洞 5 个

回复

astaxie 发起了问题 • 1 人关注 • 0 个回复 • 1674 次浏览 • 2016-11-08 15:46 • 来自相关话题

新版本Go将会对database/sql进行大量改进

niugou 回复了问题 • 6 人关注 • 3 个回复 • 1718 次浏览 • 2016-11-05 21:23 • 来自相关话题

Go语法要增加一个alias的功能

boilingbit 回复了问题 • 6 人关注 • 8 个回复 • 1939 次浏览 • 2016-11-05 16:27 • 来自相关话题

TiDB 优化器实现的基础:统计信息的收集

qiuyesuifeng 发表了文章 • 0 个评论 • 464 次浏览 • 2016-11-04 10:37 • 来自相关话题

收集统计信息的意义 一个 SQL 数据库里,优化器实现的好坏对性能的影响是决定性的。一个未经优化的执行计划和经过充分优化后的执行计划,执行时间的差别往往是成千上万倍。而对一个 SQL 优化器来说... 查看全部


收集统计信息的意义
一个 SQL 数据库里,优化器实现的好坏对性能的影响是决定性的。一个未经优化的执行计划和经过充分优化后的执行计划,执行时间的差别往往是成千上万倍。而对一个 SQL 优化器来说,统计信息是必不可少的条件,只有依赖统计信息提供的数据,优化器才可以正确估算不同的执行计划的执行代价,以选择最优的执行计划。就像一个大厨无论多么优秀,没有上等食材也是无法做出美味的饭菜。



统计信息包含的内容


统计信息有两类,包括 Table 统计信息和 Column 统计信息。
Table 统计信息包含 Row Count 和 Row Size。
Column 统计信息包含 Null Count,Max Value,Min Value,Distinct Count 以及 Histogram。其中 Histogram 用来记录这个 Column 的数据详细分布,可以用来估算大于、小于或等于某个 Value 的 Row Count。


统计信息采集的步骤


1)在 TiDB 执行 ANALYZE TABLE 语句,手动触发收集动作。
我们知道,一个 Table 的数据往往是在不断变化的,我们无法保证统计信息时刻保持在最新状态,总会有一定的误差,如果我们不及时更新,随着时间的推进,这个误差可能会越来越大,影响到优化器的优化效果。
有时我们会需要让统计信息更新的频率低一些来降低系统的压力,因为每次的统计信息收集都是开销很大的操作。有时我们会需要立即更新统计数据,因为我们刚刚向一个表导入了大量的数据,马上就需要查询。
所以定期更新统计信息的功能,我们希望可以用独立的模块,用更灵活的策略来实现,TiDB 本身只需要支持基本的手动触发就可以了。


2)全表扫描。
全表扫描的执行过程比较长,整个扫表的任务会被分解为一系列 Region 的小请求,每个 Region 的请求会返回该 Region 包含的 Table 的部分数据。


3)用扫描得到的数据,记录 Row Count 和 Row Size,并对数据采样。
扫描得到的数据量有可能会非常大,以至于无法把全部数据保留在内存里进行处理,所以需要进行采样,当采样的概率均匀的时候,计算生成的统计信息的误差是可以接受的。这里我们设定的采样数是 1 万,无论 Table 有多大,最后保留的样本数不会超过 1 万。后面会详细介绍采样时使用到的算法。


4)采样数据生成 Column 统计信息,并保存到 KV。
采样得到的数据会进行计算生成 Histogram。


采样算法


要得到一个均匀分布的采样池,一个最简单的算法是,当我扫描整个表的时候,读到的每一行,都以一个固定的概率决定是否加入采样池,这个概率 P =(采样池大小/表的行数)。但是由于在扫描之前,我们并不知道一个表总共有多少行,所以如果使用这个算法进行采样,就需要扫描两次,第一次获取整个表的行数,第二次进行真正的采样。
一个更优化的算法,蓄水池算法,可以在表的大小未知的情况下,一次扫描得到均匀分布的采样池。


算法的实现
假如我们的样本池大小为 $M = 100$ ,从头开始扫描全表,当我们读到的记录个数 $K < 100$ 时,我们把每一条记录都加入采样池,这样保证了在记录总数小于采样池大小时,所有记录都会被选中。
我们继续扫描,当我们扫描到的第 $K = 101 条$时,我们用概率 $P = (M/K) = (100/101)$ 决定是否把这个新的记录加入采样池,如果加入了采样池,采样池的总数会超过 $M$ 的限制,这时我们需要随机选择一个旧的采样丢掉,保证采样池大小不会超过限制。
执行这样采样算法一直到全表扫描结束,我们可以得到一个均匀分布的采样池。


算法的证明
这个算法可以用归纳法来证明,如果表的大小 $N <= K$,所有记录都会放入采样池,满足均匀分布的要求。
现在假设当读取完 K 个元素时,采样池满足均匀分布的要求,采样概率 $P = (M / K)$,当读取第 $K + 1$ 个记录时,我们应用蓄水池算法,以 $P' = M / (K + 1)$ 的概率决定是否加入采样池,这时出现了两种情况,被加入和没有被加入,我们继续分析这两种情况下,这条记录被选中的概率。
如果记录没有被选中,旧样本被保留概率是$Po1 =(1 - P') P = (1 - M / (K + 1)) (M / K)$, 新样本被保留的概率是 Pn1 = 0。
如果记录被选中,采样池中已有的采样有 (M - 1) / M 的概率被保留,旧样本概率是 $Po2 = P' P Po = (M / (K + 1)) (M / K) ( (M -1) / M)$,新样本的整体概率是 $Pn2 = P' = M / (K + 1)$。
把两种情况下的概率相加, 得到旧采样被保留的概率 $Po = Po1 + Po2 = (1 - M / (K + 1)) (M / K) + (M / (K + 1)) (M / K) *( (M -1) / M) = M / (K + 1)$,新采样被保留的概率为 $Pn = Pn1 + Pn2 = 0 + M / (K + 1) = M / (K + 1)$,所有采样被保留的概率都是 $M / (K + 1)$,满足均匀分布的要求。


Histogram
Histogram 的类型主要有两种,Frequency Histogram 和 Height-Balanced Histogram。
当 $NDV < Bucket Count$ 时,Frequency 可以包含全部 value 分布信息,每一个 distinct value 占用一个 bucke。
但是当 $NDV > bucket count$ 时,Frequency Histogram 没有足够的 bucket 存放 value,我们就需要用另外的 Histogram 类型,比如 Height-Balanced Histogram。
Height-Balanced Histogram 把所有 value 从小到大排序,平均放到所有 bucket 里,但是缺点是无法记录有哪些 popular value。
Oracle 还实现了一种 Hibrid Histogram,综合了 Frequency Histogram 和 Height-Balanced Histogram 的优点,TiDB 实现的 Histogram 主要参考的就是 Oracle 的 Hibraid Histogram。
Hibrid Histogram 包含 N 个 bucket,我们设定的 N 的默认值是 256,每个 bucket 包含三个字段 numbervaluerepeatnumber 代表放在这个 bucket 里的最后一个 value 在 table 里排序后的 offset,value 就是放在这个 bucket 里的最大的那一个 value,repeat 代表最大的 value 的个数。
Hibrid Histogram 在生成的过程中,如果一个 bucket 装满了,遇到下一个 value 的时候,比较一下这个新的 value 和前一个 value 是否相等,如果相等,增加 repeat 值,直到遇到一个更大的 value,换下一个 bucket 存放这个 value,这样保证任何一个 value 只会在 一个 bucket 内存在,相比 Height-Balanced Histogram,可以包含更准确的 value 分布信息。
我们用一个实例来说明 Hibrid Histogram 是如何存储 value 分布信息的。
给定 value 集合 $['a', 'a', 'b', 'c', c', 'c', 'c', 'd', 'd', ‘e’]$ 和 bucket count 3,生成的 histogram 如下:



[number, value, repeat]
[2, 'b', 0]
[6, 'c', 3]
[9, 'e', 0]



我们可以看到这个集合内, 'c' 的个数有 4 个,在第二个 bucket 里准确记录了 'c' 的 repeat 数量 3,这样我们在查询条件为 where column = 'c'的时候,就可以准确的估算执行开销。
统计信息的收集,使 TiDB 的优化器掌握数据分布详情,准确估算执行开销,从而实现高效的 CBO (cost base optimization)。




这是对 PingCAP 第 15 期 NewSQL Meetup 中《TiDB 优化器统计信息的采集》这一议题的干货整理。

作为一个前沿领域的技术公司,PingCAP 希望能为在国内营造真正关注技术本身的社区和 Meetup 贡献一分力量。自 2016 年 3 月 5 日开始 ,我们在每周六举办 NewSQL Meetup,通过 1-2 个议题与大家交流讨论,并跟大家分享 TiDB 最新的一些进展和我们的思考,希望藉由这个机会让更多人关注到下一代分布式数据库。同时,我们也会定期从 Meetup 分享中筛选议题以文章形式与大家做进一步深度探讨。欢迎感兴趣的小伙伴关注 TiDB 项目,盼望各路大牛加入 PingCAP。