c++中的extern “c” {}

讲的很清晰,透彻,特此存档 原文:[C++项目中的extern "C" {}](http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html)
以下正文:

引言

在用C++的项目源码中,经常会不可避免的会看到下面的代码:

#ifdef __cplusplus
extern "C" {
#endif

/*...*/

#ifdef __cplusplus
}
#endif

它到底有什么用呢,你知道吗?而且这样的问题经常会出现在面试or笔试中。下面我就从以下几个方面来介绍它:

  • 1、#ifdef _cplusplus/#endif _cplusplus及发散
  • 2、extern "C"
    • 2.1、extern关键字
    • 2.2、"C"
    • 2.3、小结extern "C"
  • 3、C和C++互相调用
    • 3.1、C++的编译和连接
    • 3.2、C的编译和连接
    • 3.3、C++中调用C的代码
    • 3.4、C中调用C++的代码
  • 4、C和C++混合调用特别之处函数指针

1、#ifdef _cplusplus/#endif _cplusplus及发散

在介绍extern "C"之前,我们来看下#ifdef _cplusplus/#endif _cplusplus的作用。很明显#ifdef/#endif、#ifndef/#endif用于条件编译,#ifdef _cplusplus/#endif _cplusplus——表示如果定义了宏_cplusplus,就执行#ifdef/#endif之间的语句,否则就不执行。

在这里为什么需要#ifdef _cplusplus/#endif _cplusplus呢?因为C语言中不支持extern "C"声明,如果你明白extern "C"的作用就知道在C中也没有必要这样做,这就是条件编译的作用!在.c文件中包含了extern "C"时会出现编译时错误。

既然说到了条件编译,我就介绍它的一个重要应用——避免重复包含头文件。还记得腾讯笔试就考过这个题目,给出类似下面的代码(下面是我最近在研究的一个开源web服务器——Mongoose的头文件mongoose.h中的一段代码):

#ifndef MONGOOSE_HEADER_INCLUDED
#define    MONGOOSE_HEADER_INCLUDED

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

/*.................................
 * do something here
 *.................................
 */

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* MONGOOSE_HEADER_INCLUDED */

然后叫你说明上面宏#ifndef/#endif的作用?为了解释一个问题,我们先来看两个事实:

  • 这个头文件mongoose.h可能在项目中被多个源文件包含(#include "mongoose.h"),而对于一个大型项目来说,这些冗余可能导致错误,因为一个头文件包含类定义或inline函数,在一个源文件中mongoose.h可能会被#include两次(如,a.h头文件包含了mongoose.h,而在b.c文件中#include a.h和mongoose.h)——这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。
  • 从逻辑观点和减少编译时间上,都要求去除这些冗余。然而让程序员去分析和去掉这些冗余,不仅枯燥且不太实际,最重要的是有时候又需要这种冗余来保证各个模块的独立

为了解决这个问题,上面代码中的

#ifndef MONGOOSE_HEADER_INCLUDED
#define    MONGOOSE_HEADER_INCLUDED
/*……………………………*/
#endif /* MONGOOSE_HEADER_INCLUDED */

就起作用了。如果定义了MONGOOSE_HEADER_INCLUDED,#ifndef/#endif之间的内容就被忽略掉。因此,编译时第一次看到mongoose.h头文件,它的内容会被读取且给定MONGOOSE_HEADER_INCLUDED一个值。之后再次看到mongoose.h头文件时,MONGOOSE_HEADER_INCLUDED就已经定义了,mongoose.h的内容就不会再次被读取了。

2、extern "C"

首先从字面上分析extern "C",它由两部分组成——extern关键字、"C"。下面我就从这两个方面来解读extern "C"的含义。

2.1、extern关键字

在一个项目中必须保证函数、变量、枚举等在所有的源文件中保持一致,除非你指定定义为局部的。首先来一个例子:

//file1.c:
    int x=1;
    int f(){do something here}
//file2.c:
    extern int x;
    int f();
    void g(){x=f();}

在file2.c中g()使用的x和f()是定义在file1.c中的。extern关键字表明file2.c中x,仅仅是一个变量的声明,其并不是在定义变量x,并未为x分配内存空间。变量x在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。但是可以声明多次,且声明必须保证类型一致,如:

//file1.c:
    int x=1;
    int b=1;
    extern c;
//file2.c:
    int x;// x equals to default of int type 0
    int f();
    extern double b;
    extern int c;

在这段代码中存在着这样的三个错误:

  1. x被定义了两次
  2. b两次被声明为不同的类型
  3. c被声明了两次,但却没有定义

回到extern关键字,extern是C/C++语言中表明函数全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

与extern对应的关键字是 static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

2.2、"C"

典型的,一个C++程序包含其它语言编写的部分代码。类似的,C++编写的代码片段可能被使用在其它语言编写的代码中。不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。

为了使它们遵守统一规则,可以使用extern指定一个编译和连接规约。例如,声明C和C++标准库函数strcyp(),并指定它应该根据C的编译和连接规约来链接:

extern "C" char* strcpy(char*,const char*);

注意它与下面的声明的不同之处:

extern char* strcpy(char*,const char*);

下面的这个声明仅表示在连接的时候调用strcpy()。

extern "C"指令非常有用,因为C和C++的近亲关系。注意:extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。C表示符合C语言的编译和连接规约的任何语言,如Fortran、assembler等。

还有要说明的是,extern "C"指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了extern "C",仍然要遵守C++的类型检测、参数转换规则。

再看下面的一个例子,为了声明一个变量而不是定义一个变量,你必须在声明时指定extern关键字,但是当你又加上了"C",它不会改变语义,但是会改变它的编译和连接方式。

如果你有很多语言要加上extern "C",你可以将它们放到extern "C"{ }中。

2.3、小结extern "C"

通过上面两节的分析,我们知道extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言)

3、C和C++互相调用

我们既然知道extern "C"是实现的类C和C++的混合编程。下面我们就分别介绍如何在C++中调用C的代码、C中调用C++的代码。首先要明白C和C++互相调用,你得知道它们之间的编译和连接差异,及如何利用extern "C"来实现相互调用。

3.1、C++的编译和连接

C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:

void print(int i);
void print(char c);
void print(float f);
void print(char* s);

编译为:

_print_int
_print_char
_print_float
_pirnt_string

这样的函数名,来唯一标识每个函数。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用print(3)时,它会去查找_print_int(3)这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。

C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。

3.2、C的编译和连接

C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的print(3)时,它会去找_print_int(3)。因此extern "C"的作用就体现出来了。

3.3、C++中调用C的代码

假设一个C的头文件cHeader.h中包含一个函数print(int i),为了在C++中能够调用它,必须要加上extern关键字(原因在extern关键字那节已经介绍)。它的代码如下:

#ifndef C_HEADER
#define C_HEADER

extern void print(int i);

#endif C_HEADER

相对应的实现文件为cHeader.c的代码为:

#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
    printf("cHeader %d\n",i);
}

现在C++的代码文件C++.cpp中引用C中的print(int i)函数:

extern "C"{
#include "cHeader.h"
}

int main(int argc,char** argv)
{
    print(3);
    return 0;
}

执行程序输出:

image 

3.4、C中调用C++的代码

现在换成在C中调用C++的代码,这与在C++中调用C的代码有所不同。如下在cppHeader.h头文件中定义了下面的代码:

#ifndef CPP_HEADER
#define CPP_HEADER

extern "C" void print(int i);

#endif CPP_HEADER

相应的实现文件cppHeader.cpp文件中代码如下:

#include "cppHeader.h"

#include <iostream>
using namespace std;
void print(int i)
{
    cout<<"cppHeader "<<i<<endl;
}

在C的代码文件c.c中调用print函数:

extern void print(int i);
int main(int argc,char** argv)
{
    print(3);
    return 0;
}

注意在C的代码文件中直接#include "cppHeader.h"头文件,编译出错。而且如果不加extern int print(int i)编译也会出错。

4、C和C++混合调用特别之处函数指针

当我们C和C++混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一中语言定义的函数。如果C和C++共享同一中编译和连接、函数调用机制,这样做是可以的。然而,这样的通用机制,通常不然假定它存在,因此我们必须小心地确保函数以期望的方式调用。

而且当指定一个函数指针的编译和连接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:

typedef int (*FT) (const void* ,const void*);//style of C++

extern "C"{
    typedef int (*CFT) (const void*,const void*);//style of C
    void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
}

void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C

//style of C
extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);

int compare(const void*,const void*);//style of C++
extern "C" ccomp(const void*,const void*);//style of C

void f(char* v,int sz)
{
    //error,as qsort is style of C
    //but compare is style of C++
    qsort(v,sz,1,&compare);
    qsort(v,sz,1,&ccomp);//ok
    
    isort(v,sz,1,&compare);//ok
    //error,as isort is style of C++
    //but ccomp is style of C
    isort(v,sz,1,&ccopm);
}

注意:typedef int (*FT) (const void* ,const void*),表示定义了一个函数指针的别名FT,这种函数指针指向的函数有这样的特征:返回值为int型、有两个参数,参数类型可以为任意类型的指针(因为为void*)。

最典型的函数指针的别名的例子是,信号处理函数signal,它的定义如下:

typedef void (*HANDLER)(int);
HANDLER signal(int ,HANDLER);

上面的代码定义了信函处理函数signal,它的返回值类型为HANDLER,有两个参数分别为int、HANDLER。 这样避免了要这样定义signal函数:

void (*signal (int ,void(*)(int) ))(int)

比较之后可以明显的体会到typedef的好处。

Go中struct{}{}的解读

新人刚开始看到go中的struct{}{}作为interface的返回值时,一定很费解,怎么理解呢?看下面的例子
关于空接口,我们有如下的用法:

var v1 interface{} = 1      // 将int类型赋值给interface{}
var v2 interface{} = "abc"    // 将string类型赋值给interface{}
var v3 interface{} = &v2    // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}

看到4,5行的用法了嘛,其实 struct{}{}只是一个空的特例而已。也就是一个空的struct实例对象而已。

Go编程tip-4

从Panic中恢复

recover()的调用仅当它在defer函数中被直接调用时才有效。在defer中和直接调用这两个是必要条件

在Slice, Array, and Map "range"语句中更新引用元素的值

在“range”语句中生成的数据的值是真实集合元素的拷贝。它们不是原有元素的引用。这意味着更新这些值将不会修改原来的数据。同时也意味着使用这些值的地址将不会得到原有数据的指针。

如果你需要更新原有集合中的数据,使用索引操作符来获得数据。
package main

import "fmt"

func main() {
    data := []int{1, 2, 3}
    for i, _ := range data {
        data[i] *= 10
    }

    fmt.Println("data:", data) //prints data: [10 20 30]
}

如果你的集合保存的是指针,那规则会稍有不同。如果要更新原有记录指向的数据,你依然需要使用索引操作,但你可以使用for range语句中的第二个值来更新存储在目标位置的数据。
package main

import "fmt"

func main() {
    data := []*struct{ num int }{{1}, {2}, {3}}

    for _, v := range data {
        v.num *= 10
    }

    fmt.Println(data[0], data[1], data[2]) //prints &{10} &{20} &{30}
}

slice中的隐藏数据

因为slice是一个指针引用,所以如果引用原有数据的一小部分,实际指向的还是原来的整个内存对象。

对slice截断对象的append操作

因为slice很像指针,整个操作很容易产生“越界覆盖”

类型声明和方法

当你通过把一个现有(非interface)的类型定义为一个新的类型时,新的类型不会继承现有类型的方法。
Fails:
package main

import "sync"

type myMutex sync.Mutex

func main() {
    var mtx myMutex
    mtx.Lock()   //error
    mtx.Unlock() //error
}

如果你确实需要原有类型的方法,你可以定义一个新的struct类型,用匿名方式把原有类型嵌入其中。
package main

import "sync"

type myLocker struct {
    sync.Mutex
}

func main() {
    var lock myLocker
    lock.Lock()   //ok
    lock.Unlock() //ok
}

defer的函数什么时候执行?

被defer的调用会在包含的函数的末尾执行,而不是包含代码块的末尾。对于Go新手而言,一个很常犯的错误就是无法区分被defer的代码执行规则和变量作用规则。如果你有一个长时运行的函数,而函数内有一个for循环试图在每次迭代时都defer资源清理调用,那就会出现问题。
解决办法就是将相关的逻辑封装成内部匿名函数,这样在函数结束时,就会调用defer指定的函数

更新map的值

如果你有一个struct值的map,你无法更新单个的struct值。这个操作无效是因为map元素是无法取址的。
但是slice元素是可以取址的。

栈和堆变量

你并不总是知道变量是分配到栈还是堆上。在C++中,使用new创建的变量总是在堆上。在Go中,即使是使用new()或者make()函数来分配,变量的位置还是由编译器决定。编译器根据变量的大小和“泄露分析”的结果来决定其位置。这也意味着在局部变量上返回引用是没问题的,而这在C或者C++这样的语言中是不行的。

如果你想知道变量分配的位置,在“go build”或“go run”上传入“-m“ gc标志(即,go run -gcflags -m app.go)。

多goroutine的读写顺序可能被重排

多个goroutine运行时,顺序的语句执行次序可能会发生调换。

Go lang Slice截取时指定最大容量以及append函数的使用说明

slice是一个三元组对象
* 一个内容指针
* 有效内容长度
* 最大长度,也就是容量。

示例:

package main

import (
    "bytes"
    "fmt"
)

func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path, '/')
    fmt.Println(sepIndex)
    dir1 := path[:sepIndex:sepIndex] //full slice expression
    fmt.Println(&path[0], &dir1[0])
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>", string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>", string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1, "suffix"...)
    fmt.Println(&dir1[0])
    path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})

    fmt.Println("dir1 =>", string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>", string(dir2)) //prints: dir2 => BBBBBBBBB (ok now)

    fmt.Println("new path =>", string(path))
}
  • 第12行的代码就是一个指定容量的截取。
  • 如果增加后不回超出slice的最大空间,slice是不会重新分配对象的。但是如果空间不够,从13行和19行的地址打印输出我们可以看到,因为append超出了指定的空间容量,系统动态重新分配了空间,地址发生了变化。

运行结果:

4
0xc82000a3b0 0xc82000a3b0
dir1 => AAAA
dir2 => BBBBBBBBB
0xc82000a450
dir1 => AAAAsuffix
dir2 => BBBBBBBBB
new path => AAAAsuffix/BBBBBBBBB
成功: 进程退出代码 0.

Go编程tips-3

String在“range”语句中的迭代值

package main

import "fmt"

func main() {  
    data := "A\xfe\x02\xff\x04"
    for _,v := range data {
        fmt.Printf("%#x ",v)
    }
    //prints: 0x41 0xfffd 0x2 0xfffd 0x4 (not ok)

    fmt.Println()
    for _,v := range []byte(data) {
        fmt.Printf("%#x ",v)
    }
    //prints: 0x41 0xfe 0x2 0xff 0x4 (good)
}

"switch"

分支默认不是继续下一个分支,默认是'break';
支持条件列表,通过逗号隔开
如果要强制进入下一分支,可以使用"fallthrough"

自增自减

不支持前置,只支持后置,也就是只支持 "i++"的用法

位运算

取反:^ XOR
与: & AND
与非:&^ AND NOT

操作符优先级

&优先于+
<<优先于+
|优先于^

未导出的结构不会被编码

[{ loading ... }]将会得到零值。下面的示例中,成员two没有被编码,所以再反序列化时,就变成空值了。

package main

import (  
    "fmt"
    "encoding/json"
)

type MyData struct {  
    One int
    two string
}

func main() {  
    in := MyData{1,"two"}
    fmt.Printf("%#v\n",in) //prints main.MyData{One:1, two:"two"}

    encoded,_ := json.Marshal(in)
    fmt.Println(string(encoded)) //prints {"One":1}

    var out MyData
    json.Unmarshal(encoded,&out)

    fmt.Printf("%#v\n",out) //prints main.MyData{One:1, two:""}
}

有coroutines的应用的退出

  • 应用是不会等待所有goroutines都完成才退出的,主应用和goroutine之间默认是不交互和感知的。

向关闭的chnnel发送数据会panic

操作没有初始化的"nil"的channel

在一个nil的channel上发送和接收操作会被永久阻塞。这个行为可以在select声明中用于动态开启和关闭case代码块的方法。

package main

import "fmt"
import "time"

func main() {
    inch := make(chan int)
    outch := make(chan int)

    go func() {
        var in <-chan int = inch
        var out chan<- int
        var val int
        for {
            select {
            case out <- val:
                out = nil
                in = inch
            case val = <-in:
                out = outch
                in = nil
            }
        }
    }()

    go func() {
        for r := range outch {
            fmt.Println("result:", r)
        }
    }()

    time.Sleep(0)
    inch <- 1
    inch <- 2
    time.Sleep(3 * time.Second)
}

HTTP响应的关闭

当你使用标准http库发起请求时,你得到一个http的响应变量。如果你不读取响应主体,你依旧需要关闭它。注意对于空的响应你也一定要这么做。对于新的Go开发者而言,这个很容易就会忘掉。
大多数情况下,当你的http响应失败时,resp变量将为nil,而err变量将是non-nil。然而,当你得到一个重定向的错误时,两个变量都将是non-nil。这意味着你最后依然会内存泄露。
通过在http响应错误处理中添加一个关闭non-nil响应主体的的调用来修复这个问题。另一个方法是使用一个defer调用来关闭所有失败和成功的请求的响应主体。

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
    _, err = io.Copy(ioutil.Discard, resp.Body)
}

resp.Body.Close()的原始实现也会读取并丢弃剩余的响应主体数据。这确保了http的链接在keepalive http连接行为开启的情况下,可以被另一个请求复用。最新的http客户端的行为是不同的。现在读取并丢弃剩余的响应数据是你的职责。如果你不这么做,http的连接可能会关闭,而不是被重用。这个小技巧应该会写在Go 1.5的文档中。

关闭HTTP连接

一些HTTP服务器保持会保持一段时间的网络连接(根据HTTP 1.1的说明和服务器端的“keep-alive”配置)。默认情况下,标准http库只在目标HTTP服务器要求关闭时才会关闭网络连接。这意味着你的应用在某些条件下消耗完sockets/file的描述符。
你可以通过设置请求变量中的Close域的值为true,来让http库在请求完成时关闭连接。
另一个选项是添加一个Connection的请求头,并设置为close。目标HTTP服务器应该也会响应一个Connection: close的头。当http库看到这个响应头时,它也将会关闭连接。
当然是否需要关闭,是由你的应用场景决定的。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    req, err := http.NewRequest("GET","http://golang.org",nil)
    if err != nil {
        fmt.Println(err)
        return
    }

    req.Close = true
    //or do this:
    //req.Header.Add("Connection", "close")

    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(len(string(body)))
}

比较Structs, Arrays, Slices, and Maps

只有每个成员本身能用"=="比较时,对象才可以用"=="比较。

DeepEqual()函数

对于不能直接比较的,可以使用DeepEqual函数
package main

import (
    "fmt"
    "reflect"
)

type data struct {
    num    int               //ok
    checks [10]func() bool   //not comparable
    doit   func() bool       //not comparable
    m      map[string]string //not comparable
    bytes  []byte            //not comparable
}

func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:", reflect.DeepEqual(v1, v2)) //prints: v1 == v2: true

    m1 := map[string]string{"one": "a", "two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:", reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:", reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true
}

DeepEqual()不会认为空的slice与“nil”的slice相等。这个行为与你使用bytes.Equal()函数的行为不同。bytes.Equal()认为“nil”和空的slice是相等的。

如果你的byte slice(或者字符串)中包含文字数据,而当你要不区分大小写形式的值时(在使用==,bytes.Equal(),或者bytes.Compare()),你可能会尝试使用“bytes”和“string”包中的ToUpper()或者ToLower()函数。对于英语文本,这么做是没问题的,但对于许多其他的语言来说就不行了。这时应该使用strings.EqualFold()和bytes.EqualFold()。

如果你的byte slice中包含需要验证用户数据的隐私信息(比如,加密哈希、tokens等),不要使用reflect.DeepEqual()、bytes.Equal(),或者bytes.Compare(),因为这些函数将会让你的应用易于被定时攻击。为了避免泄露时间信息,使用'crypto/subtle'包中的函数(即,subtle.ConstantTimeCompare())。

[转]Golang 中的格式化输入输出

待整理验证。
原文:Golang 中的格式化输入输出

正文:

【简介】

  fmt 包实现了格式化 I/O 函数,类似于 C 的 printf 和 scanf。格式“占位符”衍生自 C,但比 C 更简单。

【打印】

占位符:

[一般]

  %v 相应值的默认格式。在打印结构体时,“加号”标记(%+v)会添加字段名

  %#v 相应值的 Go 语法表示

  %T 相应值的类型的 Go 语法表示

  %% 字面上的百分号,并非值的占位符

[布尔]

  %t 单词 true 或 false。

[整数]

  %b 二进制表示

  %c 相应 Unicode 码点所表示的字符

  %d 十进制表示

  %o 八进制表示

  %q 单引号围绕的字符字面值,由 Go 语法安全地转义

  %x 十六进制表示,字母形式为小写 a-f

  %X 十六进制表示,字母形式为大写 A-F

  %U Unicode 格式:U+1234,等同于 "U+%04X"

[浮点数及其复合构成]

  %b 无小数部分的,指数为二的幂的科学计数法,与 strconv.FormatFloat 的 'b' 转换格式一致。例如 -123456p-78

  %e 科学计数法,例如 -1234.456e+78

  %E 科学计数法,例如 -1234.456E+78

  %f 有小数点而无指数,例如 123.456

  %g 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的 0)输出

  %G 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的 0)输出

[字符串与字节切片]

  %s 字符串或切片的无解译字节

  %q 双引号围绕的字符串,由 Go 语法安全地转义

  %x 十六进制,小写字母,每字节两个字符

  %X 十六进制,大写字母,每字节两个字符

[指针]

  %p 十六进制表示,前缀 0x

[注意]

  这里没有 'u' 标记。若整数为无符号类型,他们就会被打印成无符号的。类似地, 这里也不需要指定操作数的大小(int8,int64)。

  宽度与精度的控制格式以 Unicode 码点为单位。(这点与 C 的 printf 不同, 它以字节数为单位。)二者或其中之一均可用字符 '*' 表示, 此时它们的值会从下一个操作数中获取,该操作数的类型必须为 int。

// 宽度与精度的控制以 Unicode 码点为单位
fmt.Printf("\"%8s\"\n", "123456") // 最大长度为 8
// " 123456"
fmt.Printf("\"%8s\"\n", "你好") // 最大长度为 8
// " 你好"

// 宽度与精度均可用字符 '' 表示
fmt.Printf("%0
.*f \n", 8, 3, 13.25) // 总长度 8,小数位数 3
fmt.Printf("%08.3f \n", 13.25) // 总长度 8,小数位数 3
// 0013.250

  对数值而言,宽度为该数值占用区域的最小宽度;精度为小数点之后的位数。 但对于 %g/%G 而言,精度为所有数字的总数。例如,对于 123.45,格式 %6.2f 会打印 123.45,而 %.4g 会打印 123.5。%e 和 %f 的默认精度为 6;但对于 %g 而言,它的默认精度为确定该值所必须的最小位数。

  对大多数值而言,宽度为输出的最小字符数,如果必要的话会为已格式化的形式填充空格。对字符串而言,精度为输出的最大字符数,如果必要的话会直接截断。

// 宽度与精度标记字符串
fmt.Printf("%8q", "ABC") // 最小长度为 8(包括 %q 的引号字符)
// "ABC"
fmt.Printf("%.8q", "1234567890") // 最大长度为 8(不包括 %q 的引号字符)
// "12345678"

[其它标记]

  + 总打印数值的正负号;对于 %q(%+q)保证只输出 ASCII 编码的字符。

  - 在右侧而非左侧填充空格(左对齐该区域)

  # 备用格式:为八进制添加前导 0(%#o),为十六进制添加前导 0x(%#x)或

  0X(%#X),为 %p(%#p)去掉前导 0x;如果可能的话,%q(%#q)会打印原始(即反引号围绕的)字符串;如果是可打印字符,%U(%#U)会写出该字符的 Unicode 编码形式(如字符 x 会被打印成 U+0078 'x')。

  ' ' (空格)为数值中省略的正负号留出空白(% d);以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开:

fmt.Printf("% x\n", "Hello")
// 48 65 6c 6c 6f

  0 填充前导的 0 而非空格;对于数字,这会将填充移到正负号之后

[注意]

  标记有时会被占位符忽略,所以不要指望它们。例如十进制没有备用格式,因此 %#d 与 %d 的行为相同。

  对于每一个 Printf 类的函数,都有一个 Print 函数,该函数不接受任何格式化, 它等价于对每一个操作数都应用 %v。另一个变参函数 Println 会在操作数之间插入空白, 并在末尾追加一个换行符。

  不考虑占位符的话,如果操作数是接口值,就会使用其内部的具体值,而非接口本身。 因此:

var i interface{} = 23
fmt.Printf("%v\n", i)
// 会打印 23

  若一个操作数实现了 Formatter 接口,该接口就能更好地用于控制格式化。

  若其格式(它对于 Println 等函数是隐式的 %v)对于字符串是有效的(%s %q %v %x %X),以下两条规则也适用:

  1、若一个操作数实现了 error 接口,Error 方法就能将该对象转换为字符串,随后会根据占位符的需要进行格式化。

  2、若一个操作数实现了 String() string 方法,该方法能将该对象转换为字符串,随后会根据占位符的需要进行格式化。

  为避免以下这类递归的情况:

  type X string
  func (x X) String() string { return Sprintf("<%s>", x) }

  需要在递归前转换该值:
  func (x X) String() string { return Sprintf("<%s>", string(x)) }

[格式化错误]

  如果给占位符提供了无效的实参(例如将一个字符串提供给 %d),所生成的字符串会包含该问题的描述,如下例所示:

  类型错误或占位符未知:%!verb(type=value)

Printf("%d", hi)
// %!d(string=hi)

  实参太多:%!(EXTRA type=value)

Printf("hi", "guys")
// hi%!(EXTRA string=guys)

  实参太少:%!verb(MISSING)

Printf("hi%d")
// hi %!d(MISSING)

  宽度或精度不是 int 类型:%!(BADWIDTH)或 %!(BADPREC)

Printf("%*s", 4.5, "hi")
// %!(BADWIDTH)hi

Printf("%.*s", 4.5, "hi")
// %!(BADPREC)hi

  所有错误都始于“%!”,有时紧跟着单个字符(占位符),并以小括号括住的描述结尾。

【扫描】

  一组类似的函数通过扫描已格式化的文本来产生值。Scan、Scanf 和 Scanln 从 os.Stdin 中读取;Fscan、Fscanf 和 Fscanln 从指定的 io.Reader 中读取; Sscan、Sscanf 和 Sscanln 从实参字符串中读取。Scanln、Fscanln 和 Sscanln 在换行符处停止扫描,且需要条目紧随换行符之后;Scanf、Fscanf 和 Sscanf 需要输入换行符来匹配格式中的换行符;其它函数则将换行符视为空格。

  Scanf、Fscanf 和 Sscanf 根据格式字符串解析实参,类似于 Printf。例如,%x 会将一个整数扫描为十六进制数,而 %v 则会扫描该值的默认表现格式。

  格式化行为类似于 Printf,但也有如下例外:

  %p 没有实现
  %T 没有实现
  %e %E %f %F %g %G 都完全等价,且可扫描任何浮点数或复合数值
  %s 和 %v 在扫描字符串时会将其中的空格作为分隔符
  标记 # 和 + 没有实现

  在使用 %v 占位符扫描整数时,可接受友好的进制前缀 0(八进制)和 0x(十六进制)。

  宽度被解释为输入的文本(%5s 意为最多从输入中读取 5 个符文来扫描成字符串),而扫描函数则没有精度的语法(没有 %5.2f,只有 %5f)。

  当以某种格式进行扫描时,无论在格式中还是在输入中,所有非空的连续空白字符 (除换行符外)都等价于单个空格。由于这种限制,格式字符串文本必须匹配输入的文本,如果不匹配,扫描过程就会停止,并返回已扫描的实参数。

  在所有的扫描参数中,若一个操作数实现了 Scan 方法(即它实现了 Scanner 接口),该操作数将使用该方法扫描其文本。此外,若已扫描的实参数少于所提供的实参数,就会返回一个错误。

  所有需要被扫描的实参都必须是基本类型或实现了 Scanner 接口的类型。

  注意:Fscan 等函数会从输入中多读取一个字符(符文),因此,如果循环调用扫描函数,可能会跳过输入中的某些数据。一般只有在输入的数据中没有空白符时该问题才会出现。若提供给 Fscan 的读取器实现了 ReadRune,就会用该方法读取字符。若此读取器还实现了 UnreadRune 方法,就会用该方法保存字符,而连续的调用将不会丢失数据。若要为没有 ReadRune 和 UnreadRune 方法的读取器加上这些功能,需使用 bufio.NewReader。

Go编程tips-2

未使用的imports

可以注释掉或者加下划线'_',否则,编译报错

package main

import (  
    - "fmt"
    "log"
    "time"
)

var _ = log.Println

func main() {  
    _ = time.Now
}

使用简式声明重复声明变量

你不能在一个单独的声明中重复声明一个变量,但在多变量声明中这是允许的,其中至少要有一个新的声明变量。

重复变量需要在相同的代码块内,否则你将得到一个隐藏变量。

Fails:

package main

func main() {  
    one := 0
    one := 1 //error
}

Compile Error:

/tmp/sandbox706333626/main.go:5: no new variables on left side of :=

Works:

package main

func main() {  
    one := 0
    one, two := 1,2

    one,two = two,one
}

用 nil 初始化slice和map

在一个“nil”的slice中添加元素是没问题的,但对一个map做同样的事将会生成一个运行时的panic。

Works:

package main

func main() {  
    var s []int
    s = append(s,1)
}

Fails:

package main

func main() {  
    var m map[string]int
    m["one"] = 1 //error

}

map 的容量

你可以在map创建时指定它的容量,但你无法在map上使用cap()函数。
package main

func main() {  
    m := make(map[string]int,99)
    cap(m) //error
}

Compile Error:

/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap

字符串不能初始化为 "nil"

这对于经常使用“nil”分配字符串变量的开发者而言是个需要注意的地方。

二维数组的创建

你可以使用纯一维数组、“独立”切片的切片,“共享数据”切片的切片来构建动态的多维数组。

如果你使用纯一维的数组,你需要处理索引、边界检查、当数组需要变大时的内存重新分配。

使用“独立”slice来创建一个动态的多维数组需要两步。首先,你需要创建一个外部的slice。然后,你需要分配每个内部的slice。内部的slice相互之间独立。你可以增加减少它们,而不会影响其他内部的slice。

package main

func main() {  
    x := 2
    y := 4

    table := make([][]int,x)
    for i:= range table {
        table[i] = make([]int,y)
    }
}

使用“共享数据”slice的slice来创建一个动态的多维数组需要三步。首先,你需要创建一个用于存放原始数据的数据“容器”。然后,你再创建外部的slice。最后,通过重新切片原始数据slice来初始化各个内部的slice。

package main

import "fmt"

func main() {  
    h, w := 2, 4

    raw := make([]int,h*w)
    for i := range raw {
        raw[i] = i
    }
    fmt.Println(raw,&raw[4])
    //prints: [0 1 2 3 4 5 6 7] <ptr_addr_x>

    table := make([][]int,h)
    for i:= range table {
        table[i] = raw[i*w:i*w + w]
    }

    fmt.Println(table,&table[1][0])
    //prints: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>
}

判断map中节点是否存在

检测给定map中的记录是否存在的最可信的方法是,通过map的访问操作,检查第二个返回的值。

package main

import "fmt"

func main() {  
    x := map[string]string{"one":"a","two":"","three":"c"}

    if _,ok := x["two"]; !ok {
        fmt.Println("no entry")
    }
}

string的修改

如果是ASCII码,要转换成[]byte,如果是unicode,要转化成rune byte
[]byte的方法

package main

import "fmt"

func main() {  
    x := "text"
    xbytes := []byte(x)
    xbytes[0] = 'T'

    fmt.Println(string(xbytes)) //prints Text
}

string 与 rune byte 的关系

在Go当中 string底层是用byte数组存的,并且是不可以改变的。

例如 s:="Go编程" fmt.Println(len(s)) 输出结果应该是8因为中文字符是用3个字节存的。

len(string(rune('编')))的结果是3

如果想要获得我们想要的情况的话,需要先转换为rune切片再使用内置的len函数

fmt.Println(len([]rune(s)))

结果就是4了。

所以用string存储unicode的话,如果有中文,按下标是访问不到的,因为你只能得到一个byte。 要想访问中文的话,还是要用rune切片,这样就能按下表访问。

判断字符串是否为UTF8

为了知道字符串是否是UTF8,你可以使用“unicode/utf8”包中的ValidString()函数。

求字符串长度

ASCII:len();
unicode:RuneCountInString()。实际上,RuneCountInString()求的是rune的个数,不是绝对的字符个数,因为一个字符可能占多个rune

在多行的Slice、Array和Map语句中遗漏逗号

Fails:

package main

func main() {  
    x := []int{
    1,
    2 //error
    }
    _ = x
}

Works:

package main

func main() {  
    x := []int{
    1,
    2,
    }
    x = x

    y := []int{3,4,} //no error
    y = y
}

当你把声明折叠到单行时,如果你没加末尾的逗号,你将不会得到编译错误

log.Fatal和log.Panic不仅仅是Log

Logging库一般提供不同的log等级。与这些logging库不同,Go中log包在你调用它的Fatal()和Panic()函数时,可以做的不仅仅是log。当你的应用调用这些函数时,Go也将会终止应用 🙂

Go面向对象

本来想写下相关的内容,找材料时,发现了这篇不错的内容,强烈推荐,关于面向对象的基本用法都讲到了。

原文:Go面向对象
原英文:Go Object Oriented Design

正文:

Go是一个完全面向对象的语言。例如,它允许基于我们定义的类型的方法,而没有像其他语言一样的装箱/拆箱操作。

Go没有使用classes,但提供很多相似的功能:

·通过嵌入实现的自动消息委托

·通过接口实现多台

·通过exports实现的命名空间

Go语言中没有继承。忘记is-a的关系,而是就组合而言的面向对象设计。

“使用经典的继承始终是可选的;每个问题都可以通过其他方法得到解决” - Sandi Metz

通过例子说明组合

最近阅读了一篇Ruby的面向对象编程实践, 我决定使用Go语言翻译这个例子。

Chapter 6说明了一个简单的问题。维修工需要知道自行车出行需要带上的备件,决定于哪一辆自行车已经被租出去。问题可以通过经典的继承来解决,山地车和公路自行车是自行车基类的一个特殊化例子。Chapter 8使用组合改写了同一个例子。我很高兴这个例子翻译成Go。让我们看看。

Packages(包)

  1. package main
  2. import "fmt"

包提供了命名空间概念. main() 函数是这个包的入口函数. fmt包提供格式化功能

Types(类型)

  1. type Part struct {
  2.     Name        string
  3.     Description string
  4.     NeedsSpare  bool
  5. }

我们定义了一个新的类型名为Part, 非常像c的结构体

  1. type Parts []Part

Parts类型是包含Part类型的数组切片, Slice可以理解为动态增长的数组, 在Go中是很常见的.

我们可以在任何类型上声明方法,  所以我们不需要要再去封装 []Part, 这意味着 Parts 会拥有slice的所有行为, 再加上我们自己定义的行为方法.

方法

  1. func (parts Parts) Spares() (spares Parts) {
  2.     for _, part := range parts {
  3.         if part.NeedsSpare {
  4.             spares = append(spares, part)
  5.         }
  6.     }
  7.     return spares
  8. }

Go中定义方法就像一个函数,除了它有一个显式的接收者,紧接着func之后定义。这个函数利用命名返回变量,并为我们初始化备件。

方法的主体十分简单。我们重复parts,忽略索引的位置(_),过滤parts后返回。append builtin 需要分配和返回一个大的切片,因为我们并没有预先分配好它的容量。

这段代码没有ruby代码来得优雅。在Go语言中有过滤函数,但它并非是builtin.

内嵌

  1. type Bicycle struct {
  2.     Size string
  3.     Parts
  4. }

自行车由Size和Parts组成。没有给Parts指定一个名称,我们是要保证实现 内嵌。这样可以提供自动的委托,不需特殊的声明,例如bike.Spares()和bike.Parts.Spares()是等同的。

如果我们向Bicycle增加一个Spares()方法,它会得到优先权,但是我们仍然引用嵌入的Parts.Spares()。这跟继承十分相似,但是内嵌并不提供多态。Parts的方法的接收者通常是Parts类型,甚至是通过Bicycle委托的。

与继承一起使用的模式,就像模板方法模式,并不适合于内嵌。就组合和委托而言去考虑会更好,就如我们这个例子一样。

Composite Literals(复合语义)

  1. var (
  2.     RoadBikeParts = Parts{
  3.         {"chain""10-speed"true},
  4.         {"tire_size""23"true},
  5.         {"tape_color""red"true},
  6.     }
  7.     MountainBikeParts = Parts{
  8.         {"chain""10-speed"true},
  9.         {"tire_size""2.1"true},
  10.         {"front_shock""Manitou"false},
  11.         {"rear_shock""Fox"true},
  12.     }
  13.     RecumbentBikeParts = Parts{
  14.         {"chain""9-speed"true},
  15.         {"tire_size""28"true},
  16.         {"flag""tall and orange"true},
  17.     }
  18. )

Go提供优美的语法,来初始化对象,叫做 composite literals。使用像数组初始化一样的语法,来初始化一个结构,使得我们不再需要ruby例子中的Parts工厂。

  1. func main() {
  2.     roadBike := Bicycle{Size: "L", Parts: RoadBikeParts}
  3.     mountainBike := Bicycle{Size: "L", Parts: MountainBikeParts}
  4.     recumbentBike := Bicycle{Size: "L", Parts: RecumbentBikeParts}

Composite literals(复合语义)同样可以用于字段:值的语法,所有的字段都是可选的。

简短的定义操作符(:=)通过Bicycle类型,使用类型推论来初始化roadBike,和其他。

输出

  1. fmt.Println(roadBike.Spares())
  2. fmt.Println(mountainBike.Spares())
  3. fmt.Println(recumbentBike.Spares())

我们将以默认格式打印 Spares 的调用结果:

  1. [{chain 10-speed true} {tire_size 23 true} {tape_color red true}]
  2. [{chain 10-speed true} {tire_size 2.1 true} {rear_shock Fox true}]
  3. [{chain 9-speed true} {tire_size 28 true} {flag tall and orange true}]

组合 Parts

  1.     comboParts :Parts{}
  2.     comboParts = append(comboParts, mountainBike.Parts...)
  3.     comboParts = append(comboParts, roadBike.Parts...)
  4.     comboParts = append(comboParts, recumbentBike.Parts...)
  5.     fmt.Println(len(comboParts), comboParts[9:])
  6.     fmt.Println(comboParts.Spares())
  7. }

Parts 的行为类似于 slice。按照长度获取切片,或者将数个切片结合。Ruby 中的类似解决方案就数组的子类,但是当两个 Parts 连接在一起时,Ruby 将会“错置” spares 方法。

“……在一个完美的面向对象的语言,这种解决方案是完全正确的。不幸的是,Ruby语言并没有完美的实现……”
—— Sandi Metz

在 Ruby 中有一个那看的解决方法,使用 Enumerable、forwardable,以及 def_delegators。 Go有没有这样的缺陷。 []Part 正是我们所需要的,且更为简洁(更新:Ruby 的 SimpleDelegator 看上去好了一点)。

接口 Interfaces

Go的多态性由接口提供。不像JAVA和C#,它们是隐含实现的,所以接口可以为不属于我们的代码定义。

和动态类型比较,接口是在它们声明过程中静态检查和说明的,而不是通过写一系列响应(respond_to)测试完成的。

“不可能不知不觉的或者偶然的创建一个抽象;在静态类型语言中定义的接口总是有倾向性的。” - Sandi Metz

给个简单的例子,假设我们不需要打印Part的NeedsSpare标记。我们可以写这样的字符串方法:

  1. func (part Part) String() string {
  2.     return fmt.Sprintf("%s: %s", part.Name, part.Description)
  3. }

然后对上述Print的调用将会输出这样的替代结果:

  1. [chain: 10-speed tire_size: 23 tape_color: red]
  2. [chain: 10-speed tire_size: 2.1 rear_shock: Fox]
  3. [chain: 9-speed tire_size: 28 flag: tall and orange]

这个机理是因为我们实现了fmt包会用到的Stringer接口。它是这么定义的:

  1. type Stringer interface {
  2.     String() string
  3. }

接口类型在同一个地方可以用作其它类型。变量与参数可以携带一个Stringer,可以是任何实现String() string方法签名的接口。

Exports 导出

Go 使用包来管理命名空间, 要使某个符号对其他包(package )可见(即可以访问),需要将该符号定义为以大写字母开头,  当然,如果以小写字母开关,那就是私有的.包外不可见.

  1. type Part struct {
  2.     name        string
  3.     description string
  4.     needsSpare  bool
  5. }

为了对Part类型应用统一的访问原则(uniform access principle), 我们可以改变Part类型的定义并提供setter/getter 方法,就像这样:

  1. func (part Part) Name() string {
  2.     return part.name
  3. }
  4. func (part *Part) SetName(name string) {
  5.     part.name = name
  6. }

这样可以很容易的确定哪些是public API, 哪些是私有的属性和方法, 只要通过字母的大小写.(例如(part.Name()vs.part.name)

注意 我们不必要对 getters 加前Get, (例如.GetName),Getter不是必需,特别是对于字符串,当我们有需要时,我们可以使用满足Stringer 类型接口的自定义的类型去改变Name 字段。

找到一些私有性

私有命名(小写字母)可以从同一个包的任何地方访问到,即使是包含了跨越多个文件的多个结构。如果你觉得这令人不安,包也可以像你希望的那么小。

可能的情况下用(更稳固的)公共API是一个好的实践,即使是来自经典语言的同样的类中。这需要一些约定,当然这些约定可以应用在GO中。

最大的好处

组合,内嵌和接口提供了Go语言中面向对象设计的强大工具。继承概念的思想真的不起什么作用。相信我,我尝试了

习惯Go需要思维的改变,当触及到Go对象模型的力量时,我非常高兴的吃惊于Go代码的简单和简洁。

Go编程tips-1

结构体的初始化

示例代码中,前者是创建普通对象,后者是创建指针

package main

import (
    "fmt"
)

type Rect struct {
    x, y          float64
    width, height float64
}

func main() {
    // common object
    rect2 := Rect{}
    rect3 := Rect{0, 0, 100, 200}
    rect4 := Rect{width: 100, height: 200}
    fmt.Println(rect2, rect3, rect4)

    /*
        // pointer objects
        rect1 := new(Rect)
        rect2 := &Rect{}
        rect3 := &Rect{0, 0, 100, 200}
        rect4 := &Rect{width: 100, height: 200}
        fmt.Println(rect1, rect2, rect3, rect4)
    */
}

变参

go支持变参,变参中所有参数的类别必须是同一种,且必须是最后一个形参。使用方法如下:“…type”表示具有不定个type类型的参数,不定参数实质上是一个slice类型,故可以使用range对其参数进行取值。如下例子。

func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

defer 与return执行顺序

当函数执行到最后时,先执行defer语句,然后才执行return语句.所以可以利用这个来进行资源安全关闭,解加锁,记录执行情况等。defer是采用先进后出的模式的,这种情形与栈的情况一致。注意:定义的defer延迟操作,如有提供参数会发生值的拷贝,尽管这些函数在退出时才执行,但所使用的参数是在定义时就进行拷贝,拷贝的原则和变量的赋值原则一个,slice,map,channel是指针拷贝.如下例子:

package main

import (
    "fmt"
)

func main() {
    x := 1
    defer func(a int) { //直接将x=1赋值给a,虽然他在后面才执行.
        fmt.Println("a=", a)
    }(x)
    defer func() {
        fmt.Println("x=", x) //经过x++后,x值变为2
    }()
    x++
}

运行结果:
x= 2
a= 1

临时变量的作用域

go语言中对于堆和栈的内存分配没有严格区分,在go中返回一个局部变量的地址是绝对没有问题的,变量关联的存储在函数返回后依然存在.(注:尤其对由C/C++转过来的程序员,开始肯定不是很适应,但是go这种内存分配方式解放了程序员,使得程序员能够专注做事情,而不用花费太多的时间在堆和栈的内存分配上).更直接的说,在go语言中,如果一个局部变量在函数返回后仍然被使用,那么这个变量会在堆heap,而不是栈stack中进行内存分配.详情参考How do I know whether a variable is allocated on the heap or the stack?

Mac下零基础学习go语言-2-开发环境的搭建

环境介绍

  • 系统:OS X EI Capitan 10.11.1
  • go version:1.5.1

安装

下载最新的安装包 https://golang.org/dl/

mac下安装

直接点击,按照引导就可以完成安装。

linux下安装

linux下实际就是手动设置文件路径了,大概的动作如下:

#!/bin/bash
tar -C /usr/local -xzf go*.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile

最后重启,或者:

source /etc/profile

验证版本

终端验证,输入

go version
go version go1.11.2 linux/amd64

配置环境变量

设置环境变量,go默认安装在/usr/local/go 下面,参见:Mac OS X 配置环境变量

所以我们修改 .bash_profile:

export GOPATH=/Users/alex/dev/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN

注意:

  • 默认情况下GOROOT已经在安装时指定为安装目录了,是不需要设置的
  • GOPATH 设置成你的本地开发路径,而不是安装路径

设置完成后可以用 go env 命令检测

测试

在 GOPATH下 的src目录下创建项目demo。注意这个将是默认的bin档名。 然后在demo目录下创建main.go

package main

import (
  "fmt"
)

func main() {
  fmt.Println("hello world");
}

然后运行 go build demo,如果在demo项目目录下,直接 go build,完成后,在项目目录下生成 demo

直接运行 ./demo,就会输出 :hello world

DEBUG

初学者建议在LiteIDE下面debug,因为IDE直接UI支持,熟悉了以后也可以在命令行下面直接debug。第一次试用时要解决GDB的证书签名问题,可以参看网上同学给出的操作流程。

下面终于可以开始coding了!