Golang_05_项目及文件


目录:

通用

项目开发流程说明

程序框架图

当收到业务模块时,分析该模块有多少个文件(类),和各个类之间的调用关系,程序员需要根据架构师要求,进行分层。采用MVC架构,具体百度。

面向对象

将记账软件功能封装到一个结构体中,调用该结构体的方法,来实现记账,显示明细等功能,结构体的名称为FamilyAccount。通过在main方法中,创建一个结构体实例,实现记账功能。

客户管理系统

程序框架图

功能实现-显示主菜单和退出程序

  1. [目录结构]创建customer主程序目录,在内依次构建customer,customerService,customerView文件
  2. [customerModel]根据实际需要,反推分析,创建结构体中的字段,并创建工厂模式函数
  3. [customerService]创建mod文件,引包,其他功能暂不实现
  4. [customerView]显示界面、退出界面、接收用户输入功能实现并封装,main()创建实例调用方法实现主要功能

功能实现-显示客户列表

  1. [customerService]先定义一个初始化函数用来初始化用户,该函数返回切片
  2. [customerView] [main()]创建cusomerView实例,初始化该实例使其拥有各种属性
  3. [customerView]添加字段,类型引用service中的结构体。到此时model(数据)结构体负责各具体字段,service(业务)结构体负责单条数据,view(显示)结构体负责将多条数据拼接成一个整体。
  4. [customerView]创建list()方法,用于获取view结构体中的所有数据输出显示,将自定义格式封装为GetInfo()方法;[customerModel]编写GetInfo()方法,返回string类型用于输出;[customerView]case语句中调用list()方法来输出;

功能实现-添加客户信息

  1. [customerModel]添加NewCustomer2函数(不包含id字段)
  2. [customerService]添加Add方法,接收model定义的字段输出bool类型,将结果追加到现有切片中
  3. [customerView]添加add方法,接收字段整合为NewCustomer2定义的结构体并追加到切片中;[customerView]case语句中调用add()方法来输出;

功能实现-删除客户信息

  1. [customerService]添加FindById方法,用来查找指定id的下标并返回bool值
  2. [customerService]添加Del方法,用来删除。代码逻辑包含FindByID()找到下标后去除,重新赋值
  3. [customerView]添加del方法并做逻辑判断,例如选择Y时会有'删除完成'和'选择的用户不存在'两个选项;[customerView]case语句中调用del()方法来输出;

功能实现-修改客户信息

  1. [customerService]编写Modify方法,接受切片参数,通过FindByID()方法找到下标并修改,返回修改结果
  2. [customerView]编写mondify方法,调用customerService中的Modify方法并根据返回值添加业务逻辑判断

总结

  1. model层于代码最前编写,随后是service层和view层。在基础框架打好后,经常会在service层和view层之间互相切换。不推荐view层直接访问到model层
  2. 若是有其他文件访问到本文件的变量、方法时,使用工厂模式创建构造函数,特别是model层要善用Get和Set
  3. 每个方法都要注意传入的参数(值传递&引用传递),且在调用时注意调用的哪层的方法(经常会出现this.customerService.List()的方法)
  4. 将小功能封装为方法来完成包括但不限于(根据ID找切片下标、返回)的功能,返回值可能是除了bool外的数据类型供其他方法来调用
  5. 在service层中,基本的增删改查功能返回值为bool用于确认功能是否实现,再由view层根据返回的bool值做显示判断。
  6. 问题:service中参数和返回值到底该怎么配?

文件

文件在程序中是以(数据在文件和内存之间经历的路径)的形式来操作的,输入流是文件到内存,输出流是内存到文件。

通过os.Open()打开的文件属于指针类型,file可以叫file对象,file指针,也可以叫file文件句柄。

读文件的实现方式有很多中,例如通过缓存多次读取文件,一次性读取文件,一次性读取文件后再进行分片处理等。

以下为带缓冲的Reader读文件 & 一次性读取文件

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil" 
    "os" 
) 
func main() { 
    // 学习案例 带缓冲的Reader读文件
    file, err := os.Open("D:/MyNutcloud/wlhiot_manage/goproject/src/go_code/object/os/1.txt")
    if err != nil { // 正确打开文件的情况下err为nil,打开失败则err有值        
        fmt.Println("open file err=", err)
    }
    defer file.Close()
    const (
        defaultBufSize = 4096 // 默认的缓冲区为4096
    )     // bufio包见https://pkg.go.dev/bufio@go1.19#Scanner 
    reader := bufio.NewReader(file) // 将文件读取到buffer中
    for {
        str, err := reader.ReadString('\n')
        if err == io.EOF {
            break
        }
        fmt.Print(str)
    }
    fmt.Println("文件读取结束") 
    // 学习案例 一次性读取文件,ReadFile()方法隐式包含打开文件及关闭文件    
    content, err := ioutil.ReadFile("D:/MyNutcloud/wlhiot_manage/goproject/src/go_code/object/os/1.txt")
    // ioutil.ReadFile()方法返回的是[]byte类型,见https://pkg.go.dev/io/ioutil@go1.19#ReadFile
    if err != nil {
        fmt.Println("Read file err=", err)
    }
    fmt.Printf("%s", string(content)) // 需要对[]byte进行显式转换才能输出合适的字符 
}

func OpenFile(name string, flag int, perm FileMode) (*File, error)  // 格式

os.OpenFile是一个更一般性的文件打开函数,它会使用指定的选项(如O_RDONLY,见os包https://pkg.go.dev/os@go1.19#OpenFile)、指定的模式(如0666)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/O,如果出错,错误底层类型的*PathErrrot。

import (
    "bufio"
    "fmt"
    "os"
)
func main() {
    filename := "D:/MyNutcloud/wlhiot_manage/goproject/src/go_code/object/osWrite/1.txt"    
    // 使用读写、创建模式且以0666权限打开指定文件
    file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println("err=", err)
        return
    }
    defer file.Close()
    str := "hello,Gardon\n" 
    buffer := bufio.NewWriter(file) // 创建一个file文件的缓冲区
    for i := 0; i < 5; i++ {
        buffer.WriteString(str) // 往缓冲区中写入字符串
    }
    buffer.Flush() // 将所有缓冲数据写入打开的底层io.file 
}

通过iouils.RearFile和ioutil.WriteFile进行文件读写

package main
import (
    "fmt"
    "io/ioutil"
    "log"
)
func main() {
    file1Path := "D:/MyNutcloud/wlhiot_manage/goproject/src/go_code/project/repetition/demo03/1.txt"
    file2Path := "D:/MyNutcloud/wlhiot_manage/goproject/src/go_code/project/repetition/demo03/2.txt"
    content, err := ioutil.ReadFile(file1Path)
    if err != nil {
        fmt.Println("文件打开失败")
        return
    }
    err = ioutil.WriteFile(file2Path, content, 0666)
    if err != nil {
        log.Fatal(err)
    }
}

官方案例

package main
import (    "bufio"    "fmt"    "os"
)
func main() {
    counts := make(map[string]int) // 定义map用于接收键值
    files := os.Args[1:]           // command的第一个参数(文件名),接收为[]string类型
    if len(files) == 0 {
        countLines(os.Stdin, counts) // 若没有文件参数,则使用终端输入
    } else {
        for _, arg := range files { // 将[]string类型的files转换为string类型的arg
            f, err := os.Open(arg) // Open()方法返回的是*File的指针
            if err != nil {
                fmt.Fprintf(os.Stderr, "dup2: %v\n", err) //Fprint表示向标准错误流打印一条信息
                continue                                  // continue语句直接跳到for循环的下个迭代开始执行
            }
            countLines(f, counts) // 该函数会修改counts map,因为Map是引用类型
            f.Close()
        }
    }
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\t%v\n", n, line, files[0])
        }
    }
}
// 封装了缓存函数,实现根据输入的文件流将计算结果输出给counts map
func countLines(f *os.File, counts map[string]int) { // 将所有输入都由缓存管理,计算完成后由缓存输出
    input := bufio.NewScanner(f) // func NewScanner(r io.Reader) *Scanner,io.Reader既可接收文件指针也能接收os.Strin标准输入
    for input.Scan() {           // func (s *Scanner) Scan() bool,        
        counts[input.Text()]++ // func (s *Scanner) Text() string,        
        // line := input.Text()  输出'hello,Gardon1111'
        // counts[line] = counts[line] + 1  键为'hello,Gardon1111'值默认为0,每一行++一次
    }
        // NOTE: ignoring potential errors from input.Err()
}

命令行参数

os.Args是一个string切片,用来存储所有的命令行参数。第一个参数是程序名称,通过os.Args[1:]来查找。

可以用flag包来解析,来实现参数顺序任意指定。

package main
import (
"fmt"    "flag"
)
func main() {
    // 定义变量用于接受输入的命令行参数
    var user string
    var pwd string
    var host string
    var port int

    // 可以实现通过-u [字段]来指定参数
    flag.StringVar(&user,"u","","用户名,默认为空")   
    flag.StringVar(&pwd,"p","","用户名,默认为空")
    flag.StringVar(&host,"h","","用户名,默认为空")
    flag.IntVar(&port,"P",3306,"端口号,默认为3306")
    // 从os.Args[1:]中解析注册的flag,必须在所有flag都注册好而未访问其值时执行
    flag.Parse()
    fmt.Printf("user=%v,password=%v,host=%v,port=%v\n",user, pwd, host, port) 
}

$ go run main.go -p=123456 --u root --h=127.0.0.1 
user=root,password=123456,host=127.0.0.1,port=3306  // 输出结果,可以有四种方式来指定

JSON

通常程序在网络传输时会先将数据(结构体、map等)序列化成json字符串,到接受方得到json字符串时,再反序列化恢复成原来的数据类型(结构体、map等)。

Color bool `json:"color,omitempty"`  // # omitempty选项表示当该结构体成员为空或零值时,不生成JSON对象

代码见MyNutcloud/wlhiot_manage/goproject/src/go_code/project/repetition/json

序列化

在JSON中,一切皆对象,任何的数据类型都能通过JSON来表示,例如字符串、数字、数组、map、结构体等。在转换结构体的时候,能通过添加tag标签的方式指定转换后的json键名,实现机制为反射。

以下为切片的序列化:

func main(){
    var slice []map[string]interface{}
    var m1 map[string]interface{}
    m1 = make(map[string]interface{})
    m1["name"] = "绿色孩子"
    m1["age"] = 213 
    m1["address"] = "弘扬大厦"
    slice = append(slice, m1)
    var m2 map[string]interface{}
    m2 = make(map[string]interface{})
    m2["name"] = "紫色孩子"
    m2["age"] = 41
    m2["address"] = [2]string{"墨西哥","夏威夷"}
    slice = append(slice, m2)
    data, err := json.Marshal(slice)
    if err != nil {
        fmt.Printf("序列化失败 err=%v",err)
    }
    fmt.Printf("monster序列化后=%v\n",string(data))
}
// 输出'monster序列化后=[{"address":"弘扬大厦","age":213,"name":"绿色孩子"},{"address":["墨西哥","夏威夷"],"age":41,"name":"紫色孩子"}]'

反序列化

在反序列化一个JSON字符串时,要确保反序列化后的数据类型和序列化前的数据类型一致(字段和名称)。

若是要反序列化成一个结构体,则必须先定义结构体的名称。通过定义合适的数据结构,可以选择性地解码JSON中感兴趣的成员。

func main() {
    str := "[{\"address\":\"弘扬大厦\",\"age\":213,\"name\":\"绿色孩子\"},{\"address\":[\"墨西哥\",\"夏威夷\"],\"age\":41,\"name\":\"紫色孩子\"}]"    
    // 可以通过函数来返回string类型的JSON字符串来实现相同效果,就不需要转义符号了    
    var slice []map[string]interface{}
    // Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v.所以必须用指针来接收值
    err := json.Unmarshal([]byte(str),&slice)  // 下列格式是标准反序列化格式,复制粘贴即可
    if err != nil {
        fmt.Printf("反序列化失败 err=%v",err)
    }
    fmt.Printf("反序列化后的Slice=%v\n",slice)
}
// 输出'反序列化后的Slice=[map[address:弘扬大厦 age:213 name:绿色孩子] map[address:[墨西哥 夏威夷] age:41 name:紫色孩子]]'