输入与输出-fmt包

输入与输出

  • 常用输出函数

    Print、Printf、Println:直接输出内容

    Sprint、Sprintf、Sprintln:生成内容并返回字符串

    Fprint:将内容输出到一个io.Writer接口类型的变量,经常用于写入文件

    Errorf:根据format参数生成格式化字符串并返回一个包含该字符串的错误

```go
fmt.Println("打开文件出错,err:", err)//输出带换行
s2 := fmt.Sprintf("name:%s,age:%d", name, age)//带格式生成并返回
fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)//带格式写入文件
err := fmt.Errorf("这是一个错误")
```
  • 常用占位符

    占位符 说明
    %v 值的默认格式表示
    %+v 类似%v,但输出结构体时会添加字段名
    %#v 值的golang语法表示
    %T 打印值的类型
    %% 百分号
    %d 表示10进制数
    %b 表示2进制数
    %f 浮点数,有小数
    %9.2f 宽度9,精度2
    %e 科学计数法
    %s 直接输出字符串或[]byte
    %q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
    %p 指针,表示未16进制,并加上前缀0x
  • 常用输入函数

    Scan、Scanf、Scanln:可以在程序运行过程中从标准输入获取用户的输入。

    Scanln比较常用:在终端扫描标准输入,以空格分隔,直到换行结束扫描

```go
fmt.Scanln(&name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
```

bufio.NewReader:获取完整输入内容

FScan、Fscanf、Fscanln:从文件中获取输入

Sscan、Sscanf、Sscanln:从字符串获取输入

时间与日期-time包

func timeDemo() {
    now := time.Now() //获取当前时间
    fmt.Printf("current time:%v\n", now)

    year := now.Year()     //年
    month := now.Month()   //月
    day := now.Day()       //日
    hour := now.Hour()     //小时
    minute := now.Minute() //分钟
    second := now.Second() //秒
    fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)

    timestamp1 := now.Unix()     //时间戳
    timestamp2 := now.UnixNano() //纳秒时间戳
    fmt.Printf("current timestamp1:%v\n", timestamp1)
    fmt.Printf("current timestamp2:%v\n", timestamp2)   
    
    timeObj := time.Unix(timestamp1, 0)//时间戳转换成时间对象,再通过类似以上当前时间转换成时间格式

}
const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)
var _time = 10 * time.Second //10秒
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
// 12小时制
fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))
fmt.Println(now.Format("2006/01/02"))
func main() {
    now := time.Now()
    later := now.Add(time.Hour) // 当前时间加1小时后的时间
    beforer := now.Add(-time.Hour) // 当前时间减1小时后的时间
    fmt.Println(later)
}
package main

import (
    "fmt"
    "time"
)

func main() {
    /*
        用sleep实现定时器
    */
    fmt.Println(time.Now())
    time.Sleep(time.Second)
    fmt.Println(time.Now())
    /*
        用timer实现定时器
    */
    timer := time.NewTimer(time.Second)
    fmt.Println(<-timer.C)
    /*
        用after实现定时器
    */
    fmt.Println(<-time.After(time.Second))

}
func tickDemo() {
    ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
    //ticker := time.NewTicker(time.Second)
    for i := range ticker {
        fmt.Println(i)//每秒都会执行的任务
    }
}

命令行参数解析-flag包

flag包是的golang开发命令行工具更为简单。

看一个完整示例,我们就更清楚flag的用途了:

执行命令时要求输入4个参数,并指定了参数的类型与默认值

func main() {
    //定义命令行参数方式1
    var name string
    var age int
    var married bool
    var delay time.Duration
    flag.StringVar(&name, "name", "张三", "姓名")
    flag.IntVar(&age, "age", 18, "年龄")
    flag.BoolVar(&married, "married", false, "婚否")
    flag.DurationVar(&delay, "d", 0, "延迟的时间间隔")

    //解析命令行参数
    flag.Parse()
    fmt.Println(name, age, married, delay)
    //返回命令行参数后的其他参数
    fmt.Println(flag.Args())
    //返回命令行参数后的其他参数个数
    fmt.Println(flag.NArg())
    //返回使用的命令行参数个数
    fmt.Println(flag.NFlag())
}

首先flag提供了命令行help功能,执行命令行会给出相应提示:

    $ ./flag_demo -help
    Usage of ./flag_demo:
      -age int
            年龄 (default 18)
      -d duration
            时间间隔
      -married
            婚否
      -name string
            姓名 (default "张三")

其次flag提供命令行参数parse解析能力:

注意:Args()\NArg()\NFlag()的含义

$ ./flag_demo -name pprof --age 28 -married=false -d=1h30m
pprof 28 false 1h30m0s
[]
0
4

当然如果我们仅仅只是需要简单命令行输入的参数,我们也可以简单的考虑os.Args来获取命令行参数。

os.Args是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称,这和大部分语言命令行模式是类似的(python、php等)

日志-log包

官方标准简单log包,功能有限,更多可以实现流水账的日志记录,如果我们需要更多比如不同级别的日志记录,可以选择第三方日志库:logrus、zap等

//使用标准日志log,设置日志输出到xx.log文件,设置flags支持文件名、行号、日志前缀、时间格式
func main() {
    logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        fmt.Println("open log file failed, err:", err)
        return
    }
    log.SetOutput(logFile)
    log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)    
    log.SetPrefix("[JM]")
    log.Println("这里记录一条日志。")
}
[JM]2020/01/14 15:32:45.431506 .../log_demo/main.go:13: 这里记录一条日志。

IO操作-os包

os包提供了Create、NewFile、Open、OpenFile、Remove方法

返回的文件对象,提供了读写方法,比如Write、WriteAt、WriteString、Read、ReadAt方法

package main

import (
    "fmt"
    "os"
)

func main() {
    // 只读方式打开当前目录下的main.go文件
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
    // 关闭文件
    file.Close()
}
  • 写文件
```go
file.WriteString("ab\n")
file.Write([]byte("cd\n"))
```

我们会更新一篇关于byte与string区别的文章
package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开文件
    file, err := os.Open("./xxx.txt")
    if err != nil {
        fmt.Println("open file err :", err)
        return
    }
    defer file.Close()
    // 定义接收文件读取的字节数组
    var buf [128]byte
    var content []byte
    for {
        n, err := file.Read(buf[:])
        if err == io.EOF {
            // 读取结束
            break
        }
        if err != nil {
            fmt.Println("read file err ", err)
            return
        }
        content = append(content, buf[:n]...)
    }
    fmt.Println(string(content))
}

IO操作-bufio包与ioutil包

bufio包实现了带缓冲区的读写,是对文件读写的封装

前面说到开发者更喜欢使用更上层的读写方法,golang的bufio包除了实现带缓冲区的读写提高效率和稳定性外,还提供按行读方法,ioutil包提供了读取整个文件、写文件方法

bufio、ioutil包更多文件、目录读写详见官方标准库

strconv包

b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64)
i, err := strconv.ParseInt("-2", 10, 64)
u, err := strconv.ParseUint("2", 10, 64)
s1 := strconv.FormatBool(true)
s2 := strconv.FormatFloat(3.1415, 'E', -1, 64)
s3 := strconv.FormatInt(-2, 16)
s4 := strconv.FormatUint(2, 16)

strconv包中还有Append系列、Quote系列等函数。详细见官方标准库

模板-template包

html/template包实现了数据驱动的模板,用于生成可对抗代码注入的安全HTML输出。

{{.}}
type UserInfo struct {
Name   string
Gender string
Age    int
}

func sayHello(w http.ResponseWriter, r *http.Request) {
    // 解析指定文件生成模板对象
    tmpl, err := template.ParseFiles("./hello.html")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    user := UserInfo{
        Name:   "枯藤",
        Gender: "男",
        Age:    18,
    }
    // 利用给定数据渲染模板,并将结果写入w
    tmpl.Execute(w, user)
}
<body>
<p>Hello {{.Name}}</p>
<p>性别:{{.Gender}}</p>
<p>年龄:{{.Name}}</p>
</body>
{{/* a comment */}}
注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。
{{/* a comment */}}
注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止,就像这里表示的一样。
{{pipeline}}
    pipeline的值的默认文本表示会被拷贝到输出里。
{{if pipeline}} T1 {{end}}
    如果pipeline的值为empty,不产生输出,否则输出T1执行结果。不改变dot的值。
    Empty值包括false、0、任意nil指针或者nil接口,任意长度为0的数组、切片、字典。
{{if pipeline}} T1 {{else}} T0 {{end}}
    如果pipeline的值为empty,输出T0执行结果,否则输出T1执行结果。不改变dot的值。
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
    用于简化if-else链条,else action可以直接包含另一个if;等价于:
        {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
{{range pipeline}} T1 {{end}}
    pipeline的值必须是数组、切片、字典或者通道。
    如果pipeline的值其长度为0,不会有任何输出;
    否则dot依次设为数组、切片、字典或者通道的每一个成员元素并执行T1;
    如果pipeline的值为字典,且键可排序的基本类型,元素也会按键的顺序排序。
{{range pipeline}} T1 {{else}} T0 {{end}}
    pipeline的值必须是数组、切片、字典或者通道。
    如果pipeline的值其长度为0,不改变dot的值并执行T0;否则会修改dot并执行T1。
{{template "name"}}
    执行名为name的模板,提供给模板的参数为nil,如模板不存在输出为""
{{template "name" pipeline}}
    执行名为name的模板,提供给模板的参数为pipeline的值。
{{with pipeline}} T1 {{end}}
    如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。
{{with pipeline}} T1 {{else}} T0 {{end}}
    如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。
eq      如果arg1 == arg2则返回真
ne      如果arg1 != arg2则返回真
lt      如果arg1 < arg2则返回真
le      如果arg1 <= arg2则返回真
gt      如果arg1 > arg2则返回真
ge      如果arg1 >= arg2则返回真
 {{eq arg1 arg2 arg3}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>tmpl test</title>
</head>
<body>

    <h1>测试嵌套template语法</h1>
    <hr>
    {{template "ul.html"}}
    <hr>
    {{template "ol.html"}}
</body>
</html>

{{ define "ol.html"}}
<h1>这是ol.html</h1>
<ol>
    <li>AA</li>
    <li>BB</li>
    <li>CC</li>
</ol>
{{end}}
func tmplDemo(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("./t.html", "./ul.html")
if err != nil {
    fmt.Println("create template failed, err:", err)
    return
}
user := UserInfo{
    Name:   "枯藤",
    Gender: "男",
    Age:    18,
}
tmpl.Execute(w, user)

}
```

http包

Go语言内置的net/http包十分的优秀,提供了HTTP客户端和服务端的实现

resp, err := http.Get("http://www.baidu.com/")

resp, err := http.Post("http://www.9ong.com/post", "image/jpeg", &buf)

resp, err := http.PostForm("http://www.9ong.com/form",
    url.Values{"key": {"Value"}, "id": {"123"}})

if err != nil {
    // handle error
}
//使用完response后必须关闭回复的主体
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
apiUrl := "http://127.0.0.1:9090/get"
// URL param
data := url.Values{}
data.Set("name", "枯藤")
data.Set("age", "18")
u, err := url.ParseRequestURI(apiUrl)
if err != nil {
    fmt.Printf("parse url requestUrl failed,err:%v\n", err)
}
u.RawQuery = data.Encode() // URL encode
fmt.Println(u.String())
resp, err := http.Get(u.String())
if err != nil {
    fmt.Println("post failed, err:%v\n", err)
    return
}
// 表单数据
//contentType := "application/x-www-form-urlencoded"
//data := "name=jm&age=20"
// json
contentType := "application/json"
data := `{"name":"jm","age":20}`
resp, err := http.Post(url, contentType, strings.NewReader(data))
http.Get(url,json对象参数|结构体参数)
http.Post(url,options,data) //options负责设置http头等信息,data是参数json对象或结构体
```go
// http server

func sayHello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello world")
}

func main() {
    http.HandleFunc("/", sayHello)
    err := http.ListenAndServe(":9527", nil)
    if err != nil {
        fmt.Printf("http server failed, err:%v\n", err)
        return
    }
}
```

自定义server

```go
s := &http.Server{
    Addr:           ":9527",
    Handler:        myHandler,
    ReadTimeout:    10 * time.Second,
    WriteTimeout:   10 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
```

context

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。

介绍goroutine时,我们看到范例并没有在main函数里使用context,goroutine也会自动退出。原因是只有一种情况正在运行的goroutine会因为其他goroutine的结束被终止,就是main函数的退出或程序停止执行.

sync.WaitGroup解决了协程协同同步完成问题,context主要为了解决协程协同取消问题

type Context interface {
    Deadline() (deadline time.Time, ok bool) //返回当前Context被取消的时间,也就是完成工作的截止时间
    Done() <-chan struct{} //返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel
    Err() error //返回当前Context结束的原因
    Value(key interface{}) interface{} //从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据
}

注意:以下介绍的都是函数,并不是创建后context上下问对像的方法,而是context包的函数

func gen(ctx context.Context) <-chan int {
    dst := make(chan int)
    n := 1
    go func() {
        for {
            select {
            case <-ctx.Done():
                return // return结束该goroutine,防止泄露
            case dst <- n:
                n++
            }
        }
    }()
    return dst
}
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 当我们取完需要的整数后调用cancel

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}
func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
    // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}
func main() {
    // 设置一个50毫秒的超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}
type TraceCode string

func worker(){
    ...
    key := TraceCode("TRACE_CODE")
    traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code        
    ...
}
func main(){
    ...
    // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
    ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "2009")
    wg.Add(1)
    go worker(ctx)
    ...
}

golang的context设计,让我们明白一个道理,能简单处理好一个问题,就是好的解决方案,没有高贵之分。

json/xml

// 将struct、map转换成json 字符串
json.Marshal(struct|map)

//将json字符串转换成Person结构体
type Person struct{
    ...
}
jsonStr := []byte(`{"age":"18","name":"5lmh.com","marry":false}`)
var p Person
json.Unmarshal(jsonStr,&p)
xml.Marshal(struct|map)
xml.Unmarshal(xmlStr,&p)

MSGPack是二进制的json,性能更快,更省空间

需要安装第三方包:go get -u github.com/vmihailenco/msgpack

```go
msgpack.Marshal(struct|map)
msgpack.Unmarshal(msgpackbinary,&p)
```

reflect反射

反射是指在程序运行期对程序本身进行访问和修改的能力

reflect包封装了反射相关的方法:

获取类型信息:reflect.TypeOf,是静态的

获取值信息:reflect.ValueOf,是动态的

反射可以获取interface类型信息、获取值信息、修改值信息

反射可以查看结构体字段、类型、方法,修改结构体的值,调用方法

  • 空接口结合反射

    可以通过 空接口 可以表示任何参数,利用反射判断参数类型

官方标准库