[TOC]
起步
配置GOPATH
go项目的存放路径
默认创建了= =
如果更改,PATH里./go/bin
也对应修改
项目
程序以package为单位,以package main
中的func main()
为入口
生态
Web框架
- beego 国内
- gin 轻量级
- echo 差不多
- lris 较大
微服务框架
- go kit 微服务工具集
- Istio 一体化 集成式
容器编排
- k8s
- swarm
服务发现
- consul
存储引擎
- etcd 分布式k/v存储 类似redis
- tidb
静态建站
- hugo
中间件
- nsq 消息队列
- zinx 轻量服务器
- leaf 游戏服务器
- grpc RPC框架
- codis redis集群
爬虫框架
- go query
基础语法
导入
import (
"fmt"
"math"
)
import f "fmt"
func main() {
f.Println(add(42, 13)) //重命名标识符
}
函数
func add(x int, y int) int {
return x + y
}
func add(x, y int) int { //省略连续的相同类型
return x + y
}
func swap(x, y string) (string, string) { //多值返回
return y, x
}
func split(sum int) (x, y int) { //相当于提前定义了要返回的值的名称
x = sum * 4 / 9
y = sum - x
return //不带命名的return语句 将会直接返回上面定义了名称的返回值
}
变量
var a, b, c bool //声明了三个bool类型的变量
//声明可以出现在任何位置
func main() {
var i int //声明的变量具有默认值
fmt.Println(i, a, b, c)
}
var i, j int = 1, 2 //声明变量的同时初始化
func main() {
var a b, c = true, false, "no!" //变量可以从初始化中获得类型
fmt.Println(i, j, a, b, c)
}
k := 3 //简洁赋值语句 代替var
//函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。
func test(x int) int {
return x
}
func main() {
x := 1.2
fmt.Println(test(int(x))) //不同类型赋值需要显式转换
}
常量
//与var相对的 声明常量使用关键字const
const Pi = 3.14
// 常量不能用 := 声明
循环、分支
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
func main() {
sum := 1
for sum < 1000 { //Golang中的while
sum += sum
}
fmt.Println(sum)
}
if x < 0 { //分支
} else {
}
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim { //在分支判断的前面执行一个表达式
return v
}
return lim
}
func main() { //switch
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os { //没什么好说的吧
case "darwin":
fmt.Println("OS X.")
// break自动提供
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.\n", os)
}
}
func main() { // if-else if-...-else
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
//defer关键字
var s string
func getWorld() string {
s = "world"
return s
}
func main() {
defer fmt.Println(getWorld()) //将函数推迟到本层函数执行完毕后执行,但是参数表达式会立即完成
fmt.Printf("在上面那个函数真正调用之前,其参数表达式已经完成,即调用getWorld()过后,s=%s\n", s)
fmt.Println("hello")
}
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i) //被defer的函数会被压入一个栈中,依次调用
}
fmt.Println("done")
}
指针
var p *int
i := 42
p = &i
fmt.Println(*p)
*p = 21
结构体
type Vertex struct {
X int
Y int
}
func main() {
a := Vertex{1, 2}
fmt.Println(a)
}
func main() {
p := &Vertex{1, 2} //声明并初始化一个结构体指针
p.X = 1e9 //隐式(*p).X
fmt.Println(v)
}
v := Vertex{X: 1} // 只显式地给部分值 Y:0 被隐式地赋予
数组
var a [10]int //声明
primes := [6]int{2, 3, 5, 7, 11, 13} //声明并初始化
//类型不可省略
//显式初始化时数量可以省略 省略后是切片(实际上是创建一个数组,然后构建一个引用了它的切片)
//长度不可变
切片
a[low : high] //前闭后开
func main() {
a := []int{1, 2, 3, 4}
b := a[0: 2]
c := a[2: 4]
fmt.Println(b, c) //切片像是对数组的引用
for i:=0; i<4; i++{a[i] = 0}
fmt.Println(b, c) //原数组修改后切片的内容也会改动
b[0] = -1
c[0] = -1
fmt.Println(a) //同样对切片的修改也会引发原数组内容的变化
}
/*
切片拥有 长度 和 容量
len() cap()
长度:切片所包含元素的个数
容量:他的第一个元素开始,到被引用的数组的最后一个元素的个数
**/
s := []int{2, 3, 5, 7, 11, 13} //创建一个对长度为6的数组的切片
s = s[:0] //截取切片使其长度为0 长度变化 容量不变
s = s[:4] //拓展其长度 长度变化 容量不变
s = s[2:] //舍弃前两个值 长度变化 容量变化
// 超出容量发生runtimeerro
//a := make([]type, len, cap) 用make创建切片(动态数组)
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
} //二维切片
/*
向切片追加新元素
func append(s []T, vs ...T) []T
arg1: 被追加的切片
arg2: 追加进去的元素
当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
**/
func main() {
var s []int
printSlice(s)
// 添加一个空切片
s = append(s, 0)
printSlice(s)
// 这个切片会按需增长
s = append(s, 1)
printSlice(s)
// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
}
//切片遍历
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
// pow := []int{...}
func main() {
for i, v := range pow { //rang pow返回两个值,第一个是下标,第二个数元素副本,不需要的用_代替
fmt.Printf("2**%d = %d\n", i, v)
}
}
映射
//声明一个string到Type的映射
var m map[string]Type
//用make创建一个映射
m = make(map[string]Type)
//声明并初始化一个映射
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
//省略类型
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
//增、改
m[key] = val
//查
elem = m[key]
//删
delete(m, key)
//检测键是否存在值
elem, ok = m[key]
函数变量化
函数可以作为变量、参数、返回值进行传递
函数的闭包
什么是闭包?存在自由变量的函数就是闭包。
自由变量与约束变量?
func liner(a, b int) func(int) int {
return func(x int){
return x * a + b
}
}
//对于被返回的闭包可以说 x 是受限制的约束变量,a b 是自由变量
//且这个闭包与自由变量a、b是绑定的
案例:斐波那契闭包
// 返回一个“返回int的函数”
func fibonacci() func() int {
x1, x2, i := 1, 1, 0
return func() int {
i++
if i == 1 || i == 2{
return 1
}else {
t := x2
x2 += x1
x1 = t
return x2
}
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
方法(带接收者的函数)
接收者能够通过.
调用这个函数
type Rectangle struct {
X, Y float64
}
func (v Rectangle) Area() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Rectangle{3, 4}
fmt.Println(v.Area())
}
接收者只能是同一个包内定义的类型
func (f float64) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}//×
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}//√
方法对接收者的副本进行操作,通过设置指针接收者来使其作用于原本的变量
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
函数对形参和指针敏感;
指针重定向:
-
指针为接收者的方法的调用对象可以使值也可以是指针,对于值实际上的调用为
(&v).fun()
;- -
值为接收者的方法的调用对象可以是值也可以是指针,对于指针实际上的调用为
(*v).fun()
;
接口
接口是一组方法签名的集合
//eg
type Abser interface {
Abs() float64
}
//完整实例
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f := MyFloat(-math.Pi)
v := Vertex{3, 4}
a = f // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser
// 下面一行,v 是一个 Vertex(而不是 *Vertex)
// 所以没有实现 Abser。
//a = v 报错
fmt.Println(a.Abs())
}
type MyFloat float64 //拥有接口中相同签名方法,故被视为Abser的实现
func (f MyFloat) Abs() float64 { //被视为对Abser中Abs()的实现
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 { //同上
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
类型通过实现一个接口中所有方法来实现该接口,即使你并没有import那个包,故Go没有implements
关键字
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
上句,理解不能
接口可以像值一样传递,接口值调用方法会调用其具体实现的方法,在内部接口值看做元组(value, type)
接口值可以接受nil
,对接收了nil
的接口调用方法实际上也就是对nil
尝试引用,会产生空指针异常。但是奇妙的是接收了nil
的接口本身不为nil
,所以空指针预防就只能在具体实现的方法中做。
为nil
的接口,可以看成元组是空的,调用产生空指针异常
空接口 type interface {}
,因为任何类型都至少实现了0个方法,故空接口用来接收任何类型的值,类似Object
type X interface {}
func main() {
var i X
describe(i) //(<nil>, <nil>)
i = 42
describe(i) //(42, int)
i = "hello"
describe(i) //(hello, string)
}
func describe(i X) {
fmt.Printf("(%v, %T)\n", i, i)
}
类型断言提供了访问接口值的具体类型的途径
t := i.(T) // 该语句断言接口值i的具体实现是T,并将这个接口值转型具体变量t
断言失败会产生panic
,可通过 t, ok := i.(T)
获得判断是否断言成功的句柄,且失败不会产生panic
类型选择,类型断言和switch
的组合
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
一些特殊的接口:
-
type Stringer interface{ String() string }
这个接口属于包fmt
,实现这个方法用以支持许多描述类型的调用//example package main import "fmt" type IPAddr [4]byte // TODO: 给 IPAddr 添加一个 "String() string" 方法 func (v *IPAddr) String() string { return fmt.Sprintf("%v.%v.%v.%v", v[0], v[1], v[2], v[3]) } func main() { hosts := map[string]IPAddr{ "loopback": {127, 0, 0, 1}, "googleDNS": {8, 8, 8, 8}, } for name, ip := range hosts { fmt.Printf("%v: %v\n", name, ip) } }
-
type error interface{ Error() string }
,这个接口属于内建接口,实现这个接口,来获得错误信息//example package main import ( "fmt" "math" ) type ErrNegativeSqrt float64 func (number ErrNegativeSqrt) Error() string { //递归调用死循环,由于number实现了Error(),fmt在解释时将其视作error,因此调用了number.Error(),从而出现了递归调用,最终导致栈溢出 //下意识地准备用引用,麻了 //return fmt.Sprintf("cannot Sqrt negative number: %v", number) return fmt.Sprintf("cannot Sqrt negative number: %v", float64(number)) } func Sqrt(x float64) (float64, error) { if x >= 0 { return math.Sqrt(x), nil }else { return 0, ErrNegativeSqrt(x) } } func main() { fmt.Println(Sqrt(2)) fmt.Println(Sqrt(-2)) }
-
用
io.Reader
包装io.Reader
package main import ( "io" "os" "strings" ) func rot13(b byte) byte { switch { case 'A' <= b && b <= 'Z': b = (b - 'A' + 13) % 26 + 'A' case 'a' <= b && b <= 'z': b = (b - 'a' + 13) % 26 + 'a' } return b } type rot13Reader struct { r io.Reader } func (rotR *rot13Reader) Read(bytes []byte) (int, error){ n, e := rotR.r.Read(bytes) for i, value := range bytes{ bytes[i] = rot13(value) } return n, e } func main() { s := strings.NewReader("Lbh penpxrq gur pbqr!") r := rot13Reader{s} _, _ = io.Copy(os.Stdout, &r) }
-
image
接口package image type Image interface { ColorModel() color.Model Bounds() Rectangle At(x, y int) color.Color }
多线程
goroutine
——> go fun()
启动一个新的Go程并执行fun()
Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。 ? 不同的地址空间就不需要同步吗?
信道是带有类型的管道
ch := make(chan int) // 声明并初始化一个信道
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。从而完成同步
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)//17
go sum(s[len(s)/2:], c)//-5
go sum(s[:], c)//12
x, y, z:= <-c, <-c, <-c // 从 c 中接收
//17进入信道后阻塞之,取出后-5进入信道
带缓冲的信道
ch := make(chan int, 2)
触发deadlock
ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3
测试信道是否被关闭
v, ok := <- ch //若没有值可以接收且信道已被关闭,那么在执行完后ok为false
循环接收
for i := range ch //直到ch被关闭
关闭信道 close(ch)
只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发panic
通常情况下,信道不像文件不需要特地关闭,除非用于逻辑控制
// 这功能有点像yield啊
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
select
挑选一个准备好的信道操作
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
select
默认选择,其他分支都没有准备好,就会触发default
func main() {
tick := time.Tick(100 * time.Millisecond) //每100ms触发一次
boom := time.After(500 * time.Millisecond) //500ms后触发
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
练习:等价二叉查找树
func Travel(t *tree.Tree, ch chan int){
if t == nil {
return
}
Travel(t.Left, ch)
ch <- t.Value
Travel(t.Right, ch)
}
// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int){
Travel(t, ch)
close(ch)
}
// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool{
c1, c2 := make(chan int), make(chan int)
go Walk(t1, c1)
go Walk(t2, c2)
for val1 := range c1{
if val1 != <-c2 {
return false
}
}
return true
}
互斥锁
sync.Mutex
Lock
方法和Unlock
方法defer Unlock()
以确保必定会解锁
练习:web爬虫
/*待更正*/
package main
import (
"fmt"
"sync"
)
type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}
type Crawler struct {
sync.Mutex
fetched map[string]string
}
// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func (crawler *Crawler) Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
// 下面并没有实现上面两种情况:
//defer fmt.Printf("<- Done with %v\n", url)
if depth <= 0 {
return
}
//判断结果是否已经存在
crawler.Lock()
if _, ok := crawler.fetched[url]; ok{
crawler.Unlock()
return
}
//crawler.fetched[url] = "[loading.]"
crawler.Unlock()
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
crawler.Lock()
crawler.fetched[url] = body
crawler.Unlock()
done := make(chan bool)
for _, u := range urls {
go func(url string) {
//Lambda式中函数不能用迭代变量
crawler.Crawl(url, depth-1, fetcher)
done <- true
}(u)
}
//利用channel的阻塞等待函数执行结束
for range urls {
<- done
}
return
}
func main() {
c := Crawler{ fetched: make(map[string]string)}
c.Crawl("https://golang.org/", 4, fetcher)
for url, body := range c.fetched {
fmt.Printf("found: %s %q\n", url, body)
}
}
// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}
// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"errors"
"fmt"
"sync"
)
type Fetcher interface {
// Fetch returns the body of URL and
// a slice of URLs found on that page.
Fetch(url string) (body string, urls []string, err error)
}
// fetched tracks URLs that have been (or are being) fetched.
// The lock must be held while reading from or writing to the map.
// See https://golang.org/ref/spec#Struct_types section on embedded types.
var fetched = struct {
m map[string]error
sync.Mutex
}{m: make(map[string]error)}
var loading = errors.New("url load in progress") // sentinel value
// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
if depth <= 0 {
//fmt.Printf("<- Done with %v, depth 0.\n", url)
return
}
fetched.Lock()
if _, ok := fetched.m[url]; ok {
fetched.Unlock()
fmt.Printf("<- Done with %v, already fetched.\n", url)
return
}
// We mark the url to be loading to avoid others reloading it at the same time.
fetched.m[url] = loading
fetched.Unlock()
// We load it concurrently.
body, urls, err := fetcher.Fetch(url)
// And update the status in a synced zone.
fetched.Lock()
fetched.m[url] = err
fetched.Unlock()
if err != nil {
fmt.Printf("<- Error on %v: %v\n", url, err)
return
}
fmt.Printf("Found: %s %q\n", url, body)
done := make(chan bool)
for i, u := range urls {
fmt.Printf("-> Crawling child %v/%v of %v : %v.\n", i, len(urls), url, u)
go func(url string) {
Crawl(url, depth-1, fetcher)
done <- true
}(u)
}
for i, u := range urls {
fmt.Printf("<- [%v] %v/%v Waiting for child %v.\n", url, i, len(urls), u)
<-done
}
fmt.Printf("<- Done with %v\n", url)
}
func main() {
Crawl("https://golang.org/", 4, fetcher)
fmt.Println("Fetching stats\n--------------")
for url, err := range fetched.m {
if err != nil {
fmt.Printf("%v failed: %v\n", url, err)
} else {
fmt.Printf("%v was fetched\n", url)
}
}
}
// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
func (f *fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := (*f)[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}
// fetcher is a populated fakeFetcher.
var fetcher = &fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}
Go Modules
替代Go Path工作模式,用来创建项目
Go mod命令
命令 | 作用 |
---|---|
go mod init | 生成go.mod文件 |
go mod download | 下载go.mod中指明的依赖 |
go mod tidy | 整理现有的依赖 |
go mod graph | 查看现有的依赖结构 |
go mod edit | 编辑go,mod |
go mod vendor | 导出项目所有依赖到vendor目录 |
go mod verify | 校验一个依赖是否被修改过 |
go mod why | 查看为什么需要依赖某模块 |
go mod 环境变量
GOPROXY
拉取依赖时的镜像站,修改:go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
GOSUMDB
下载校验和检查站,可从上获取GONOPROXY|GONOSUMDB|GOPRIVATE
私有库,不会经过GOPROXY
或GOSUMDB
,GOPRIVATE
会覆盖前2
文件操作
读
file, err := os.Open("")
if err != nil {
//文件打开错误
}
defer file.Close() // 关闭
// 使用字节数组
var buffer [256]byte
n, err := file.Read(buffer)
if err != nil {
//读字节错误
//文件末尾会抛出错误EOF
}
//使用bufio
reader := bufio.NewReader(file)
line, err := reader.ReadString('\n')
//使用ioutil
content, err := ioutil.ReadFile("")
写
func OpenFile(name string, flag int, pernm fileMode) (*file, error) {...}
os.OpenFile("xxx.txt", os.O_RDONLY, 0777)
os.O_RDONLY
: 只读os.O_WRONLY
: 只写os.O_RDWR
: 读写os.O_CREAT
: 创建os.O_TRUNC
: 清空os.O_APPEND
: 追加
file.Write([]byte )
file.WriteString(string )
使用bufio
writer := bufio.NewWriter(file)
writer.WriteString("???") //写入缓冲
writer.Flush() //写入文件
使用ioutil
err := ioutil.WriteFile("xxx", []byte, perm)
if err != nil {...}
获取标准输入
var str string
fmt.Scanln(&str) // 以空白符号截止
reader := bufio.NewReader(os.stdin) //标准输出同理
str, err := reader.ReadString('\n') //指定字符截止