标签 struct 下的文章

Go 1.8中值得关注的几个变化

在已经过去的2016年Go语言继在2009年之后再次成为编程语言界的明星- 问鼎TIOBE 2016年度语言。这与Go team、Go community和全世界的Gophers的努力是分不开的。按计划在这个2月份,Go team将正式发布Go 1.8版本(截至目前,Go的最新版本是Go 1.8rc3)。在这里我们一起来看一下在Go 1.8版本中都有哪些值得Gopher们关注的变化。

一、语言(Language)

Go 1.8版本依旧坚守Go Team之前的承诺,即Go1兼容性:使用Go 1.7及以前版本编写的Go代码,理论上都可以通过Go 1.8进行编译并运行。因此在臆想中的Go 2.0变成现实之前,每个Go Release版本在语言这方面的“改变”都会是十分微小的。

1、仅tags不同的两个struct可以相互做显式类型转换

在Go 1.8版本以前,两个struct即便字段个数相同且每个字段类型均一样,但如果某个字段的tag描述不一样,这两个struct相互间也不能做显式类型转换,比如:

//go18-examples/language/structtag.go
package main

import "fmt"

type XmlEventRegRequest struct {
    AppID     string `xml:"appid"`
    NeedReply int    `xml:"Reply,omitempty"`
}

type JsonEventRegRequest struct {
    AppID     string `json:"appid"`
    NeedReply int    `json:"reply,omitempty"`
}

func convert(in *XmlEventRegRequest) *JsonEventRegRequest {
    out := &JsonEventRegRequest{}
    *out = (JsonEventRegRequest)(*in)
    return out
}

func main() {
    in := XmlEventRegRequest{
        AppID:     "wx12345678",
        NeedReply: 1,
    }
    out := convert(&in)
    fmt.Println(out)
}

采用Go 1.7.4版本go compiler进行编译,我们会得到如下错误输出:

$go build structtag.go
# command-line-arguments
./structtag.go:17: cannot convert *in (type XmlEventRegRequest) to type JsonEventRegRequest

但在Go 1.8中,gc将忽略tag值的不同,使得显式类型转换成为可能:

$go run structtag.go
&{wx12345678 1}

改变虽小,但带来的便利却不小,否则针对上面代码中的convert,我们只能做逐一字段赋值了。

2、浮点常量的指数部分至少支持16bits长

在Go 1.8版本之前的The Go Programming Language Specificaton中,关于浮点数常量的指数部分的描述如下:

Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed exponent of at least 32 bits.

在Go 1.8版本中,文档中对于浮点数常量指数部分的长度的实现的条件放宽了,由支持最少32bit,放宽到最少支持16bits:

Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed binary exponent of at least 16 bits.

但Go 1.8版本go compiler实际仍然支持至少32bits的指数部分长度,因此这个改变对现存的所有Go源码不会造成影响。

二、标准库(Standard Library)

Go号称是一门”Batteries Included”编程语言。“Batteries Included”指的就是Go语言强大的标准库。使用Go标准库,你可以完成绝大部分你想要的功能,而无需再使用第三方库。Go语言的每次版本更新,都会在标准库环节增加强大的功能、提升性能或是提高使用上的便利性。每次版本更新,标准库也是改动最大的部分。这次也不例外,我们逐一来看。

1、便于slice sort的sort.Slice函数

在Go 1.8之前我们要对一个slice进行sort,需要定义出实现了下面接口的slice type:

//$GOROOT/src/sort.go
... ...
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

标准库定义了一些应对常见类型slice的sort类型以及对应的函数:

StringSlice -> sort.Strings
IntSlice -> sort.Ints
Float64Slice -> sort.Float64s

但即便如此,对于用户定义的struct或其他自定义类型的slice进行排序仍需定义一个新type,比如下面这个例子中的TiboeIndexByRank:

//go18-examples/stdlib/sort/sortslice-before-go18.go
package main

import (
    "fmt"
    "sort"
)

type Lang struct {
    Name string
    Rank int
}

type TiboeIndexByRank []Lang

func (l TiboeIndexByRank) Len() int           { return len(l) }
func (l TiboeIndexByRank) Less(i, j int) bool { return l[i].Rank < l[j].Rank }
func (l TiboeIndexByRank) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }

func main() {
    langs := []Lang{
        {"rust", 2},
        {"go", 1},
        {"swift", 3},
    }
    sort.Sort(TiboeIndexByRank(langs))
    fmt.Printf("%v\n", langs)
}

$go run sortslice-before-go18.go
[{go 1} {rust 2} {swift 3}]

从上面的例子可以看到,我们要对[]Lang这个slice进行排序,我们就需要为之定义一个专门用于排序的类型:这里是TiboeIndexByRank,并让其实现sort.Interface接口。使用过sort包的gophers们可能都意识到了,我们在为新的slice type实现sort.Interface接口时,那三个方法的Body几乎每次都是一样的。为了使得gopher们在排序slice时编码更为简化和便捷,减少copy&paste,Go 1.8为slice type新增了三个函数:Slice、SliceStable和SliceIsSorted。我们重新用Go 1.8的sort.Slice函数实现上面例子中的排序需求,代码如下:

//go18-examples/stdlib/sort/sortslice-in-go18.go
package main

import (
    "fmt"
    "sort"
)

type Lang struct {
    Name string
    Rank int
}

func main() {
    langs := []Lang{
        {"rust", 2},
        {"go", 1},
        {"swift", 3},
    }
    sort.Slice(langs, func(i, j int) bool { return langs[i].Rank < langs[j].Rank })
    fmt.Printf("%v\n", langs)
}

$go run sortslice-in-go18.go
[{go 1} {rust 2} {swift 3}]

实现sort,需要三要素:Len、Swap和Less。在1.8之前,我们通过实现sort.Interface实现了这三个要素;而在1.8版本里,Slice函数通过reflect获取到swap和length,通过结合闭包实现的less参数让Less要素也具备了。我们从下面sort.Slice的源码可以看出这一点:

// $GOROOT/src/sort/sort.go
... ...
func Slice(slice interface{}, less func(i, j int) bool) {
    rv := reflect.ValueOf(slice)
    swap := reflect.Swapper(slice)
    length := rv.Len()
    quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length))
}

2、支持HTTP/2 Push

继在Go 1.6版本全面支持HTTP/2之后,Go 1.8又新增了对HTTP/2 Push的支持。HTTP/2是在HTTPS的基础上的下一代HTTP协议,虽然当前HTTPS的应用尚不是十分广泛。而HTTP/2 Push是HTTP/2的一个重要特性,无疑其提出的初衷也仍然是为了改善网络传输性能,提高Web服务的用户侧体验。这里我们可以借用知名网络提供商Cloudflare blog上的一幅示意图来诠释HTTP/2 Push究竟是什么:

img{512x368}

从上图中,我们可以看到:当Browser向Server发起Get page.html请求后,在同一条TCP Connection上,Server主动将style.css和image.png两个资源文件推送(Push)给了Browser。这是由于Server端启用了HTTP/2 Push机制,并预测判断Browser很可能会在接下来发起Get style.css和image.png两个资源的请求。这是一种典型的:“你可能会需要,但即使你不要,我也推给你”的处世哲学^0^。这种机制虽然在一定程度上能改善网络传输性能(减少Client发起Get的次数),但也可能造成带宽的浪费,因为这些主动推送给Browser的资源很可能是Browser所不需要的或是已经在Browser cache中存在的资源。

接下来,我们来看看Go 1.8是如何在net/http包中提供对HTTP/2 Push的支持的。由于HTTP/2是基于HTTPS的,因此我们先使用generate_cert.go生成程序所需的私钥和证书:

// 在go18-examples/stdlib/http2-push目录下,执行:

$go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1
2017/01/27 10:58:01 written cert.pem
2017/01/27 10:58:01 written key.pem

支持HTTP/2 Push的server端代码如下:

// go18-examples/stdlib/http2-push/server.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

const mainJS = `document.write('Hello World!');`

func main() {
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
            http.NotFound(w, r)
            return
        }
        pusher, ok := w.(http.Pusher)
        if ok {
            // If it's a HTTP/2 Server.
            // Push is supported. Try pushing rather than waiting for the browser.
            if err := pusher.Push("/static/img/gopherizeme.png", nil); err != nil {
                log.Printf("Failed to push: %v", err)
            }
        }
        fmt.Fprintf(w, `<html>
<head>
<title>Hello Go 1.8</title>
</head>
<body>
    <img src="/static/img/gopherizeme.png"></img>
</body>
</html>
`)
    })
    log.Fatal(http.ListenAndServeTLS(":8080", "./cert.pem", "./key.pem", nil))
}

运行这段代码,打开Google Chrome浏览器,输入:https://127.0.0.1:8080,忽略浏览器的访问非受信网站的警告,继续浏览你就能看到下面的页面(这里打开了Chrome的“检查”功能):

img{512x368}

从示例图中的“检查”窗口,我们可以看到gopherizeme.png这个image资源就是Server主动推送给客户端的,这样浏览器在Get /后无需再发起一次Get /static/img/gopherizeme.png的请求了。

而这一切的背后,其实是HTTP/2的ResponseWriter实现了Go 1.8新增的http.Pusher interface:

// $GOROOT/src/net/http/http.go

// Pusher is the interface implemented by ResponseWriters that support
// HTTP/2 server push. For more background, see
// https://tools.ietf.org/html/rfc7540#section-8.2.
type Pusher interface {
    ... ...
    Push(target string, opts *PushOptions) error
}

3、支持HTTP Server优雅退出

Go 1.8中增加对HTTP Server优雅退出(gracefullly exit)的支持,对应的新增方法为:

func (srv *Server) Shutdown(ctx context.Context) error

和server.Close在调用时瞬间关闭所有active的Listeners和所有状态为New、Active或idle的connections不同,server.Shutdown首先关闭所有active Listeners和所有处于idle状态的Connections,然后无限等待那些处于active状态的connection变为idle状态后,关闭它们并server退出。如果有一个connection依然处于active状态,那么server将一直block在那里。因此Shutdown接受一个context参数,调用者可以通过context传入一个Shutdown等待的超时时间。一旦超时,Shutdown将直接返回。对于仍然处理active状态的Connection,就任其自生自灭(通常是进程退出后,自动关闭)。通过Shutdown的源码我们也可以看出大致的原理:

// $GOROOT/src/net/http/server.go
... ...
func (srv *Server) Shutdown(ctx context.Context) error {
    atomic.AddInt32(&srv.inShutdown, 1)
    defer atomic.AddInt32(&srv.inShutdown, -1)

    srv.mu.Lock()
    lnerr := srv.closeListenersLocked()
    srv.closeDoneChanLocked()
    srv.mu.Unlock()

    ticker := time.NewTicker(shutdownPollInterval)
    defer ticker.Stop()
    for {
        if srv.closeIdleConns() {
            return lnerr
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
        }
    }
}

我们来编写一个例子:

// go18-examples/stdlib/graceful/server.go

import (
    "context"
    "io"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"
)

func main() {
    exit := make(chan os.Signal)
    signal.Notify(exit, os.Interrupt)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        log.Println("Handle a new request:", *r)
        time.Sleep(10 * time.Second)
        log.Println("Handle the request ok!")
        io.WriteString(w, "Finished!")
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: http.DefaultServeMux,
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil {
            log.Printf("listen: %s\n", err)
        }
    }()

    <-exit // wait for SIGINT
    log.Println("Shutting down server...")

    // Wait no longer than 30 seconds before halting
    ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
    err := srv.Shutdown(ctx)

    log.Println("Server gracefully stopped:", err)
}

在上述例子中,我们通过设置Linux Signal的处理函数来拦截Linux Interrupt信号并处理。我们通过context给Shutdown传入30s的超时参数,这样Shutdown在退出之前会给各个Active connections 30s的退出时间。下面分为几种情况run一下这个例子:

a) 当前无active connections

在这种情况下,我们run上述demo,ctrl + C后,上述demo直接退出:

$go run server.go
^C2017/02/02 15:13:16 Shutting down server...
2017/02/02 15:13:16 Server gracefully stopped: <nil>

b) 当前有未处理完的active connections,ctx 超时

为了模拟这一情况,我们修改一下参数。让每个request handler的sleep时间为30s,而Shutdown ctx的超时时间改为10s。我们再来运行这个demo,并通过curl命令连接该server(curl -v http://localhost:8080),待连接成功后,再立即ctrl+c停止Server,待约10s后,我们得到如下日志:

$go run server.go
2017/02/02 15:15:57 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} <nil> 0 [] false localhost:8080 map[] map[] <nil> map[] [::1]:52590 / <nil> <nil> <nil> 0xc420016700}
^C2017/02/02 15:15:59 Shutting down server...
2017/02/02 15:15:59 listen: http: Server closed
2017/02/02 15:16:09 Server gracefully stopped: context deadline exceeded

c) 当前有未处理完的active connections,ctx超时之前,这些connections处理ok了

我们将上述demo的参数还原,即request handler sleep 10s,而Shutdown ctx超时时间为30s,运行这个Demo后,通过curl命令连接该server,待连接成功后,再立即ctrl+c停止Server。等待约10s后,我们得到如下日志:

$go run server.go
2017/02/02 15:19:56 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} <nil> 0 [] false localhost:8080 map[] map[] <nil> map[] [::1]:52605 / <nil> <nil> <nil> 0xc420078500}
^C2017/02/02 15:19:59 Shutting down server...
2017/02/02 15:19:59 listen: http: Server closed
2017/02/02 15:20:06 Handle the request ok!
2017/02/02 15:20:06 Server gracefully stopped: <nil>

可以看出,当ctx超时之前,request处理ok,connection关闭。这时不再有active connection和idle connection了,Shutdown成功返回,server立即退出。

4、Mutex Contention Profiling

Go 1.8中runtime新增了对Mutex和RWMutex的profiling(剖析)支持。golang team成员,负责从go user角度去看待go team的work是否满足用户需求的Jaana B. Dogan在其个人站点上写了一篇介绍mutex profiling的文章,这里借用一下其中的Demo:

//go18-examples/stdlib/mutexprofile/mutexprofile.go

package main

import (
    "net/http"
    _ "net/http/pprof"
    "runtime"
    "sync"
)

func main() {
    var mu sync.Mutex
    var items = make(map[int]struct{})

    runtime.SetMutexProfileFraction(5)
    for i := 0; i < 1000*1000; i++ {
        go func(i int) {
            mu.Lock()
            defer mu.Unlock()
            items[i] = struct{}{}
        }(i)
    }

    http.ListenAndServe(":8888", nil)
}

运行该程序后,在浏览器中输入:http://localhost:8888/debug/pprof/mutex,你就可以看到有关该程序的mutex profile(耐心等待一小会儿,因为数据的采样需要一点点时间^0^):

--- mutex:
cycles/second=2000012082
sampling period=5
378803564 776 @ 0x106c4d1 0x13112ab 0x1059991

构建该程序,然后通过下面命令:

go build mutexprofile.go
./mutexprofile
go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1

可以进入pprof交互界面,这个是所有用过go pprof工具gophers们所熟知的:

$go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1
Fetching profile from http://localhost:8888/debug/pprof/mutex?debug=1
Saved profile in /Users/tony/pprof/pprof.mutexprofile.localhost:8888.contentions.delay.003.pb.gz
Entering interactive mode (type "help" for commands)
(pprof) list
Total: 12.98s
ROUTINE ======================== main.main.func1 in /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/experiments/go18-examples/stdlib/mutexprofile/mutexprofile.go
         0     12.98s (flat, cum)   100% of Total
         .          .     17:            mu.Lock()
         .          .     18:            defer mu.Unlock()
         .          .     19:            items[i] = struct{}{}
         .          .     20:        }(i)
         .          .     21:    }
         .     12.98s     22:
         .          .     23:    http.ListenAndServe(":8888", nil)
         .          .     24:}
ROUTINE ======================== runtime.goexit in /Users/tony/.bin/go18rc2/src/runtime/asm_amd64.s
         0     12.98s (flat, cum)   100% of Total
         .          .   2192:    RET
         .          .   2193:
         .          .   2194:// The top-most function running on a goroutine
         .          .   2195:// returns to goexit+PCQuantum.
         .          .   2196:TEXT runtime·goexit(SB),NOSPLIT,$0-0
         .     12.98s   2197:    BYTE    $0x90    // NOP
         .          .   2198:    CALL    runtime·goexit1(SB)    // does not return
         .          .   2199:    // traceback from goexit1 must hit code range of goexit
         .          .   2200:    BYTE    $0x90    // NOP
         .          .   2201:
         .          .   2202:TEXT runtime·prefetcht0(SB),NOSPLIT,$0-8
ROUTINE ======================== sync.(*Mutex).Unlock in /Users/tony/.bin/go18rc2/src/sync/mutex.go
    12.98s     12.98s (flat, cum)   100% of Total
         .          .    121:            return
         .          .    122:        }
         .          .    123:        // Grab the right to wake someone.
         .          .    124:        new = (old - 1<<mutexWaiterShift) | mutexWoken
         .          .    125:        if atomic.CompareAndSwapInt32(&m.state, old, new) {
    12.98s     12.98s    126:            runtime_Semrelease(&m.sema)
         .          .    127:            return
         .          .    128:        }
         .          .    129:        old = m.state
         .          .    130:    }
         .          .    131:}
(pprof) top10
1.29s of 1.29s total (  100%)
      flat  flat%   sum%        cum   cum%
     1.29s   100%   100%      1.29s   100%  sync.(*Mutex).Unlock
         0     0%   100%      1.29s   100%  main.main.func1
         0     0%   100%      1.29s   100%  runtime.goexit

go pprof的另外一个用法就是在go test时,mutexprofile同样支持这一点:

go test -mutexprofile=mutex.out
go tool pprof <test.binary> mutex.out

5、其他重要改动

Go 1.8标准库还有两个值得注意的改动,一个是:crypto/tls,另一个是database/sql。

HTTPS逐渐成为主流的今天,各个编程语言对HTTPS连接的底层加密协议- TLS协议支持的成熟度日益被人们所关注。Go 1.8给广大Gophers们带来了一个更为成熟、性能更好、更为安全的TLS实现,同时也增加了对一些TLS领域最新协议规范的支持。无论你是实现TLS Server端,还是Client端,都将从中获益。

Go 1.8在crypto/tls中提供了基于ChaCha20-Poly1305的cipher suite,其中ChaCha20是一种stream cipher算法;而Poly1305则是一种code authenticator算法。它们共同组成一个TLS suite。使用这个suite,将使得你的web service或站点具有更好的mobile浏览性能,这是因为传统的AES算法实现在没有硬件支持的情况下cost更多。因此,如果你在使用tls时没有指定cipher suite,那么Go 1.8会根据硬件支持情况(是否有AES的硬件支持),来决定是使用ChaCha20还是AES算法。除此之外,crypto/tls还实现了更为安全和高效的X25519密钥交换算法等。

Go 1.4以来,database/sql包的变化很小,但对于该包的feature需求却在与日俱增。终于在Go 1.8这个dev cycle中,govendor的作者Daniel TheophanesBrad Fitzpatrick的“指导”下,开始对database/sql进行“大规模”的改善。在Go 1.8中,借助于context.Context的帮助,database/sql增加了Cancelable Queries、SQL Database Type、Multiple Result Sets、Database ping、Named Parameters和Transaction Isolation等新Features。在GopherAcademy的Advent 2016系列文章中,我们可以看到Daniel Theophanes亲手撰写的文章,文章针对Go 1.8 database/sql包新增的features作了详细解释。

三、Go工具链(Go Toolchain)

在目前市面上的主流编程语言中,如果说Go的工具链在成熟度和完善度方面排第二,那没有语言敢称自己是第一吧^_^。Go 1.8在Go Toolchain上继续做着持续地改进,下面我们来逐一看看。

1、Plugins

Go在1.8版本中提供了对Plugin的初步支持,并且这种支持仅限于Linux。plugin这个术语在不同语言、不同情景上下文中有着不同的含义,那么什么是Go Plugin呢?

Go Plugin为Go程序提供了一种在运行时加载代码、执行代码以改变运行行为的能力,它实质上由两个部分组成:

  • go build -buildmode=plugin xx.go 构建xx.so plugin文件
  • 利用plugin包在运行时动态加载xx.so并执行xx.so中的代码

C程序员看到这里肯定会有似曾相识的赶脚,因为这和传统的动态共享库在概念上十分类似:

go build -buildmode=plugin xx.go 类似于 gcc -o xx.so -shared xx.c
go plugin包 类似于 linux上的dlopen/dlsym或windows上的LoadLibrary

我们来看一个例子!我们先来建立一个名为foo.so的go plugin:

//go18-examples/gotoolchain/plugins/foo.go

package main

import "fmt"

var V int
var v int

func init() {
        V = 17
        v = 23
        fmt.Println("init function in plugin foo")
}

func Foo(in string) string {
        return "Hello, " + in
}

func foo(in string) string {
        return "hello, " + in
}

通过go build命令将foo.go编译为foo.so:

# go build -buildmode=plugin foo.go
# ldd foo.so
    linux-vdso.so.1 =>  (0x00007ffe47f67000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9d06f4b000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9d06b82000)
    /lib64/ld-linux-x86-64.so.2 (0x000055c69cfcf000)

# nm foo.so|grep Foo
0000000000150010 t local.plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879.Foo
0000000000150010 T plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879.Foo
000000000036a0dc D type..namedata.Foo.

我们看到go plugin的.so文件就是一个标准的Linux动态共享库文件,我们可以通过nm命令查看.so中定义的各种符号。接下来,我们来load这个.so,并查找并调用相应符号:

//go18-examples/gotoolchain/plugins/main.go

package main

import (
        "fmt"
        "plugin"
        "time"
)

func init() {
        fmt.Println("init in main program")
}

func loadPlugin(i int) {
        fmt.Println("load plugin #", i)
        var err error
        fmt.Println("before opening the foo.so")

        p, err := plugin.Open("foo.so")
        if err != nil {
                fmt.Println("plugin Open error:", err)
                return
        }
        fmt.Println("after opening the foo.so")

        f, err := p.Lookup("Foo")
        if err != nil {
                fmt.Println("plugin Lookup symbol Foo error:", err)
        } else {
                fmt.Println(f.(func(string) string)("gophers"))
        }

        f, err = p.Lookup("foo")
        if err != nil {
                fmt.Println("plugin Lookup symbol foo error:", err)
        } else {
                fmt.Println(f.(func(string) string)("gophers"))
        }

        v, err := p.Lookup("V")
        if err != nil {
                fmt.Println("plugin Lookup symbol V error:", err)
        } else {
                fmt.Println(*v.(*int))
        }

        v, err = p.Lookup("v")
        if err != nil {
                fmt.Println("plugin Lookup symbol v error:", err)
        } else {
                fmt.Println(*v.(*int))
        }
        fmt.Println("load plugin #", i, "done")
}

func main() {
        var counter int = 1
        for {
                loadPlugin(counter)
                counter++
                time.Sleep(time.Second * 30)
        }
}

执行这个程序:

# go run main.go
init in main program
load plugin # 1
before opening the foo.so
init function in plugin foo
after opening the foo.so
Hello, gophers
plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
17
plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
load plugin # 1 done

load plugin # 2
before opening the foo.so
after opening the foo.so
Hello, gophers
plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
17
plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
load plugin # 2 done
... ...

我们来分析一下这个执行结果!

a) foo.go中的代码也包含在main package下,但只是当foo.so被第一次加载时,foo.go中的init函数才会被执行;
b) foo.go中的exported function和variable才能被Lookup到,如Foo、V;查找unexported的变量和函数符号将得到error信息,如:“symbol foo not found in plugin”;
c) Lookup返回的是plugin.Symbol类型的值,plugin.Symbol是一个指向plugin中变量或函数的指针;
d) foo.go中的init在后续重复加载中并不会被执行。

注意:plugin.Lookup是goroutine-safe的。

在golang-dev group上,有人曾问过:buildmode=c-shared和buildmode=plugin有何差别?Go team member给出的答案如下:

The difference is mainly on the program that loads the shared library.

For c-shared, we can't assume anything about the host, so the c-shared dynamic library must be self-contained, but for plugin, we know the host program will be a Go program built with the same runtime version, so the toolchain can omit at least the runtime package from the dynamic library, and possibly more if it's certain that some packages are linked into the host program. (This optimization hasn't be implemented yet, but we need the distinction to enable this kind of optimization in the future.)

2、默认的GOPATH

Go team在Go 1.8以及后续版本会更加注重”Go语言的亲民性”,即进一步降低Go的入门使用门槛,让大家更加Happy的使用Go。对于一个Go初学者来说,一上来就进行GOPATH的设置很可能让其感到有些迷惑,甚至有挫折感,就像建立Java开发环境需要设置JAVA_HOME和CLASSPATH一样。Gophers们期望能做到Go的安装即可用。因此Go 1.8就在这方面做出了改进:支持默认的GOPATH。

在Linux/Mac系下,默认的GOPATH为$HOME/go,在Windows下,GOPATH默认路径为:%USERPROFILE%/go。你可以通过下面命令查看到这一结果:

$ go env
GOARCH="amd64"
GOBIN="/home/tonybai/.bin/go18rc3/bin"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/tonybai/go"
GORACE=""
GOROOT="/home/tonybai/.bin/go18rc3"
GOTOOLDIR="/home/tonybai/.bin/go18rc3/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build313929093=/tmp/go-build -gno-record-gcc-switches"
CXX="g++"
CGO_ENABLED="1"
PKG_CONFIG="pkg-config"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"

BTW,在Linux/Mac下,默认的GOROOT为/usr/local/go,如果你的Go环境没有安装到这个路径下,在没有设置$GOROOT环境变量的情况下,当你执行go subcommand相关命令时,你会看到如下错误:

$go env
go: cannot find GOROOT directory: /usr/local/go

3、其他变化

Go 1.8删除了Go 1.7中增加的用于关闭ssa新后端的”-ssa=0” compiler flag,并且将ssa backend扩展到所有architecture中,对ssa后端也进一步做了优化。与此同时,为了将来进一步的性能优化打基础,Go 1.8还引入了一个新编译器前端,当然这对于普通Gopher的Go使用并没有什么影响。

Go 1.8还新增go bug子命令,该命令会自动使用默认浏览器打开new issue页面,并将采集到的issue提交者的系统信息填入issue模板,以帮助gopher提交符合要求的go issue,下面是go bug打开的issue page的图示:

img{512x368}

四、性能变化(Performance Improvement)

无论是Gotoolchain、还是runtime(包括GC)的性能,一直都是Go team重点关注的领域。本次Go 1.8依旧给广大Gophers们带来了性能提升方面的惊喜。

首先,Go SSA后端扩展到所有architecture和新编译器前端的引入,将会给除X86-64之外架构上运行的Go代码带来约20-30%的运行性能提升。对于x86-64,虽然Go 1.7就已经开启了SSA,但Go 1.8对SSA做了进一步优化,x86-64上的Go代码依旧可能会得到10%以内的性能提升。

其次,Go 1.8持续对Go compiler和linker做性能优化,和1.7相比,平均编译链接的性能提升幅度在15%左右。虽然依旧没有达到Go 1.4的性能水准。不过,优化依旧在持续进行中,目标的达成是可期的。

再次,GC在低延迟方面的优化给了我们最大的惊喜。在Go 1.8中,由于消除了GC的“stop-the-world stack re-scanning”,使得GC STW(stop-the-world)的时间通常低于100微秒,甚至经常低于10微秒。当然这或多或少是以牺牲“吞吐”作为代价的。因此在Go 1.9中,GC的改进将持续进行,会在吞吐和低延迟上做一个很好的平衡。

最后,defer的性能消耗在Go 1.8中下降了一半,与此下降幅度相同的还有通过cgo在go中调用C代码的性能消耗。

五、小结兼参考资料

Go 1.8的变化不仅仅是以上这些,更多变化以及详细的描述请参考下面参考资料中的“Go 1.8 Release Notes”:

以上demo中的代码在这里可以找到。

《Understanding and Using C Pointers》要点先睹为快

如果你问十个C程序员:你觉得C语言的核心是什么?这十个程序员都会回答:指针。

指针具备成为C语言核心的两个关键要素:强大争议

* 指针的强大源自于其天生与机器内存模型的适配。使用指针让代码紧凑,并可获得仅次于汇编代码的执行效率;使用指针可以让C程 序员毫不费力地尽情操纵着内存中的每个byte甚至是bit;使用指针可以为C程序员提供无与伦比的操作灵活性。总之,在C语言中指针几乎是无所 不能的代名词。得指针者得天下,没有指针,C语言将变得平庸。

* 成也指针,败也指针。指针的争议之处就在于其在赋予C程序员无比强大的Power的同时,也常常带来无穷的烦恼甚至灾祸,比如 内存问题、调试困难或因指针导致的程序崩溃等。就好比人类社会,做核心人物有争议是难免的,比如足球界有马拉多纳,跳水界有菲尔普斯,斯诺克界有 奥沙利文^_^。

好了,言归正传,我们回到C语言图书上来。目前市面上的C语言书籍,无论国内国外,无论经典还是山寨,基本都是百科大全型,将C语言讲的面面俱 到。比如最近的一本大而全的经典应当属《C Programming , A Modern Approach》,中文版书名为《C语言程序设计:现代方法》第2版。以至于发展到今天,C语言似乎也没啥可讲的了,新出的C语言书大多是与前辈们雷同 的作品。近两年来也有O'reilly出版的C语言书籍,比如:

*《Head First C
*《21st Century C – C Tips from the New School

前者是典型的Head First风格的C教程,后者则是另辟蹊径,结合C语言外延(构建、调试、打包、版本控制、面向对象与C、知名C语言开源库等)进行讲解。这两本书虽形式 有变化,但终究脱离不开百科大全型,针对C的核心-指针并未有较多的深入探讨。而市场上专门写指针的书也稀少的很(似乎鬼子国那边有一本,叫什么 《征服C指针》),唯一的一本书名与指针扯上关系的书《Pointers on C》(中文名“C和指针”)其实依旧是一本C语言大全。于是乎国外著名出版社O'Reilly今年5月出品了一本专门讲解C语言核心 – 指 针的书《Understanding and Using C Pointers》,以满足C程序员深入理解C语言核心并实现进阶的诉求。O'Reilly就是O'Reilly,总是能抓住C语言书籍方面的深度阅读需 求^_^。

《Understanding and Using C Pointers》是个小册子,拢共才200多页,但内容却全部是围绕C语言指针展开的,从最基本的指针声明与操作、C内存模型、动态内存分配,讲到指针 与数组、结构体、字符串的关系,再到最后指针的高级特性:强制转换、Strict Aliasing、线程共享、多态支持等,由浅入深的进行细致的剖析。其作者认为作为C语言核心的指针值得花200页篇幅去讲解,而且期望所有读者在读完 此书后能对C指针有个扎实的理解。总之,这本书对系统C程序员理解C语言的核心-指针是大有裨益的。在其中文版(已经由图灵出版社引进版权了)尚 未出版之前,这里带你先了解以下本书的要点:

第一章 简介

1、指针与内存

   【指针声明语法】
    int *pi;

   【理解复杂指针声明】
    方法:从后向前读,例子:

   const int *pci;

   pci is a variable                                   pci
   pci is a pointer variable                           *pci
   pci is a pointer variable to an integer             int *pci
   pci is a pointer variable to a constant integer     const int *pci

    【地址操作符】
     pi = &num;

    【输出指针值】
    通过%x、%o、%p输出(printf)指针的值,一般使用%p(%p输出结果不一定等同于%x,是与实现有关的)。例子如下:
     int num = 0;
     int *pi = &num;
     printf("Address of num: %d Value: %d\n",&num, num);
     printf("Address of pi: %d Value: %d\n",&pi, pi);

     Address of num: 4520836 Value: 0
     Address of pi: 4520824 Value: 4520836

    【通过间接访问操作符解引用指针】
      间接访问操作符*,使用例子如下:
     int num = 5;
     int *pi = &num;
     printf("%d\n",*pi); // Displays 5
     *pi = 200;
     printf("%d\n",num); // Displays 200

     【指向函数的指针】
        void (*foo)();  // 这个变量声明中的foo就是一个指向函数的指针

     【Null概念】

         null concept
             赋值为NULL的指针变量表示该指针不指向任何内存地址。

         null pointer constant
             null concept的具体支撑实现,其常量值可能是常量值0,也可能不是。依具体实现而定。

         NULL macro
             在许多标准库实现中,NULL定义如下:#define NULL ((void *)0),这也是我们对NULL的通常理解。当然这是依Compiler的具体实现而定的。如果编译 器使用非全0位模式实现了NULL,那该编译器就要保证在指针上下文中使用的NULL或0是null pointer。

         ASCII NUL
             一个全0的字节。

         null string
             一个不包含任何字符的空字符串。C字符串在最后都放置一个结尾0值。

         null statement
             只包含一个分号的空语句。

         指向void的指针
             指向void的指针被成为通用指针,可以用于引用任意类型的数据。它有两个属性:
                    – 指向void的指针与指向char类型的指针具有相同的内存表示与内存对齐约束。
                    – void指针永远不等于其他类型指针,两个赋值为NULL的void pointer是相等的。

             任何指针都可以被赋给一个void pointer,并且之后还可以被转换回其原来的类型。
             int num;
             int *pi = &num;                   
             void* pv = pi;
             pi = (int*) pv;

            
             void pointer用于数据指针,而不是函数指针。
             全局void pointer或static void pointer在程序启动时被初始化为NULL。

2、指针大小与类型
        在多数现代平台上,指针的大小都是相同的,与其类型无关。指向char的指针与指向结构体的指针大小相同。
        指向函数的指针可能与指向数据类型的指针大小有差异,这要依具体实现而定。
     
     【内存模型】
             在不同机器和编译器下,C语言原生类型的大小是不同的。
             描述不同数据模型的一般记法:I In L Ln LL LLn P Pn,例如LP64、ILP64、LP32等。
 
     【预定义的指针相关类型】
            size_t 用于表示对象的大小的一个安全类型。
            ptrdiff_t 用于处理指针运算
            intptr_t和uintptr_t 用于存 储指针地址

       int num;
       intptr_t *pi = &num;

3、指针操作符

     【指针运算】
       pointer + integer
           指针实际移动的字节数 = integer + sizeof(integer_type)
           void* pointer的指针运算操作行为是未定义的,依赖Compiler的具体实现。

       pointer – integer
           指针实际移动的字节树 = integer – sizeof(integer_type)。

       pointer1 – pointer2
           两个指针所指地址间的差值,常用于判断数组中元素的先后次序。

       比较pointers

     【指针比较】
              指针可以使用标准的比较操作符(> and <)进行比较,可用来判断数组中元素的先后次序。

4、指针的通常用法
    
     【多级间接寻址】
              双指针(double pointer) – 指向指针的指针。

            char *titles[] = {"A Tale of Two Cities",
                        "Wuthering Heights","Don Quixote",
                        "Odyssey","Moby-Dick","Hamlet",
                        "Gulliver's Travels"};
      char **bestBooks[3];
      bestBooks[0] = &titles[0];
      bestBooks[1] = &titles[3];
      bestBooks[2] = &titles[5];

          
            间接寻址的级数并没有限制,但过多的级数会让人难以理解。
  
    【常量和指针】

            指向常量的指针
         const int limit = 500;
         const int *pci = &limit;

                  *pci = 600;/* Error, 我们不能解引用一个常量指针并修改其所指的内存值 */
                 
                 const int *pci <=> int const *pci;

            指向非常量的常量指针
         int num;
         int *const cpi = &num;

                  *cpi = 25; /* 可以解引用常量指针并修改其所指的内存的值 */
         int limit;
         cpi = &limit; /* Error,我们不能为常量指针重新赋新值 */

         const int limit1 = 300;
         int *const cpi1 = &limit1; /* Warning: 指向非常量的常量指针被用常量 的地址初始化了 */
 
      指向常量的常量指针    
         const int limit = 300;
         const int *const cpci = &limit;
/* 声明后,我们不能通过cpci修改limit,也不能为cpci重新赋值 */

            指向“指向常量的常量指针”的指针
         const int limit = 300;
         const int *const cpci = &limit;
         const int *const *pcpci = &cpci;

第二章 C语言动态内存管理

在运行时通过函数手工从heap分配和释放内存的过程称为动态内存管理。

1、动态内存分配
    【使用malloc函数】
      int *pi = (int*) malloc(sizeof(int));
      *pi = 5;
      free(pi);

    【内存泄漏】
            – 丢失了内存地址
            – 没有调用free函数释放内存

 2、动态分配内存函数
      malloc、realloc、calloc、free
      是否对malloc出的内存起始地址进行强制转型
             int *p = (int*)malloc(4);
             void *pointer可以转换为任意类型指针,没有强制转型也可以。
             但显式的强制转型可以通过代码看出意图,并且与C++编译器(包括早期C编译器)兼容
                       
      你不能用内存分配函数分配的内存去初始化全局或Static变量。
      alloca函数用于在栈上动态分配内存,函数结束时,这块内存自动释放;但alloca不是标准C库函数,移植性差。
      C99支持可变长度数组(VLA),数组声明时的元素个数可以是运行时才能确定值的变量,但数组size一旦在运行时被确定,数组大小就无法再做改变:
       void compute(int size) {
           char* buffer[size];
           …
       }
         

 3、悬挂指针
     被free后依然引用原先内存地址的指针,称为dangling pointer。
     悬挂指针可能导致如下问题:
            – 如果访问其引用的内存,将导致不可预期的结果
            – 如果内存不可访问了,将导致段错误
            – 存在潜在的安全风险。

     悬挂指针引起的问题调试起来十分困难,以下几种方法用于避免发生悬挂指针问题或快速查找悬挂指针问题:
            – free后,设置指针为NULL;
            – 编写一个替代free的函数;
            – 用特定值填充free的内存块,便于快速定位dangling pointer问题
            – 使用第三方工具检查dangling pointer问题

第三章 指针与函数

当与函数一起使用时,指针有两个方面发挥重要作用:
   – 当指针以参数形式传递给函数时,允许函数修改指针所指内存区域的值,并且这种传递方式更加高效;
   – 声明函数指针时,函数的名字被求值为函数的地址。
 
1、程序栈和堆

    【程序栈】
      栈和堆共享一块内存区域。栈在这块区域的低地址部分,堆在高地址部分。
      程序栈用于存放栈帧(stack frame),栈帧中存放的是函数的参数与local变量。
      栈增长方向:向上;堆的增长方向:向下。

    【栈帧的组成】
     一个栈帧包含如下几个元素:
           – 返回地址
           – 本地变量
           – 函数参数
           – 栈指针(Stack pointer)和栈帧指针(base pointer or frame pointer)

     Stack pointer和frame pointer用于运行时系统对栈的管理。前者总是指向栈的顶端;后者指向栈帧内的某个地址,比如函数的返回地址;frame pointer辅助程序访问栈帧内的元素。

     栈帧的创建,见下面例子:
        float average(int *arr, int size) {
            int sum;
            printf("arr: %p\n",&arr);
            printf("size: %p\n",&size);
            printf("sum: %p\n",&sum);

            for(int i=0; i<size; i++) {
                sum += arr[i];
            }
            return (sum * 1.0f) / size;
    }

      average的栈帧中沿着栈“向上”的方向,依次推入的是:
            – 参数 size、arr (与声明的顺序恰好相反)
            – 函数average调用的返回地址
            – 本地变量sum(如果有多个本地变量,推入栈的顺序也与变量声明顺序相反)

      每个线程通常都在自己的栈中创建栈帧。

2、指针作为参数和返回值

      C语言的参数是“按值传递”的,包括指针本身,函数内使用的是参数的copy。
      在处理大数据结构时,将指针作为参数传递给函数或作为返回值会使得程序执行起来更加高效(只是copy一个指针大小的数据,而不是指针所指向的数据对象大 小)。
      另外一个以指针作为函数参数的目的是希望在函数内部对数据进行修改。
      当传递一个指向常量的指针给函数时,其意图为不希望函数内部对指针所指的数据进行修改。例如void passingAddressOfConstants(const int* num1, int* num2),不希望num1所指数据被修改。
      将指针作为返回值返回时,应避免以下几个常见问题:
            – 返回未初始化的指针
            – 返回指向非法地址的指针
            – 返回指向函数本地变量的指针
            – 返回指针后,没有释放其所指的内存块
 
      如果函数要修改的不是参数中指针所指的数据,而是指针本身所指的内存地址,那么应以double pointer形式作为函数参数:

        void allocateArray(int **arr, int size, int value) {
            *arr = (int*)malloc(size * sizeof(int));
            if(*arr != NULL) {
                for(int i=0; i<size; i++) {
                    *(*arr+i) = value;
                }
            }
        }

      int *vector = NULL;
      allocateArray(&vector,5,45);

3、函数指针
      函数指针就是存放函数地址的指针。 
      使用函数指针可能导致程序运行变慢(可能感知不到),因为函数指针的使用可能导致CPU无法正确的运用分支预测,导致CPU流水线中断。

    【声明函数指针】

      函数指针的声明看起来像函数原型,比如:void (*foo)(int i);
      程序员应该确保通过函数指针调用函数的正确使用,因为C编译器不会检查是否正确的为函数指针传入正确的参数(类型、顺序以及个数)。
      通常我们用typedef声明一个函数指针类型,比如:
          typedef void (*funcptr)(int i);
          funcptr fp = foo;

    【函数指针强制转型】
     
      一个类型的函数指针可以被强制转为另外一种类型函数指针。
      转型后的指针 == 转型前的指针
     
        typedef int (*fptrToSingleInt)(int);
        typedef int (*fptrToTwoInts)(int,int);
        int add(int, int);
        fptrToTwoInts fptrFirst = add;
        fptrToSingleInt fptrSecond = (fptrToSingleInt)fptrFirst;
        fptrFirst = (fptrToTwoInts)fptrSecond;
        printf("%d\n",fptrFirst(5,6));

      在函数指针间转换,很可能导致函数调用失败。

第四章 指针与数组

1、数组概述

数组与指针记法关系紧密,在特定上下文中可以相互替换。
数组内部表示中并没有数组长度信息。
 
  【一维数组】
    int vector[5];

    一维数组是一个线性结构。数组下标起始于0,终止于(元素个数-1)。

  【二维数组】
    int matrix[2][3] = {{1,2,3},{4,5,6}};

    二维数组使用行和列标识数组元素。这类数组需要被映射到一个一维地址空间中。
    在C中,二维数组的第一行放在内存的最开始处,接下来是第二行,…,直到最后一行,这就是所谓的“行主序”。

  【多维数组】
    int arr3d[3][2][4] = {
        {{1, 2, 3, 4}, {5, 6, 7, 8}},
        {{9, 10, 11, 12}, {13, 14, 15, 16}},
        {{17, 18, 19, 20}, {21, 22, 23, 24}}
  };

    二维以上的维数的数组称为多维数组,其元素内存分配依旧遵守二维数组那种映射方式。

2、指针记法(notation)与数组

    指针记法与数组记法在一定场合可以互换,但两者并不完全相同。
    数组名单独使用时,我们得到的是数组的地址;该地址等同于数组内第一个元素的地址。

  int vector[5] = {1, 2, 3, 4, 5};
  int *pv = vector;
  int (*pv)[5] = &vector;

    vector与&vector不同,前者返回指向一个整型变量的指针(int *),后者返回一个指向整个数组的指针(int[5] *)。
  pv[i] <=> *(pv + i)
  *(pv + i) <=> *(vector + i)

  【指针与数组间的不同】

    int vector[5] = {1, 2, 3, 4, 5};
  int *pv = vector;

    sizeof(vector) = 20 != sizeof(pv)

    pv是lvalue,可以被修改而指向不同的地址;比如pv = pv + 1
    而vector不能被修改。vector = vector + 1这个表达式是错误的,不过pv = vector + 1是ok的。

  【使用malloc创建一维数组】
    int *pv = (int*) malloc(5 * sizeof(int));
    pv[3] = 10;

     可使用realloc改变malloc创建的数组的大小。
    
3、传递一维数组
    两种记法:数组记法和指针记法,分别如下:
    void displayArray(int arr[], int size);
    void displayArray(int* arr, int size);

    无论哪种,displayArray函数体内int arr[]或int *arr都将以int *arr方式使用,即数组名退化为指针,sizeof(arr) = 指针长度,而不是数组总长度。

   【一维指针数组】
   
    int* arr[5];
    for(int i=0; i<5; i++) {
        arr[i] = (int*)malloc(sizeof(int));
        *arr[i] = i;
    }

   【指针与多维数组】
         多维数组可以看成是由子数组组成的,就好比二维数组的每行都可以看成是一个一维数组。
         int matrix[2][5] = {{1,2,3,4,5},{6,7,8,9,10}};
         int (*pmatrix)[5] = matrix;

4、传递多维数组

   void display2DArray(int arr[][5], int rows);<=>
   void display2DArray(int (*arr)[5], int rows);

      上面两个版本是等价的。两个版本都指定了列的值,因为编译器需要知道每行的元素个数。

     注意第二个版本不等价于void display2DArray(int *arr[5], int rows)

      在void display2DArrayUnknownSize(int *arr, int rows, int cols)的 函数体实现中,你不能使用arr[i][j],因为arr并未被声明为二维数组。

5、动态分配二维数组

     【采用不连续的内存分配方式】

    int rows = 2;
    int columns = 5;
    int **matrix = (int **) malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int *) malloc(columns * sizeof(int));
    }

     【采用连续内存分配的方式】

    int rows = 2;
    int columns = 5;
    int **matrix = (int **) malloc(rows * sizeof(int *));
    matrix[0] = (int *) malloc(rows * columns * sizeof(int));
    for (int i = 1; i < rows; i++)
        matrix[i] = matrix[0] + i * columns;

       or

    int *matrix = (int *)malloc(rows * columns * sizeof(int));

第五章 指针与字符串

1、字符串基础

     字符串:以ASCII结尾'\0'字符结尾的字符序列。
     分类:字节字符串(byte string) – char类型字符序列
               宽字符串(wide string) – wchar_t 类型字符序列(每个字符16bit or 32bit,依编译器实现而定)
     字符串声明:char header[32] or char *header

    【字符串字面量池(String literal pool)】
      字符串字面量定义后将被放在字面量池中。这块内存区域存放的是组成字符串的字符序列。当一个字面量多次使用时,通常在字面量池中只存储一份该字符串。这将 降低程序的内存使用量。并且通常情况下,字面量池中的字符串是immutable的。

      大多数编译器都提供了编译开关,用于指示是否关闭字符串字面量池,比如Gcc的-fwritable-strings。

     【字符串初始化】、
            char *header = "Media Player";

      or
      char header[] = "Media Player";

      or
      char header[13];
      strcpy(header,"Media Player");

      or
      char *header = (char*) malloc(strlen("Media Player")+1);
      strcpy(header,"Media Player");

2、标准字符串操作

      比较字符串:strcmp
      拷贝字符串:strcpy
      连接字符串:strcat

3、传递字符串

      传递简单字符串:
      size_t stringLength(char* string) ;
      size_t stringLength(char string[]);

      传递字符串常量:
      size_t stringLength(const char* string);

4、返回字符串

         返回一个字面量:return "Boston Processing Center"
         动态分配的内存:
         char* spaces = (char*) malloc(number + 1);
         … …
         return spaces;

         返回local字符串的地址是危险的。

5、函数指针与字符串

第六章 指针与结构体

1、简介

    【如何为结构体分配内存】      
      结构体的大小往往大于该结构体所有字段大小之和,因为有数据对齐的需求,导致编译器在进行结构体内存分配时进行了padding操作。特定数据类型具有一 定的对齐要求,比如short类型的字段要求其地址能被2整除,而integer类型的字段要求其起始地址能被4整除。

      考虑到这些多余分配的内存,你应该谨慎对待如下操作:
      – 小心使用指针运算
      – 结构体数组的元素间有多余内存空间

    【结构体内存释放】
      为结构体分配内存时,运行时不会自动为结构体内的指针字段分配内存;同理,释放结构体内存时,运行时也不会自动释放结构体内指针字段所指向的内存。

    【避免malloc和free的额外开销】
     malloc和free多次重复调用时,会给程序带来额外的开销。一个解决方法就是自己维护一份已分配的结构。需要时,从这个池里取出一份,释放时,直接 返回给池中。如果没有可用的结构时,才考虑新创建一个。

2、使用指针支持数据结构

无论是简单还是复杂的数据结构,指针都提供了更加灵活的支持,包括链表、队列、栈以及树等。

第七章 安全问题以及不当使用指针
   
深入理解指针以及其正确的使用方法有利于开发出安全可信赖的应用。

OS引入了一些提升安全的技术,比如 Address Space Layout Randomization和Data Execution Prevention。

【Address Space Layout Randomization (ASLR) ,地址空间布局随机化】
  ASLR技术使得程序的数据区域随机布局,数据区域包括:代码、栈、堆。随机的放置这些区域让代码攻击行为很难精确预测特定代码的内存地址并使用它们。

【Data Execution Prevention(DEP),数据执行保护】
  DEP技术会阻止执行非执行数据区域中的代码。在一些攻击中,一些非执行数据区域中的数据被恶意覆写为代码,执行权也被转移到那里。但有了DEP后,这些 恶意代码将无法执行。

1、指针声明与初始化

   【不正确的指针声明】
     int* ptr1, ptr2;
      ptr1是指针,但ptr2只是一个整型变量。

      正确声明方法:int *ptr1, *ptr2; /* 更好的做法是每行仅声明一个变量 */

      下面做法存在同样的问题:
   #define PINT int*
   PINT ptr1, ptr2;

      用typedef就没有问题了:
   typedef int* PINT;
   PINT ptr1, ptr2;

   【使用指针前未初始化】
     使用前未做初始化的指针,常称作野指针(wild pointer):

   int *pi;
    …
   printf(“%d\n”,*pi);

    【处理未初始化的指针】
      指针脸上没有写自己是否做过初始化^_^。通常有三种方法用于对付未初始化的指针:
        – 总是将指针初始化为NULL;
        – 使用assert函数
        – 使用第三方工具
       
2、指针使用问题
   
      缓冲区溢出(Buffer overflow)可能由以下原因导致:
      – 访问数组元素的时候没有检查下标值
      – 做数组指针相关运算时不够谨慎
      – 用gets之类的函数从标准输入读取字符串
      – 使用strcpy和strcat不当

     【测试NULL】
       调用malloc后,总是检查返回值是否为NULL。

     【误用解引用操作符】
       int num;
       int *pi;
       *pi = &num

     【悬挂指针】

     【访问数组越界】

       char firstName[8] = "1234567";
       char middleName[8] = "1234567";
       char lastName[8] = "1234567";
       middleName[-2] = 'X';
       middleName[0] = 'X';
       middleName[10] = 'X';

     【错误计算数组大小】
         当将数组作为参数传递给函数时,务必将函数的Size一并传入,这个Size信息将避免数组访问越界。

     【误用sizeof操作符】
        int buffer[20];
        int *pbuffer = buffer;
        for(int i=0; i<sizeof(buffer); i++) {
            *(pbuffer++) = 0;
        }

         sizeof(buffer)=>sizeof(buffer)/sizeof(buffer[0]);

      【总是匹配指针类型】
      【有界指针(bounded pointer)】
      【字符串安全问题】
        对strcpy和strcat使用不当,会导致缓冲区溢出。
        在C11标准中加入了strcat_s和strcpy_s函数,如果发生缓冲区溢出,它们会返回错误。

      【函数指针问题】
       不要将函数赋值给签名不同的函数指针,这很可能将导致未定义行为发生。
      
3、内存释放问题
      【两次free】
      【清除敏感数据】
         一个良好的实践是覆写哪些不再需要的敏感数据。

        char *name = (char*)malloc(…);
        …
        memset(name,0,sizeof(name));
        free(name);

4、使用静态分析工具

      比如Gcc -Wall等。

第八章  其他零碎的知识点

1、指针转型
      指针转型有几个原因:
      – 访问特定目的的地址
      – 分配一个地址代表一个端口
      – 决定机器的endianess

    【访问特定的地址】
      #define VIDEO_BASE 0xB8000
      int *video = (int *) VIDEO_BASE;
      *video = 'A';

    【访问一个端口】
      #define PORT 0xB0000000
      unsigned int volatile * const port = (unsigned int *) PORT;
      *port = 0x0BF4; // write to the port
      value = *port; // read from the port

    【判断机器的endianess】
      int num = 0×12345678;
      char* pc = (char*) &num;
      for (int i = 0; i < 4; i++) {
          printf("%p: %02x \n", pc, (unsigned char) *pc++);
      }

2、Aliasing、Strict Aliasing和restrict关键字

两个指针同时指向一块相同的内存地址,这两个指针被称为aliasing。

     int num = 5;
     int* p1 = &num;
     int* p2 = &num;

aliasing的使用对编译器生成的代码强加了限制。
如果两个指针引用相同位置,每个指针都可以修改这块地址。当编译器生成读写这块内存的代码时,不总是可以通过将值存储在寄存器中这种办法来优化代 码。对每次引用,将强制使用机器级别的低效load和store操作。

Strict Aliasing:另外一种形式的aliasing。strict aliasing不允许不同类型的指针指向同一块内存区域。下面代码:一个指向整型的指针alias了一个指向float类型的指针了,这违反了Strict Aliasing的规则。

    float number = 3.25f;
    unsigned int *ptrValue = (unsigned int *)&number;
    unsigned int result = (*ptrValue & 0×80000000) == 0;

如果仅仅是符号标志和修饰符不同,是不会影响strict aliasing的,下面的语句是符合Strict aliasing规则的:

    int num;
    const int *ptr1 = &num;
    int *ptr2 = &num;
    int volatile ptr3 = &num;

有些场合,相同数据的不同表示是很有用处的,下面一些方法可以避免与Strict aliasing规则冲突:
        – 使用Union: 多个数据类型的联合体可以规避strict aliasing
        – 关闭strict aliasing :利用编译器提供的开关将strict aliasing关闭(不建议这么做哦),
                     比如Gcc提供的一些开关:
                 -fno-strict-aliasing 关闭strict aliasing
                 -fstrict-aliasing 打开strict aliasing
                 -Wstrict-aliasing 针对strict aliasing相关问题给出警告

        – 使用char pointer:char pointer可以alias任何对象。

       【使用Union实现一个值的多种方式表示】
   
        typedef union _conversion {
            float fNum;
            unsigned int uiNum;
        } Conversion;
        int isPositive1(float number) {
            Conversion conversion = { .fNum =number};
            return (conversion.uiNum & 0×80000000) == 0;
        }

           由于没有指针,所以不存在违反Strict aliasing的问题。

       【Strict Aliasing】
         编译器假设多个不同类型的指针不会引用到同一个数据对象,这样在strict aliasing的规则下,编译器才能够实施一些优化。如果假设不成立,那很可能发生意料之外的结果。

         即使是两个拥有相同字段,但名字不同的结构体,其对应的指针也不能引用同一个对象。但通过typedef结构体类型指针与原类型指针可以引用同一个数据对象。

         typedef struct _person {
            char* firstName;
            char* lastName;
            unsigned int age;
        } Person;
        typedef Person Employee;
        Person* person;
        Employee* employee;

       【使用restrict关键字】
         使用restrict关键字,意即告诉编译器这个指针没有被alias,这样编译器将可以进行优化,生成更为高效的代码。通常的优化方法是缓存这个指针。
         不过即便使用了restrict关键字,对编译器来说也只是一个建议,编译器可自行选择是否进行优化。
         建议新代码中都要使用restrict关键字。

        void add(int size, double * restrict arr1, const double * restrict arr2) {
            for (int i = 0; i < size; i++) {
                arr1[i] += arr2[i];
            }
        }

        double vector1[] = {1.1, 2.2, 3.3, 4.4};
        double vector2[] = {1.1, 2.2, 3.3, 4.4};
        add(4,vector1,vector2);

         以上是add函数的正确用法。

        double vector1[] = {1.1, 2.2, 3.3, 4.4};
        double *vector3 = vector1;
        add(4,vector1,vector3);
        add(4,vector1,vector1);

        这个例子中vector3与vector1指向同一份数据,也许add可以正常工作,但这个函数的调用结果并不那么可靠。

        标准C库中有多个函数使用了restrict关键字,比如void *memcpy(void * restrict s1, const void * restrict s2, size_t n)等。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats