青训营笔记(1):Go语言入门之基础语法篇| 青训营

153 阅读8分钟

这篇笔记记录本人学习Go语言基础入门的一些收获和体会,我之前主要是写Java比较多,所以对于Go语言这个内嵌C类型的语言,需要认真过一遍基础,好好看书学一遍。

为何是Go?

优势

  • 部署简单:直接编译为机器码,不依赖于其他库,直接运行即可部署
  • 语言层面的并发:充分利用CPU多核
  • 丰富标准库
    • runtime调度机制
    • 高效GC机制
  • 跨平台
  • 完善的工具链
    • 有编译、代码格式化、包管理、代码补充提示、单元测试框架等工具。
    • 大厂领军

Go强项:

  • 云计算基础设施:dockerk8s、etcd、七牛云
  • 基础后端软件:tidb、influxdb、cockroachdb
  • 微服务:go-kit、micro、bilibili、monzo bank
  • 互联网基础设施:以太坊、hyperledger

不足

  • 包管理:大部分第三库托管在GitHub上,私人维护,代码稳定性有风险。
  • 不支持泛型emmm
  • 所有的异常都用Error来处理。无try-cache语法,写Java的不太习惯。
  • 对C的降级处理并非无缝。没有C降级到asm那么完美(序列化问题)

Win11实战搭建Go开发环境

  1. 去官网下载Go的安装包,直接安装。
  2. 下载VsCode,在扩展那边输入go,安装第一个插件。
  3. 切换代理,否则Vscode安装工具会一直失败,当时在这步就卡住了很久。 命令行输入:go env -w GOPROXY=https://goproxy.cn,direct,将GOPROXY换成七牛云的站点。(附:这里direct指的是如果找不到包,就到回源到GitHub下载)
  4. 使用VsCode创建main.go,这个时候会提示要下载一堆工具,确定即可。下好的文件会在C:\\User\\用户名\\go\\bin下。
  5. 下载Code Runner插件,可以直接运行Go程序。

编译运行命令: go build -o main.exe main.go 编译,生成可执行程序

go run main.go 编译+运行

Go基础语法

Hello World

package main

import (
	"fmt"
	"time"
)

func main() {  //语法:
	fmt.Println("hello Go!")
	time.Sleep(1*time.Second)
}

package:表示程序的包名,如果两个.go文件都是main包,那么他们就无需互相导包,编译的时候会在一起编译。

值得注意的是,Go语法要求左花括号和main同行,我直接狂喜!

func abc(){ 正确,,但是func abc() 换行{ 错误。

变量声明

  • 局部变量支持四种方法。
    • var a int
    • var b int = 100
    • 自动推断:var c = 10
    • 省略var:e:= 100
  • 全局变量不支持:=
  • 多变量声明:var xx,yy = 100,"张三"

const那些事儿

  • const()可以定义枚举,枚举中每行会有一个叫iota的值, 每行的iota都会累加1,第一行的iota默认是0
  • iota只能够配合const符号一起使用。平时是不能使用的。
  • 奇怪的语法增加了:下面的每一行会遵循上面行的公式
const(
	BEIJING = 10*iota  //		0
	SHANGHAI		// 		iota = 1  SHANGHAI=10
	SHANTOU			//		iota = 2  SHANTOU=20
)
const(
	a,b = iota+1,iota+2 // iota = 0   a = 1  b= 2
	c,d		// iota = 1  c = 2 d = 3
	e,f		// iota = 2  e = 3 f = 4

	g,h = iota * 2, iota * 3 // iota = 3  g = 6  h=9
	i,k // iota=4  i = 8  k = 12
)

函数

  • 函数定义形式:func 函数名(参数)(返回值参数){}
  • 返回值:
    • 支持多个返回值
    • 返回值可以匿名
    • 可以给有名字的返回值变量赋值
    • 返回值参数是一个局部变量!
//值返回
func fool(a string, b int) int {
	c:= 100
	return c
}
//返回值匿名
func fool2(a string, b int)(int,int){
	return 666,777
}
//返回值具有参数名称
func fool3(a string, b int)(r1 int,r2 int){
	//给有名称的返回值变量赋值
	r1 = 1024
	r2 = 2048
	return 
}
//返回值具有参数名称
func fool4(a string, b int)(r1,r2 int){
	//形参是局部变量,返回值的形参也是局部变量。作用域是整个函数内。
	fmt.Println(r1) // 0
	fmt.Println(r2) // 0
	//给有名称的返回值变量赋值
	r1 = 4096
	r2 = 10086
	return 
}

init函数和import导包

init函数优先调用于main,我的理解是类似于Java的静态代码块。

导包:

  • 导了包必须使用,否则会报错。
  • import _ "lib1" 表示这个包导入但是不使用,匿名别名。
  • import . "lib2" 表示可以不通过.的方式调用,而是直接调用。
  • import mylib2 "lib2" 别名,包长可以用

defer语句

  • defer 会在函数执行结束后执行。
  • 多个def的执行顺序:以堆栈的形式,后进先出。
  • 先调用return,然后调用defer。

切片——动态数组

  • 普通数组根据不同的长度,打印类型是不同的。这就引出了函数传参的问题。
  • 奇怪的语法:Golang的普通数组给函数传递,是值传递!

切片的使用:

  • 初始化:
    • slice1 := []int{1,2,3}
    • 声明但是不分配空间,var slice2 []int
    • slice2 = make([]int, 3)
    • var nums = make([]int,3,5) //长度3,容量5
  • 长度:len(c),容量:cap(c)
    • slice有尾指针,指向长度的最后一个。即使后面还有容量,但是访问是不合法的!
  • 追加元素:nums = append(nums, 1)
    • cap满了会自动扩容。
  • 截取元素:左闭右开。
    • fmt.Println(nums[0:2])
    • fmt.Println(nums[4:])
  • 拷贝切片,底层指向的数组是一样的。改变一个会修改另外一个。s1:=nums[0:2]
  • copy可以讲底层的slice数组一起拷贝.copy(s2,nums)

Map

  • 声明:
    • var myMap1 map[string]string 表示key为string,value也是string
    • myMap1 = make(map[string]string,10) 开辟空间
    • myMap2 :=make(map[int]string)
    • myMap3 :=map[string]string{ "first": "java", "second" : "c++", "third" : "python", }
  • 增加值:
    • myMap1["one"] = "java"
  • 删除:
    • delete(cityMap,"USA")
  • 遍历:
for key,value :=range cityMap{
    fmt.Println("key = " ,key)
    fmt.Println("value = " ,value)
}

OOP

结构体:

type Human struct{
	name string
	sex string
}
func (this *Human) Eat(){
	fmt.Println("Human.eat() 被执行")
}
//main中
	hero := Hero{
		Name:  "张三",
		Ad:    100,
		Level: 1}
	hero.Show()
  • 封装:
    • 成员函数的编写方式,是在函数名前加大括号写类型。有指针和不是指针两种。
    • 类名首字母大写,表示其他包能访问。
    • 属性首字母大写,表示属性可以对外访问。
type Human struct{
	name string
	sex string
}
func (this *Human) Eat(){
	fmt.Println("Human.eat() 被执行")
}
func (this *Human) Walk(){
	fmt.Println("Human.Walk() 被执行")
}
type SuperMan struct{
	Human // SuperMan继承了Human
	level int
}
//重定义父类的方法Eat
func (this *SuperMan) Eat(){
	fmt.Println("SuperMan.Eat() 被执行")
}
//子类定义的新方法
func (this *SuperMan) Fly(){
	fmt.Println("SuperMan.Fly() 被执行")
}
func main(){
	h:= Human{"Ajax","male"}
	h.Eat()
	h.Walk()
	//定义子对象
	s:=SuperMan{Human{"Sonia","female"},18}
	s.Walk() // 调用父类的方法
	s.Eat() //子类重写的方法
	s.Fly() // 子类的独有方法	
}
  • 继承:使用方式是在结构体声明第一行加上父类类名。
type AnimalIF interface {
	Sleep()
	GetColor() string
	GetType() string
}
//具体的类
type Cat struct{
	color string
}
func (this *Cat) Sleep(){
	fmt.Println("Cat is Sleeping.")
}
func (this *Cat)GetType()string{
	return "Cat"
}
func (this *Cat)GetColor()string{
	return this.color
}
//多态
func showAnimal(animal AnimalIF){
	animal.Sleep() 
	fmt.Println("color = ",animal.GetColor())
	fmt.Println("type = ",animal.GetType())
	fmt.Println("===== ===== ===== =====")
}
func main(){
	var animal AnimalIF //接口数据类型,父类指针
	
	animal = &Cat{"Green"} // 绑定子类
	animal.Sleep() //  调用Cat的方法
	
	cat:=Cat{"Green"}
	showAnimal(&cat)
}
  • 多态:
    • 定义接口:type AnimalIF interface {
      • 接口本质是个指针
      • 类必须实现全部接口方法,否则指针无法正确指向。
    • 多态基本要素:有一个父类接口,由子类实现父类全部接口。
    • 父类类型的变量指向子类的具体方法。

interface{}是万能类型

  • interface{}是通用万能类型。
    • int string float32 float64 struct ...都实现了interface
    • 可以用interface{}类型引用任意的数据类型。
  • 区分引用的底层数据类型——类型断言机制
    • value,ok := arg.(string) //判断arg是否是string类型

pair

  • go语言中变量的结构分为type和value。其中type由分为static type和concrete type。
  • 无论怎么赋值,pair的底层不变
package main

import "fmt"

type Reader interface{
	ReadBook()
}

type Writer interface{
	WriteBook()
}

type Book struct{
}

func (this *Book) ReadBook(){
	fmt.Println("Read a Book")
}

func (this *Book) WriteBook(){
	fmt.Println("Write a Book")
}

func main(){
	//b : pair<type:Book, value:book{}地址>
	b := &Book{}
	// r: pair<type, value:>
	var r Reader
	//r : pair<type: Book , value:book{}地址>
	r = b
	r.ReadBook()
	var w Writer
	//w : pair<type :Book, value: book{}地址 >
	w = r.(Writer) //此处断言成功:w和r具体的type是一致的。
	w.WriteBook()

}

反射reflect

反射允许我们看出一个变量的数据类型。

  • 接口1:ValueOf - 获取输入参数接口中数据的值,如果接口为空返回0
  • 接口2:TypeOf - 动态获取输入参数接口中值的类型,如果接口为空返回nil

这里给出使用反射的例子,具体的要看官方文档:

  • 反射获取type:inputType := reflect.TypeOf(arg)
  • 反射获取value inputValue := reflect.ValueOf(arg)
  • 反射获取字段:
    • 获取reflect.Type,得到NumField,进行遍历. field := inputType.Field(i)
    • 得到每个field,数据类型 。field.Type field.Name
    • 通过field有一个Interface()方法得到对应的value value := inputValue.Field(i).Interface()
  • 反射获取方法:
    • method := inputType.Method(i)
    • method.Name\method.Type

Golang反射解析结构体标签Tag

  • 标签的格式:key:value
  • 作用:属性在不同包中的用法可以不同。常见用JSON、ORM。
type resume struct{
	Name string `info:"name" doc:"我的名字"`
	Sex string `info:"sex" doc:"性别"`
}

func findTag(str interface{}){
	tag := reflect.TypeOf(str).Elem()

	for i:=0;i<tag.NumField();i++{
		taginfo := tag.Field(i).Tag.Get("info")
		tagdoc := tag.Field(i).Tag.Get("doc")
		fmt.Println("info : ",taginfo)
		fmt.Println("doc : ",tagdoc)
	}
}

json案例:

  • 导包:encoding/json
type Movie struct{
	Title string `json:"title"`
	Year int	`json:"year"`
	Price int 	`json:"rmb"`
	Actors []string `json:"actors"`
}
func main(){
	movie :=Movie{"我是电影",2004,8,[]string{"Ajax","Cagur"}}
	//encoing: 结构体-->json
	jsonStr,err := json.Marshal(movie)
	if err!=nil{
		fmt.Println("json marshal error",err)
		return
	}
	//Json格式的字符串
	fmt.Printf("jsonstr = %s\n",jsonStr)

	//decoding : 将json --> 结构体
	my_movie :=Movie{}
	err = json.Unmarshal(jsonStr,&my_movie)
	if err!=nil{
		fmt.Println("json unmarshal error",err)
	}
	fmt.Println(my_movie)
}