心血来潮领略Golang的魅力

心血来潮领略Golang的魅力

来由

  很早就想了解一下Go语言了,但是一直没有行动起来,偶然的情况下看到用更少的代码,更短的编译时间,创建运行更快的程序,享受更多的乐趣并且部署十分方便,可以直接用自带的编译器编译成Linux、Windows下的可执行文件,这个我是真的感觉太方便了,就一行命令,打包成一个可执行文件,不依赖任何类似Java里jdk才能运行jar包,不需要目标电脑有任何go环境,直接就可以运行,像Java如果要打包成 windows 可执行文件(.exe),还需要经过一系列的操作,相比执行是比较麻烦的。

冲着这股好奇心

然后就好奇去看了一下go语言圣经,一发不可收拾,放下手中的活儿,只想看文章,超级棒!😂😂

以下是入门学习的demo,慢慢更新...

test.go

package main

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

//声明变量
var c, python, java bool

//变量的声明及初始化,下面同时声明了h g 并且同时初始化为 3 和 6
var h, g int = 3, 6

//声明变量并且赋值
var (
	q int    = 18
	w bool   = true
	e string = "声明了字符串"
)

//声明一个常量
const t = 78

//定义一个函数
//定义函数的参数的时候,如果参数是指针类型的,传参的时候也必须传指针地址,否则不通过编译
func add(x int, y int) int {
	return x + y
}

//当函数的参数类型相同时,相同参数类型可以在最后一个只写一次
func addTwo(x, y int) int {
	return x + y
}

//函数的多返回值
func swap(x, y string) (string, string) {
	return x, y
}

//给返回值命名
//返回值被命名,它们会被视作定义在函数顶部的变量
func splitName(sum int) (i, j int) {
	//也就是在这个位置初始化了 x y
	i = sum + 10
	j = i / 2
	//返回的时候可以直接写return,默认返回命名了的x y值
	return
}

//变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。
//这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
//每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
func init(){
	fmt.Println("程序开始运行...")
}

//可变参数的函数使用
//可变参数必须位于参数列表的最后。
func listParam(str ...string){
    //这里的str其实就是一个数组
    fmt.Println(str)
}

//可以给可变参数传递数组,例如下面:
// var arr = []int{1,4,6,8}
//传递数组的调用方式为:
// min(arr...)
func min(number ...int){
    //todo 
}

//for循环的使用
func forExp() {
	//go中只有for这一种循环

	//一般
	sum := 0
	for i := 0; i <= 10; i++ {
		sum += i
	}
	fmt.Println("一般用法:", sum)

	//省略 前置和后置语句
	sum2 := 1
	for sum2 < 1000 {
		sum2 += sum2
	}
	fmt.Println("省略前置和后置的循环:", sum2)

	//无限循环(这里演示,就限定为满足一定条件的时候跳出for)
	sum3 := 0
	for {
		sum3 += 1
		if sum3 > 100 {
			break
		}
	}
	fmt.Println("无限循环:", sum3)

	//for循环遍历切片
	//使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本,副本而已,修改副本不会影响到原数据。
	qie := []int{45, 34, 67, 89, 12}
	for index, item := range qie {
		fmt.Println("下标=", index, "副本值=", item)
	}

	//切片循环的时候忽略下标或者忽略值
	//忽略下标
	for _, item := range qie {
		fmt.Println("副本值=", item)
	}
	//忽略值
	for index := range qie {
		fmt.Println("下标=", index)
	}
}

//if的用法
func ifExp(x int) {
	//go中if不需要小括号
	if x == 100 {
		fmt.Println("你传进来了", x)
	} else if x == 200 {
		fmt.Println("你传进来了", x)
	} else {
		fmt.Println("我不懂你传给我什么数字")
	}

	//if条件前面可以有一个简单的语句
	//下面的if中,声明了一个简短的变量v,这个变量只能在if及它的else中使用
	if v := x - 10; v < 100 {
		fmt.Println("你的数太小了,给你减了10只剩", v, "了")
	}
}

//switch的使用
func switchExp(str string) {
	//Go 只运行选定的 case,而非之后所有的 case。
	//Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。
	//Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。
	switch str {
	case "cyh":
		fmt.Println("你传进来的是:", str)
	default:
		fmt.Println("不知道你传了个神马")
	}

	//没有条件的 switch
	//没有条件的 switch 同 switch true 一样。
	//这种形式能将一长串 if-then-else 写得更加清晰。
	//strings.Count(str, "") - 1  是获取字符串str的长度
	len := strings.Count(str, "") - 1
	switch {
	case len == 0:
		fmt.Println("传进来的字符串没有长度!")
	case len == 1:
		fmt.Println("传进来的字符串长度=", len)
	case len == 2:
		fmt.Println("传进来的字符串长度=", len)
	case len == 3:
		fmt.Println("传进来的字符串长度=", len)
	default:
		fmt.Println("懒得理你了,不知道多长!", len)
	}
}

//函数值
//函数也是一种值,可以跟变量、结构体一样,随处传递
func funcExp(f func(int, int) int) {
	fmt.Println("使用传递进来的函数:", f(7, 5))
}

//闭包
func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

//go里的结构体
//大写开头的结构体命名,表示公有的,别的类库可以使用,如果是小写开头,表示私有的,别的类库引用不到它
//go里的结构体只能存放属性,并且不需要var开头,属性名小写开头代表私有变量
//属性名大写开头代表公有变量
//结构体里不能包含函数
type preson struct {
	Name string
	Age  int
}

//go 里没有“类”的概念,结构体应该是最接近“类”的了,但是结构体里面不能写方法,但是外面却可以
//下面给结构体新增一个方法
func (p preson) toString() string {
	return "preson[ Name=" + p.Name + ",Age=" + strconv.Itoa(p.Age) + "]"
}

//上面的结构体函数,是普通的值接受者函数,传进去的p preson是原结构体的副本而已,并不是真实的p preson,如果你对它修改,是不会影响它的原结构体的
//如果需要做到一经修改,原结构体就跟着改,那就需要用到指针接受者的方式
func (p *preson) setMyName(name string) {
	p.Name = name
}

func main() {
	fmt.Println("Hello, 世界")
	fmt.Println("你好狗语言", math.Pi)

	//声明并初始化两个变量 x y ,这种短语声明的方式只能用在函数内部
	//常量不能用 := 语法声明。只能用const
	x := 40
	y := 67
	fmt.Println(x, "+", y, "=", add(x, y))

	//多返回值函数
	fmt.Println(swap("哈哈", "呵呵"))

	//命名返回值
	fmt.Println(splitName(10))

	//for循环的使用
	forExp()

	//if 的使用
	ifExp(100)

	//switch的使用
	switchExp("cyh")

	//defer 推迟执行
	//defer 语句会将函数推迟到外层函数返回之后执行。
	//推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
        //defer会比return更晚执行
        //当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)
	//下面你会发现,总是 world 最后打印出来
	defer fmt.Println("\n---程序运行结束---")
	fmt.Println("hello")

	//指针
	ii, jj := 42, 2701

	pp := &ii        // 指向 ii
	fmt.Println(*pp) // 通过指针读取 ii 的值
	*pp = 21         // 通过指针设置 ii 的值,因为这个时候,pp和ii指向同一个地址,任何一方修改了这个地址上的值,另一方也会跟着被改
	fmt.Println(ii)  // 查看 ii 的值

	pp = &jj        // 指向 jj
	*pp = *pp / 37  // 通过指针对 jj 进行除法运算
	fmt.Println(jj) // 查看 jj 的值

	//结构体的使用
	//结构体实例化
	per := preson{"cyh", 25}
	//使用其里面的属性,使用 x.XX 的形式
	fmt.Println("Name=", per.Name)
	//属性赋值
	per.Age = 26
	fmt.Println("Age=", per.Age)
	//go 里没有“类”的概念,结构体中不能包含函数,但是结构体是可以拥有函数的,只不过这个函数是跟结构体自身平级的
	//调用结构体的函数
	fmt.Println("调用结构体的方法:", per.toString())
	//调用指针型的结构体函数
	per.setMyName("名字改变了吗?")
	fmt.Println("调用指针型的结构体函数后:", per)
	//结构体的实例化示例
	var (
		//常规
		p1 = preson{"tt", 90}
		//指明只对某个属性初始化,其他没有赋值的,数值默认0,字符串默认“”
		p2 = preson{Name: "tt"}
		p3 = preson{}
		//指针结构体
		p4 = &preson{"haha", 78}
	)
	fmt.Println("p1=", p1.Name, "p2=", p2.Name, "p3=", p3.Name, "p4=", p4.Name)

	//数组
	//声明 长度为10 int类型的数组,数组的长度不可再改变!!
	var aar1 [10]int
	//给数组赋值
	aar1[0] = 19
	//从数组中取值
	fmt.Println("aar1第1个元素=", aar1[0])
	//声明并初始化数组
	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println("声明并初始化数组=", primes)

	//切片
	//数组的长度是固定的,不可更改,切片可以很好的解决这个问题
	//切片是以两个下标组成,包括头 不包括尾
	var qie []int = primes[0:4]
	fmt.Println("切片数组:", qie)
	//切片并不存储任何数据,它只是描述了底层数组中的一段。
	//更改切片的元素会修改其底层数组中对应的元素。
	//就是说,切片只是引用了数组的地址,如果改了切片里的值,原数组的值也会被修改
	qie[0] = 1555
	fmt.Println("切片修改了第0个值,现在原数组=", primes, "可以发现,原数组的第0个值也被改变了!")
	//前面也说到了,数组是有固定长度的,而切片可以很好的解决这个问题
	qieAar := []int{2, 4, 6, 7, 3, 9}
	fmt.Println("没有固定长度的切片=", qieAar)
	//向切片追加元素
	qieAar = append(qieAar, 888)
	//同时追加多个
	qieAar = append(qieAar, 999, 1000, 2000)
	fmt.Println("追加后的结果:", qieAar)
	//初始化一个空的切片数组
	testqie := []int{}
	//随后可以满满给它添加值,就像Java里的List
	testqie = append(testqie, 78)
	fmt.Println(testqie)

	//键值映射(类似Map)
	//map[key类型]存储的类型
	var m map[string]preson
	//make 函数会返回给定类型的映射,并将其初始化备用。
	m = make(map[string]preson)
	//赋值
	m["key"] = preson{"啦啦", 60}
	//取值
	fmt.Println("map取值=", m["key"])
	//添加值
	m["add"] = preson{"新增的值", 56}
	//删除某个值
	delete(m, "add")
	//检测某个键是否存在
	//若 key 在 m 中,ok 为 true ,elem为其值;否则,ok 为 false,elem为该映射元素类型的零值。
	//有没有发现这种方式跟取值很像,返回值多了一个是否有值的条件而已
	elem, ok := m["add"]
	fmt.Println("检测某个键是否存在", ok, elem)

	//函数值
	//函数也是一种值,可以跟变量、结构体一样,随处传递
	//把一个函数传进去,传进去的函数不需要写小括号,它只要 参数列表 返回值类型 符合要求即可
	funcExp(addTwo)

	//函数的闭包
	//Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
	//例如下面的:函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}

	//接口
	//todo 待续....

}

紧接着看看go里的接口是如何使用的:
我新建了一个main.go,因为之前的太长了


package main

import (
	"fmt"
)

//定义一个接口
//go 里的接口是一个函数的集合,接口里不能定义属性,只能包含函数
//如果接口名称大写开头,就表示此接口可以导出
// go 里接口的实现都是隐式的,没有类似Java里implements这么明显的实现
//一个结构体只需要包含此接口全部的,记住是全部的函数签名,就会被认为是实现了此接口
//在下面的例子中,USB接口有两个函数,Disk结构体都实现了,就已经表示Disk实现了USB
type USB interface {
	//定义函数,不需要 func 关键字,只需要写函数签名即可
	start()
	end()
}

type Disk struct {
	name string
}

//实现接口的1函数
func (d *Disk) start() {
	fmt.Println("磁盘名称: ", d.name)
}

//实现接口的2函数,到此,USB接口的全部函数都被Disk实现了,在实现的过程USB全程不会出现一个字眼,这就是隐式的实现了USB接口
func (d *Disk) end() {
	fmt.Println(d.name, "磁盘使用结束")
}

//接口的使用,此函数接受参数USB
func diskStart(usb USB) {
	//有一点需要注意的,虽然传进来的实际上是Disk 【看main函数的实现】,但是参数接受的是USB,所以并不能使用Disk里的属性
	//只能调用USB应该有的函数
	usb.start()
}

//空接口的使用
//空接口可以接收任何类型
//空接口被用来处理未知类型的值。
func nilInterface(value interface{}) {
	//所以这里 value可能是任何类型
	fmt.Println(value)
}

func main() {
	fmt.Println("---程序运行---")
	//实例化Disk
	disk := Disk{"移动硬盘"}
	//虽然diskStart接受的是USB接口,但Disk已经实现了USB,就可以当成USB传入,这就是go里但多态
	diskStart(&disk)

	//调用空接口
	nilInterface("这是空接口,可以传任何类型!")
}



要解释的都在注释里了,结合代码看更合适!

go的一些规范或知识

  • 不像Java一样,go的文件都是小写命名,多个单词以下划线连接,例如:hello_world.go
  • 每个项目都必须包含一个package main的主入口文件,即使这个文件不放在main文件夹下。
  • go文件中,大写开头的标识符(包括常量、变量、类型、函数名、结构字段等等),被称为导出标识符,表示别的文件可以引用它,如果是小写的,那只能在当前文件使用。
  • 导入包的时候可以使用别名,例如:import(f "fmt"),此时,就可以使用f.Println()了。
  • go中代码格式都是定死的,当然,编译器会帮你做好,你只管写即可。
  • Go 语言中不存在类型继承,不存在类,目前1.15.3,也没有泛形。
  • Go中的init()函数和main()都是特殊的。
  • Go中,常量定义只能是:布尔型、数字型(整数型、浮点型和复数)和字符串型,结构体是不可以的。
  • 变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShips 和 startDate。但如果你希望你的变量可以被外部引用,那就大写开头。
  • Go中局部变量声明出来必须被使用,否则报错,但全局变量是允许声明但不使用但。
  • 字符串操作:https://learnku.com/docs/the-way-to-go/strings-and-strconv-packages/3588
  • Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
  • Go中对指针地址上的值赋值需要用到*号,例如:*x = 34,表示对x这个指针上的值赋值34。
  • 在Go中,是没有函数重载的,同一个源文件中,每一个函数的名字都必须唯一
  • go中的map,其key 可以是任意可以用 == 或者 != 操作符比较的类型,比如 string、int、float。所以数组、切片和结构体不能作为 key (译者注:含有数组切片的结构体不能作为 key,只包含内建类型的 struct 是可以作为 key 的),但是指针和接口类型可以。如果要用结构体作为 key 可以提供 Key() 和 Hash() 方法,这样可以通过结构体的域计算出唯一的数字或者字符串的 key。其value 可以是任意类型的,没错,是任何类型,包括函数,以及你自己定义的类型。map 是 引用类型 的: 内存用 make 方法来分配。不要使用 new,永远用 make 来构造 map。
  • 新建 go mod 项目,先创建项目目录mkdir mygomod,然后进入项目目录执行 go mod init mygomod 即可。
  • 如果go拉取第三方库的时候被墙了,可以设置代理到https://goproxy.cn/,具体可以访问此站点查看明细。(go原先指向的是GOPROXY="https://proxy.golang.org,direct")

待续....