Go语言案例(一)音乐播放器

一、GOPATH 与 Go Mod

(小白学习go,可能存在问题,欢迎大家批评指正~)

最开始,我的项目结构如下:
在这里插入图片描述
结果发现,main.go中 import包tool 爆红,明明路径都写对了,为什么还是报错呢?原来是gopath配置的问题,gopath就是用来管理包的。

于是,我开始配置gopath。
GOPATH官方文档解释是:GOPATH 环境变量指定了你的工作空间位置。它或许是你在开发Go代码时, 唯一需要设置的环境变量。
① 打开设置,发现使用的是全局GOPATH
在这里插入图片描述
但本项目的生成地址并不是全局GOPATH所在的地址,因此需要修改为本项目所在的地址:
在这里插入图片描述
② 既然选择使用gopath,那么就要使用GOPATH的目录
GOPATH目录一般为:

  --bin      # 存放编译后的可执行文件,也就是.exe文件
  --pkg      # 依赖包编译后的*.a文件
  --src      # 存放源码文件,以代码包为组织形式

由此我对项目结构进行了修改,此时导入tool包就不会爆红了
在这里插入图片描述
③ 运行项目
由于是新建的项目,第一次的运行配置尚未添加
在这里插入图片描述
我们不需要手动进行配置,直接找到main函数选择运行即可
在这里插入图片描述
此时他会自动生成配置项
在这里插入图片描述
上述便完成了整个项目在GOPATH下的运行,但是GOPATH存在一些问题,比如

  1. 当我们 go get 第三方包 时,GoPath 会在 GOPATH 路径上安装第三方包,下载下来的包会乱七八糟的放在你的工作路径scr下面,和你自己写的源码混在一起。
  2. 项目结构固定死,bin/pkg/scr必须放在同一个目录下,比如像我前面发生的爆红,一部分原因就是 我的代码必须放在 GOPATH 里,才能运行,所以我才需要把全局GOPATH改为局部GOPATH。

如果我们使用GOPATH将会发生以下情况:

  1. 要么不同的项目使用不同的GOPATH,这个时候大家互不干扰,看上去还挺和谐的,但是如果不同项目使用到了同一个包,那么就是各自放到各自的GOPATH,内存空间占用大,其实这个包可以大家共用的。
  2. 要么不同项目使用同一个 GOPATH,此时解决了重复依赖占用空间的问题,但是不同项目的源码又会混杂在一起,还夹杂着第三方包,简直乱的不行。

但若此时使用go mod来管理包,就会变得异常方便和整洁,而且使用go mod,scr可以不用和bin/pkg放在同一个目录下,能保证在不同地方构建,获得的依赖模块是一致的,也就是说,无论我的go文件在哪里,都可以引用包。
使用go mod 管理项目,就不需要非得把项目放到GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目,都能用了。

go mod就是在 GoPath 之外再弄一个 GoModule 目录(就是 go mod init 的那个目录),这样自己的代码安装在 GoModule 目录里,第三方则安装在 GoPath 目录里。


下面来看看go mod怎么用
Modules官方定义为:

模块是相关Go包的集合。modules是源代码交换和版本控制的单元。
go命令直接支持使用modules,包括记录和解析对其他模块的依赖性。modules替换旧的基于GOPATH的方法来指定在给定构建中使用哪些源文件。

① 首先打开cmd,输入go env查看
在这里插入图片描述
② 设置 GO111MODULE=ON
在这里插入图片描述
③ 打开项目终端,输入go mod init 项目名,创建go.mod
在这里插入图片描述
④ 创建成功
在这里插入图片描述
其实我们完全不用这么麻烦,直接在新建项目的时候,创一个含有mod的项目就可以:
在这里插入图片描述
我原本认为,在我把gopath的项目变成go mod项目之后,我就不需要再管本地项目包导入的问题了,但是我后面发现,即便使用go mod,我在导入tool包时依旧爆红,于是参考了下面两篇文章,但是都不能解决

go mod 怎么导入本地其它项目的包

go mod 如何导入本地的包

最终发现是我路径写错的问题:
如果项目结构长这样,那么调用tool包就应该这样调用:
在这里插入图片描述
在这里插入图片描述
就目前来看,局部GOPATH的设置可以使得我们直接使用 “tool” 导入项目包,而如果使用全局GOPATH,那么我们就要使用相对路径来导入本地包。

二、常用的标准库

(一)ftm


func main() {
	正常打印字符串和变量,不会进行格式化,不会自动换行,需要手动添加 \n 进行换行,多个变量值之间不会添加空格
    fmt.Print("hello", "world\n")
    
    //正常打印字符串和变量,不会进行格式化,多个变量值之间会添加空格,并且在每个变量值后面会进行自动换行
    fmt.Println("hello", "world")
    
	//可以按照自己需求对变量进行格式化打印。需要手动添加 \n 进行换行
    fmt.Printf("hello world\n")
}


helloworld
hello world
hello world

func main() {
    n := 1024
    fmt.Printf("%d 的 2 进制:%b \n", n, n)
    fmt.Printf("%d 的 8 进制:%o \n", n, n)
    fmt.Printf("%d 的 10 进制:%d \n", n, n)
    fmt.Printf("%d 的 16 进制:%x \n", n, n)
}

10242 进制:10000000000
10248 进制:2000
102410 进制:1024
102416 进制:400

type Profile struct {
    name string
    gender string
    age int
}

func main() {
    var people = Profile{name:"wangbm", gender: "male", age:27}
    fmt.Printf("%v \n", people)  // output: {wangbm male 27}
    fmt.Printf("%T \n", people)  // output: main.Profile

    // 打印结构体名和类型
    fmt.Printf("%#v \n", people) // output: main.Profile{name:"wangbm", gender:"male", age:27}
    fmt.Printf("%+v \n", people) // output: {name:wangbm gender:male age:27}
    fmt.Printf("%% \n") // output: %
}

{wangbm male 27}
main.Profile
main.Profile{name:"wangbm", gender:"male", age:27}
{name:wangbm gender:male age:27}
%

func main() {
    fmt.Printf("%t \n", true)   //output: true
    fmt.Printf("%t \n", false)  //output: false
}

func main() {
    fmt.Printf("%s \n", []byte("Hello, Golang"))  // output: Hello, Golang
    fmt.Printf("%s \n", "Hello, Golang")     // output: Hello, Golang

    fmt.Printf("%q \n", []byte("Hello, Golang"))  // output: "Hello, Golang"
    fmt.Printf("%q \n", "Hello, Golang")     // output: "Hello, Golang"
    fmt.Printf("%q \n", `hello \r\n world`)  // output: "hello \\r\\n world"

    fmt.Printf("%x \n", "Hello, Golang")     // output: 48656c6c6f2c20476f6c616e67
    fmt.Printf("%X \n", "Hello, Golang")     // output: 48656c6c6f2c20476f6c616e67  //每个字节两个字符
}

Hello, Golang
Hello, Golang

"Hello, Golang"
"Hello, Golang"
"hello \\r\\n world"

48656c6c6f2c20476f6c616e67 
48656C6C6F2C20476F6C616E67

(二)os/exec

1. 只执行命令,不获取结果
os/exec 用于执行命令的库,exec.Command 函数返回一个 Cmd 对象

package main

import (
    "log"
    "os/exec"
)

func main() {
    cmd := exec.Command("ls", "-l", "/var/log/")
    err := cmd.Run()
    if err != nil {
        log.Fatalf("cmd.Run() failed with %s\n", err)
    }
}

2. 执行命令,并获取结果

package main

import (
"fmt"
"log"
"os/exec"
)

func main() {
    cmd := exec.Command("ls", "-l", "/var/log/")
    out, err := cmd.CombinedOutput()
    if err != nil {
        fmt.Printf("combined out:\n%s\n", string(out))
        log.Fatalf("cmd.Run() failed with %s\n", err)
    }
    fmt.Printf("combined out:\n%s\n", string(out))
}

如果是如下代码:

exec.Command("ls", "-l", "/var/log/*.log")

那么会报错,因为go不认识通配符*,而是将*.log当做一个文件了

三、项目中涉及语法概念

(一)结构体

type MusicEntry struct {  
	Id string
	Name string
	Artist string
	Source string
	Type string
}

type MusicManager struct {
	musics []MusicEntry   //以数组切片作为结构体成员
}
//这里*是重点
func (m *MusicManager) Len() int {
	return len(m.musics)
}

其中Len是方法名,而(m *MusicManager) :表示将 Len 方法与 MusicManager 的实例绑定。我们把 MusicManager 称为方法的接收者,而 m表示实例本身,它相当于 Python 中的 self,在方法内可以使用 m.属性名 的方法来访问实例属性。

结构体赋值:

music1 := &MusicEntry{"1", "My Heart Will Go On", "Celion Dion", "http://xxxxxxxxx", "MP3"}

为什么要用*呢?——(m *MusicManager)
当你想要在方法内改变实例的属性的时候,必须使用指针做为方法的接收者。

至此,我们知道了两种定义方法的方式:

  • 以值做为方法接收者
  • 以指针做为方法接收者

那我们如何进行选择呢?以下几种情况,应当直接使用指针做为方法的接收者。

  • 你需要在方法内部改变结构体内容的时候
  • 出于性能的问题,当结构体过大的时候
  • 结构体往往和指针关联

Go中的结构体当然也可以进行嵌套

type company struct {
    companyName string
    companyAddr string
}

type staff struct {
    name string
    age int
    gender string
    position string
}

嵌套完之后

type staff struct {
    name string
    age int
    gender string
    position string
    company   // 匿名字段!!嵌套重点
}

在 Go 语言中,函数名的首字母大小写非常重要,它被来实现控制对方法的访问权限。

  • 当方法的首字母为大写时,这个方法对于所有包都是Public,其他包可以随意调用

  • 当方法的首字母为小写时,这个方法是Private,其他包是无法访问的。


(二)接口

在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。

Go语言的接口并不是其他语言(C++、Java、C#等)中所提供的接口概念。

  1. 侵入式接口 主要表现在 实现类需要 明确声明 自己实现了某个接口。
  2. 非侵入式接口 ——Go语言使用
    在Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口。

举个栗子
先定义一个商品(Good)的接口,意思是一个类型或者结构体,只要实现了settleAccount() 和 orderInfo() 两个方法,那这个类型/结构体就是一个商品。

type Good interface {
    settleAccount() int
    orderInfo() string
}

然后我们定义两个结构体,分别是手机和赠品。

type Phone struct {
    name string
    quantity int
    price int
}

type FreeGift struct {
    name string
    quantity int
    price int
}

他俩分别实现了Good中的方法:

// Phone
func (phone Phone) settleAccount() int {
    return phone.quantity * phone.price
}
func (phone Phone) orderInfo() string{
    return "您要购买" + strconv.Itoa(phone.quantity)+ "个" +
        phone.name + "计:" + strconv.Itoa(phone.settleAccount()) + "元"
}

// FreeGift
func (gift FreeGift) settleAccount() int {
    return 0
}
func (gift FreeGift) orderInfo() string{
    return "您要购买" + strconv.Itoa(gift.quantity)+ "个" +
        gift.name + "计:" + strconv.Itoa(gift.settleAccount()) + "元"
}

此时就说明,这俩在Go语言看来都是商品

再举个栗子

假设我们有如下接口:每个接口都有各自 必须要实现的方法

type IFile interface { 
 Read(buf []byte) (n int, err error) 
 Write(buf []byte) (n int, err error) 
 Close() error 
} 

type IReader interface { 
 Read(buf []byte) (n int, err error) 
}
 
type IWriter interface { 
 Write(buf []byte) (n int, err error) 
}

type ICloser interface { 
 Close() error 
}

这里我们定义了一个File类,并实现有Read()、Write()、Close()等方法。

type File struct { 
 // ... 
} 
func (f *File) Read(buf []byte) (n int, err error) 
func (f *File) Write(buf []byte) (n int, err error) 
func (f *File) Close() error

那么就说明,File类不仅仅实现了IFile 接口,同时也实现了IReader 、IWriter 、ICloser 接口。
意思就是说,File类就是一个文件,同时File类也是一个阅读器,也是一个编辑器,也是一个关闭器。

尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类实现了这些接口定义的方法,可以进行赋值:

var file1 IFile = new(File) 
var file2 IReader = new(File) 
var file3 IWriter = new(File) 
var file4 ICloser = new(File)

这样的好处在于:
不用为了实现一个接口而导入一个包,因为多引用一个外部的包,就意味着更多的耦合。
接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。


同时,在 Go 语言中,是通过接口来实现的多态。
什么是多态呢? 比如在好学生这个接口下,不同人对好学生的定义不同,有些人认为只要成绩好就是好学生,有些人认为综合发展才是好学生,不同的人有不一样的看法,这就是多态。


接口还可以进行赋值

  1. 将对象实例赋值给接口
var xxx 接口名 = &实例对象

为什么要取地址呢?这是因为Go会将 无 * 转成 有 * 的,反之则不成立
举个栗子

type Integer int
func (a Integer) Less(b Integer) bool { 
 return a < b 
} 
func (a *Integer) Add(b Integer) { 
 *a += b 
} 

//相应地,我们定义接口LessAdder,如下:

type LessAdder interface { 
 Less(b Integer) bool
 Add(b Integer) 
}

Go语言可以根据下面的函数:

func (a Integer) Less(b Integer) bool

自动生成一个新的Less()方法:

func (a *Integer) Less(b Integer) bool { 
 return (*a).Less(b) 
}

而且你赋值的是

var n LessAdder = &a

而不是

var n LessAdder = a

这个时候使用的是&,此时(*a)就可以被读取到。

  1. 将一个接口赋值给另一个接口
    只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等同的。
    如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A。

四、源码附录

main.go

package main
import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
	"tool"
)
var MusicMN *tool.MusicManager
var id int = 1
var ctrl, signal chan int
func handleLibCommands(tokens []string) {
	switch tokens[1] {
	case "list":
		for i := 0; i < MusicMN.Len(); i++ {
			e, _ := MusicMN.Get(i)
			fmt.Println(i+1, ":", e.Name, e.Artist, e.Source, e.Type)
		}
	case "add": {
		if len(tokens) == 6 {
			id++
			MusicMN.Add(&tool.MusicEntry{strconv.Itoa(id),
				tokens[2], tokens[3], tokens[4], tokens[5]})
		} else {
			fmt.Println("USAGE: lib add <name><artist><source><type>")
		}
	}
	case "remove":
		if len(tokens) == 3 {
			MusicMN.Remove(tokens[2])
		} else {
			fmt.Println("USAGE: lib remove <name>")
		}
	default:
		fmt.Println("Unrecognized lib command:", tokens[1])
	}
}

func handlePlayCommand(tokens []string) {
	if len(tokens) != 2 {
		fmt.Println("USAGE: play <name>")
		return
	}
	e := MusicMN.Find(tokens[1])
	if e == nil {
		fmt.Println("The music", tokens[1], "does not exist.")
		return
	}
	tool.Play(e.Source, e.Type)
}

func main() {
	fmt.Println(` 
 Enter following commands to control the player: 
 lib list -- View the existing music lib 
 lib add <name><artist><source><type> -- Add a music to the music lib 
 lib remove <name> -- Remove the specified music from the lib 
 play <name> -- Play the specified music 
 `)
	MusicMN = tool.NewMusicManager()
	r := bufio.NewReader(os.Stdin)
	for {
		fmt.Print("Enter command-> ")
		rawLine, _, _ := r.ReadLine()
		line := string(rawLine)
		if line == "q" || line == "e" {
			break
		}
		tokens := strings.Split(line, " ")
		if tokens[0] == "lib" {
			handleLibCommands(tokens)
		} else if tokens[0] == "play" {
			handlePlayCommand(tokens)
		} else {
			fmt.Println("Unrecognized command:", tokens[0])
		}
	}
}

manager.go

package tool

import "errors"

type MusicEntry struct {
	Id string
	Name string
	Artist string
	Source string
	Type string
}

type MusicManager struct {
	musics []MusicEntry
}

func NewMusicManager() *MusicManager {
	return &MusicManager{make([]MusicEntry, 0)}
}

func (m *MusicManager) Len() int {
	return len(m.musics)
}

func (m *MusicManager) Get(index int) (music *MusicEntry, err error) {
	if index < 0 || index >= len(m.musics) {
		return nil, errors.New("Index out of range.")
	}
	return &m.musics[index], nil
}

func (m *MusicManager) Find(name string) *MusicEntry {
	if len(m.musics) == 0 {
		return nil
	}
	for _, m := range m.musics {
		if m.Name == name {
			return &m
		}
	}
	return nil
}

func (m *MusicManager) Add(music *MusicEntry) {
	m.musics = append(m.musics, *music)
}

func (m *MusicManager) Remove(name string) *MusicEntry {
	var index int
	for i, m := range m.musics {
		if m.Name == name {
			index=i
		}
	}
	removedMusic := &m.musics[index]
	m.musics = append(m.musics[:index],m.musics[index+1:]...)
	return removedMusic
}

manager_test.go

package tool
import (
	"testing"
)
func TestOps(t *testing.T) {
	mm := NewMusicManager()
	if mm == nil {
		t.Error("NewMusicManager failed.")
	}
	if mm.Len() != 0 {
		t.Error("NewMusicManager failed, not empty.")
	}
	m0 := &MusicEntry{
		"1", "My Heart Will Go On", "Celion Dion", "http://xxxxxxxxx", "MP3"}
	mm.Add(m0)
	if mm.Len() != 1 {
		t.Error("MusicManager.Add() failed.")
	}
	m := mm.Find(m0.Name)
	if m == nil {
		t.Error("MusicManager.Find() failed.")
	}
	if m.Id != m0.Id || m.Artist != m0.Artist || m.Name != m0.Name || m.Source != m0.Source || m.Type != m0.Type {
		t.Error("MusicManager.Find() failed. Found item mismatch.")
	}
	m, err := mm.Get(0)
	if m == nil {
		t.Error("MusicManager.Get() failed.", err)
	}
	m = mm.Remove("My Heart Will Go On")
	if m == nil || mm.Len() != 0 {
		t.Error("MusicManager.Remove() failed.", err)
	}
}

MP3.go

package tool
import (
	"fmt"
	"time"
)
type MP3Player struct {
	stat int
	progress int
}
func (p *MP3Player)Play(source string) {
	fmt.Println("Playing MP3 music", source)
	p.progress = 0
	for p.progress < 100 {
		time.Sleep(100 * time.Millisecond) // 假装正在播放
		fmt.Print(".")
		p.progress += 10
	}
	fmt.Println("\nFinished playing", source)
}

play.go

package tool

import "fmt"
type Player interface {
	Play(source string)
}

func Play(source, mtype string) {
	var p Player
	switch mtype {
	case "MP3":p = &MP3Player{}
	default:
		fmt.Println("Unsupported music type", mtype)
		return
	}
	p.Play(source)
}

输入与输出结果:
在这里插入图片描述

参考文档

http://golang.iswbm.com/en/latest/c05/c05_02.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值