接口

接口    ->     是一种 类型!!! 一种抽象的类型

接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。 

接口类型

在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface是一组method的集合,是duck-type programming的一种体现。
接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。
不关心属性(数据),只关心行为(方法)。

为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。

为什么要使用接口

type Cat struct{}

func (c Cat) Say() string { return "喵喵喵" }

type Dog struct{}

func (d Dog) Say() string { return "汪汪汪" }

func main() {
    c := Cat{}
    fmt.Println("猫:", c.Say())
    d := Dog{}
    fmt.Println("狗:", d.Say())
}

上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代
码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢? 

像类似的例子在我们编程过程中会经常遇到:

比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?

比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?

比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?

Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。
当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。 

接口的定义

Go语言提倡 面向接口编程。

每个接口由数个方法组成,接口的定义格式如下:.

type 接口类型名 interface{
    方法名1( 参数列表1 )  返回值列表1
    方法名2( 参数列表2 )  返回值列表2
    ...
}

解析

接口名:
使用type将接口定义为自定义的类型名。
Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。
接口名最好要能突出该接口的类型含义。

方法名:
当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

洗衣机 例子

// 只要一个类型 他实现了 wash()和 dry() 方法,就称这个类型实现了xiyiji接口
type xiyiji interface{
     wash()
     dry()
}

type Haier struct {
    name string
    price float32
    mode string
}


func (h Haier) wash(){
    fmt.Println("海尔 可以洗衣服")
}

func (h Haier) dry() {
    fmt.Println("海尔 可以甩干")
}

type abc struct {
    name string
}

func (a abc) wash(){
    fmt.Println("abc 可以洗衣服")
}

func (a abc) dry() {
    fmt.Println("abc 可以甩干")
}


func main() {
    var a xiyiji  // 声明一个xiyiji 类型变量 a 是interface类型

    h1 := Haier{    //  实例化了一个Haier结构体对象 -> h1
        name : "hc",
        price: 998.99,
        mode : "滚筒" 
    }
    fmt.Printf("%T\n",h1)   // h1 是Haier结构体的类型
    a = h1  // 接口是一种类型,一种抽象的类型
    fmt.Println(a)

    aaa := abc{
        name: "HC",
    }
    a = aaa
    fmt.Println(a)
}

举个例子:

type writer interface{
    Write([]byte) error
}

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。 

实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

我们来定义一个Sayer接口:

// Sayer 接口
type Sayer interface {
    say()
}
定义dog和cat两个结构体:

type dog struct {}

type cat struct {}
因为Sayer接口里只有一个say方法,所以我们只需要给dog和cat 分别实现say方法就可以实现Sayer接口了。

// dog实现了Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
}

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。 

接口类型变量

那实现了接口有什么用呢?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。

func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
}

值接收者和指针接收者实现接口的区别

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们有一个Mover接口和一个dog结构体。

type Mover interface {
    move()
}

type dog struct {}
// 值接收者实现接口

func (d dog) move() {
    fmt.Println("狗会动")
}

此时实现接口的是dog类型:

func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
    x.move()
}

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为
Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui。 

指针接收者实现接口

同样的代码我们再来测试一下使用指针接收者有什么区别:

func (d *dog) move() {
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
}

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

面试题

请问下面的代码是否能通过编译?

type People interface {
    Speak(string) string
}

type Student struct{}

func (stu *Stduent) Speak(think string) (talk string) {
    if think == "sb" {
        talk = "你是个大帅比"
    } else {
        talk = "您好"
    }
    return
}

func main() {
    var peo People = Student{}
    think := "bitch"
    fmt.Println(peo.Speak(think))
}

 

类型与接口的关系

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,

如下: Mover接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}
dog既可以实现Sayer接口,也可以实现Mover接口。

type dog struct {
    name string
}

// 实现Sayer接口
func (d dog) say() {
    fmt.Printf("%s会叫汪汪汪\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会动\n", d.name)
}

func main() {
    var x Sayer
    var y Mover

    var a = dog{name: "旺财"}
    x = a
    y = a
    x.say()
    y.move()
}

多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求必须由一个move方法。

// Mover 接口
type Mover interface {
    move()
}

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

type dog struct {
    name string
}

type car struct {
    brand string
}

// dog类型实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
    fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了

func main() {
    var x Mover
    var a = dog{name: "大A"}
    var b = car{brand: "大BB"}
    x = a
    x.move()
    x = b
    x.move()
}

上面的代码执行结果如下:

大A会跑
大BB速度70迈

 并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

// WashingMachine 洗衣机
type WashingMachine interface {
    wash()
    dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
    fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
    dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
    fmt.Println("洗刷刷")
}

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
}

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:

type cat struct {
    name string
}

func (c cat) say() {
    fmt.Println("喵喵喵")
}

func (c cat) move() {
    fmt.Println("猫会动")
}

func main() {
    var x animal           // animal 是一个嵌套型的类型接口,可以声明一个变量 吧这个变量的类型设置成  嵌套型的类型接口
    x = cat{name: "花花"}   // 将  创建的 结构体  初始化
    x.move()   // 调用这个嵌套型的接口  即: animal -> Mover -> move()
    x.say()    // 调用这个嵌套型的接口  即: animal -> Sayer -> say()
}

 

空接口 

空接口      ->  可以存储任意类型的变量

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

func main() {
    // 定义一个空接口x
    var x interface{}
    s := "Hello H.c papa"
    x = s
    fmt.Printf("type:%T value:%v\n", x, x)
    i := 100
    x = i
    fmt.Printf("type:%T value:%v\n", x, x)
    b := true
    x = b
    fmt.Printf("type:%T value:%v\n", x, x)
}

 

空接口的应用

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的字典。

// 空接口作为map值
    var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "H.c papa"
    studentInfo["age"] = 18
    studentInfo["married"] = false
    fmt.Println(studentInfo)

类型断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

接口值

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

我们来看一个具体的例子:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

x.(T)

解析

x:  表示类型为interface{}的变量
T:  表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

func main() {
    var x interface{}
    x = "Hello hc papa"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

func justifyType(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Println("unsupport type!")
    }
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只
会增加不必要的抽象,导致不必要的运行时损耗。

 值接收者和指针接收者实现接口的区别

// 实现接口时 使用指针接受者和使用值接受者的区别

type animal interface{
    speak()
    move()
}

type cat struct {
    name string
}

//  方法一  值类型接受者
/// cat 类型 实现 animal的接口
func (c cat) speak(){
    fmt.Println("喵喵喵")
}

func (c cat) move(){
    fmt.Println("猫会动")
}

//  方法二  指针类型接受者
/// cat 类型 实现 animal的接口
func (c *cat) speak(){
    fmt.Println("喵喵喵")
}

func (c *cat) move(){
    fmt.Println("猫会动")
}

func main() {
    var x animal
    // hh := cat{"花花"}  // 值类型
    // x = hh   // 实现animal接口的是 *cat类型, hh此时是一个cat类型
    //  就是看 上班的方法是什么,下面定义的时候就用什么,一般多用指针

    tom := &cat{"aaa"}  // 指针类型,因为有语法糖 会*cat
    x = tom    //  指针
    tom.speak()   // tom -> (*tom).speak()
    tom.move()    // tom -> (*tom).move()
    fmt.Println(x)
}

打开和关闭文件

计算机中的文件是存储在外部介质(通常是磁盘)上的数据集合,文件分为文本文件和二进制文件。

os.Open( ) 函数能够打开一个文件, 返回一个 *File 和一个 err 。
对得到的文件实例调用 close() 方法能够关闭文件。

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()
}

为了防止文件忘记关闭,我们通常使用defer注册文件关闭语句。  

读取文件

file.Read()

基本使用

Read方法定义如下:

func (f *File) Read(b []byte) (n int, err error)

它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回0和io.EOF。

举个例子:

func main() {
    // 只读方式打开当前目录下的main.go文件
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
    defer file.Close()
    // 使用Read方法读取数据
    var tmp = make([]byte, 128)
    n, err := file.Read(tmp)
    if err == io.EOF {
        fmt.Println("文件读完了")
        return
    }
    if err != nil {
        fmt.Println("read file failed, err:", err)
        return
    }
    fmt.Printf("读取了%d字节数据\n", n)
    fmt.Println(string(tmp[:n]))
}

 循环读取

使用for循环读取文件中的所有数据。

func main() {
    // 只读方式打开当前目录下的main.go文件
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
    defer file.Close()
    // 循环读取文件
    var content []byte
    var tmp = make([]byte, 128)
    for {
        n, err := file.Read(tmp)
        if err == io.EOF {
            fmt.Println("文件读完了")
            break
        }
        if err != nil {
            fmt.Println("read file failed, err:", err)
            return
        }
        content = append(content, tmp[:n]...)
    }
    fmt.Println(string(content))
}

bufio读取文件

bufio是在file的基础上封装了一层API,支持更多的功能。

package main

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

// bufio按行读取示例
func main() {
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for {
        line, err := reader.ReadString('\n') //注意是字符
        if err == io.EOF {
            fmt.Println("文件读完了")
            break
        }
        if err != nil {
            fmt.Println("read file failed, err:", err)
            return
        }
        fmt.Print(line)
    }
}

ioutil读取整个文件

io/ioutil包的ReadFile方法能够读取完整的文件,只需要将文件名作为参数传入。

package main

import (
    "fmt"
    "io/ioutil"
)

// ioutil.ReadFile读取整个文件
func main() {
    content, err := ioutil.ReadFile("./main.go")
    if err != nil {
        fmt.Println("read file failed, err:", err)
        return
    }
    fmt.Println(string(content))
}

 

文件写入操作

os.OpenFile()函数能够以指定模式打开文件,从而实现文件写入相关功能。

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    ...
}

name:要打开的文件名         flag:打开文件的模式。 

模式有以下几种:

模式                 含义
os.O_WRONLY         只写
os.O_CREATE         创建文件
os.O_RDONLY         只读
os.O_RDWR           读写
os.O_TRUNC          清空
os.O_APPEND         追加

perm:
文件权限,一个八进制数。
r(读)04,w(写)02,x(执行)01。

Write和WriteString

func main() {
    file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    str := "hello HC"
    file.Write([]byte(str))       //写入字节切片数据
    file.WriteString("hello 小王子") //直接写入字符串数据
}

bufio.NewWriter

func main() {
    file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    writer := bufio.NewWriter(file)
    for i := 0; i < 10; i++ {
        writer.WriteString("hello HC\n") //将数据先写入缓存
    }
    writer.Flush() //将缓存中的内容写入文件
}

 ioutil.WriteFile

func main() {
    str := "hello Hc"
    err := ioutil.WriteFile("./xx.txt", []byte(str), 0666)
    if err != nil {
        fmt.Println("write file failed, err:", err)
        return
    }
}

练习

copyFile

借助io.Copy()实现一个拷贝文件函数。

// CopyFile 拷贝文件函数
func CopyFile(dstName, srcName string) (written int64, err error) {
    // 以读方式打开源文件
    src, err := os.Open(srcName)
    if err != nil {
        fmt.Printf("open %s failed, err:%v.\n", srcName, err)
        return
    }
    defer src.Close()
    // 以写|创建的方式打开目标文件
    dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE, 0644)
    if err != nil {
        fmt.Printf("open %s failed, err:%v.\n", dstName, err)
        return
    }
    defer dst.Close()
    return io.Copy(dst, src) //调用io.Copy()拷贝内容
}
func main() {
    _, err := CopyFile("dst.txt", "src.txt")
    if err != nil {
        fmt.Println("copy file failed, err:", err)
        return
    }
    fmt.Println("copy done!")
}

实现一个cat命令

使用文件操作相关知识,模拟实现linux平台cat命令的功能。

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "os"
)

// cat命令实现
func cat(r *bufio.Reader) {
    for {
        buf, err := r.ReadBytes('\n') //注意是字符
        if err == io.EOF {
            break
        }
        fmt.Fprintf(os.Stdout, "%s", buf)
    }
}

func main() {
    flag.Parse() // 解析命令行参数
    if flag.NArg() == 0 {
        // 如果没有参数默认从标准输入读取内容
        cat(bufio.NewReader(os.Stdin))
    }
    // 依次读取每个指定文件的内容并打印到终端
    for i := 0; i < flag.NArg(); i++ {
        f, err := os.Open(flag.Arg(i))
        if err != nil {
            fmt.Fprintf(os.Stdout, "reading from %s failed, err:%v\n", flag.Arg(i), err)
            continue
        }
        cat(bufio.NewReader(f))
    }
}

日志库 实战
日志库 是一个写日志的库, 因为自带的log过于基础。

日志库需求
1.  支持往文件里面写日志。
2.  支持往终端上打印日志。

// 日志分级别
// DEBUG TRACE INFO  WARRN  ERROR CIRTAL
// 调试  描述  一般   告警   错误   严重

// 设置常量
const (
   DEBUG = iota  // 0
   TRACE         // 1
   INFO          // 2
   WARN          // 3
   ERROR         // 4
   CIRTAL        // 5
)

func getLevelStr(level int)string {
    switch level{
    case 0: 
        return "DEBUG"
    }
    case 1: 
        return "TRACE"
    }
    case 2: 
        return "INFO"
    }
    case 3: 
        return "WARN"
    }
    case 4: 
        return "ERROR"
    }
    case 5: 
        return "CIRTAL"
    }
    default: 
        return "DEBUG"
    }

}

./mylog 目录
file_log.go

package mylog

//  FileLogger 往文件中记录日志的结构体
type FileLogger struct{
    level        int    // 只有大于这个级别的才记录日志
    logFilePath  string
    logFileName  string
    logFile      *os.File  // os 包中 File 类型的指针
}

// NewFileLogger 是一个生成文件日志结构体实例的构造函数
func NewFileLogger(level int, logFilePath, logFileLog string) *FileLogger {
    flObj: =  &FileLogger{
        level:        level,
        logFilePath:  logFilePath,
        logFileLog:   logFileLog,
    }
    flObj.initFileLogger() //  调用下面的初始化文件句柄方法
    return flObj
}

// 专门用来初始化文件日志的文件句柄
func (f *FileLogger)initFileLogger(){
    //  打开 日志文件
    filepath := fmt.Sprintf("%s/%s", f.logFilePath, f.logFileName)
    file, err := os.OpenFile(filepath, os.O_CERATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
       paninc(fmt.Sprintf("open file:%s failed", filepath))
    }
    f.logFile = file
}


// 记录日志
//  调试信息
func (f *FileLogger) Debug(format string, arge ...interface{}) {
    if f.level > DEBUG {   // 如果你设置的日志级别大于当前级别就不用写日志
       return
    }
    fileName, funcName, line := getCallerInfo() 
    // 往文件里写
    // 往哪个文件里面写?
    // 日志的格式要丰富起来 时间 日志级别 哪个文件哪一行哪一个具体的函数 日志信息
    // f.logFile.WriteString(msg)   满足不了要求

    // [2019-08-30 17:56:01] [DEBUG] main.go [4] id 为 1 的用户一直尝试登陆
    
    
    nowStr := time.Now().Format("[2006-01-02 15:04:05.000]")
    format := fmt.Sprintf("%s [%s] [%s:%s] [%d] %s ", 
                     nowStr, getLevelStr(f.level), fileName, funcName, line, format)
                      // 时间 [级别] [文件:函数] [行号] 信息
    fmt.Fprintf(f.logFile, format, args...)
    fmt.Fprintln(f.logFile)     // 加 换行

}

// 一般输出信息
func (f *FileLogger) Info(msg string){
    // 往文件里写
    // 往哪个文件里面写?
    f.logFile.WriteString(msg)
}

// 错误信息
func (f *FileLogger) Error(msg string){
    // 往文件里写
    // 往哪个文件里面写?
    f.logFile.WriteString(msg)
}

 util.go

package mylog

import (
    "runtime"
    "fmt"
)

fucn  getCallerInfo(fileName, funcName string, line int){
    pc, file, line, ok := runtime.Caller(2)  // 0即当前的文件,1 即向上找一层 ,2 即上两层
    if !ok {
        return
    }
    // 根据PC 拿到当前执行的函数名
    funcName = runtime.FuncForPC(pc).Name()
    funcName = path.Base(funcName) // 函数名
    fileName = path.Base(file)     // 文件
    fmt.Println(funcName, fileName, line)
    return 
}

./log 目录
main.go

package main

import "...PATH..."

func main() {
    fl := mylog.NewFileLogger(mylog.DEBUG, "./", "test.log")   // 定义级为debug
    id := 1
    fl.Debug("这是一条 测试DEBUG日志", id)

    // fmt.Println("可以申请IPO")
    // fl.Info("这是一条 一般输出信息日志")  
    // fl.Error("这是一条 错误信息日志")
}

 复习
内容回顾

接口,空接口 是种类型,是一种抽象的类型

是一个规范一个约定一堆方法签名的集合 ,接口名格式 加'er'

type mover interface{
    move()
}

实现接口
实现了改接口规定的所有方法就实现了该接口

空接口
interface{}
任意变量,都可以存到空接口变量中

指针接受者和 值接收者 实现接口的区别

1. 需要修改接受者中值
2. 接受者是拷贝代价比较大的对象
3. 保证一致性,如果有某个方法使用了指针接受者,那么其他方法也应该使用指针接收者

文件操作, 读写。
打开文件

os.Open() // 只读文件 即可

OpenFile() // 针对比较复杂的方法用这个

time包

time.Now().Format("[2006-01-02 15:04:05.000]") // 24小时制
now.Unix() // 时间戳

定律
注释,日志,测试