Golang 进阶用法

[Golang]: 进阶用法

主要介绍在 Golang 中相对进阶的用法,如interface、reflection、Tag。善用这些技巧可以使得程序码更加简洁。ex, 透过 interface 的技巧使得 func 的参数更加有弹性;使用 reflection 进一步资料属於的型态、甚至达到无须知道 type 也能够修改资料; Tag 让你 mapping 资料更加方便。

From personal blog

Golang Interface

Interface{} 型别转换

在Golang 中Interface资料结构是相当重要的,由於 Golang 属於强型别语言,因此在func 中的 parameter 与 return value 时常会因为结构受限造成许多的不方便,然而interface就是来解决问题。(它本身可以是Golang 语言中任一 type 进而解决该问题。) @playground

package main

import (
	"fmt"
	"strconv"
)

type str string

func (s str) String() string {
	return string(s)
}

type Stringer interface {
	String() string
}

func ToString(any interface{}) string {
	if v, ok := any.(Stringer); ok {
		return v.String()
	}
	switch v := any.(type) {
	case int:
		return strconv.Itoa(v)
	case float64:
		return strconv.FormatFloat(v, 'g', -1, 64)
	}
	return "???"
}

func main() {
	var ex int = 1
	fmt.Println(ToString(ex))
	
	var ex2 float64 = 0.1
	fmt.Println(ToString(ex2))

	var ex3 Stringer = str("1")
	fmt.Println(ToString(ex3))
}

如上述程序码,实作ToString 方法时,需要传入各种型态(type)的参数,此时Interface 的弹性就派上用场了,藉由还原的语法进行实作,绕过强型别参数型态固定的问题。

Interface{} 多形

此外在Golang中若要做到 abstract method 的话则也需要 interface,它提供抽象方法的功能,并且可以在compile time 就能排除抽象方法实作上部份错误,ex. 少定义方法等...。换句话说,在Golang中实作多型 须仰赖interface@playground

package main

import (
	"fmt"
)


type Car interface {
	AddOil(gallon int)
	Run()
}

type VovoCar struct {
}

func (v *VovoCar) AddOil(gallon int) {
	fmt.Printf("vovo car add %d gallon\n", gallon)
}

func (v *VovoCar) Run() {
	fmt.Printf("vovo car add run\n")
}

type ToyotaCar struct {
}

func (t *ToyotaCar) AddOil(gallon int) {
	fmt.Printf("Toyota car add %d gallon\n", gallon)
}

func (t *ToyotaCar) Run() {
	fmt.Printf("Toyota car add run\n")
}

func main() {
	var c Car = &VovoCar{}
	var c2 Car = &ToyotaCar{}
	c.AddOil(10)
	c.Run()

	c2.AddOil(100)
	c2.Run()
}

Golang Reflection

Reflection 是一种用於描述程序语言的工具。由於在Golang 中任何型别都可以是一种Interface,因此时常需要与Reflection 进行搭配,故笔者认为是一种Interface的配套工具。主要有三种用法:

package main

import (
	"fmt"
	"reflect"
)

type Example struct {}

func main() {
	fmt.Println(reflect.TypeOf(&Example{}))	// *main.Eaxmple
}

package main

import (
	"fmt"
	"reflect"
)

type Example struct {
	name string
}

func main() {
	val := reflect.ValueOf(&Example{ name: "Example name"}).Interface().(*Example) 
	fmt.Println(val) // &{Example name}
}
  • 将某interface 资料注入到其他interface 中。 @palyground
    • 主要透过Call by Reference 的原理进行修改,因此dest 型态必须为Ptr
    • 搭配reflect.ValueOf.Elem.Field 找出struct filed 的位置进而修改资料。
package main

import (
	"fmt"
	"reflect"
)

type Example struct {
	Id   string
	Name string
}

func ChangeValue(dest interface{}) {
	valDest := reflect.ValueOf(dest)

	for i := 0; i < valDest.Elem().NumField(); i++ {
		if i == 0 {
			valDest.Elem().Field(i).Set(reflect.ValueOf("change_id"))
		} else {
			valDest.Elem().Field(i).Set(reflect.ValueOf("change_name"))
		}
	}
}

func main() {
	destVal := &Example{Id: "test_id", Name: "test_name"}
	fmt.Println(destVal) // &{test_id test_name}
	ChangeValue(destVal)
	fmt.Println(destVal) // &{change_id change_name}
}

Golang Tag

在Golang Struct 资料结构中,可自定义 Tag,有点类似於其他语言的Annotation,例如在判读 Json 的Key 值时,须利用`json:"name"`的方式填入。然而在学习Golang 的初期时常会勿以为 Tag 是不可定义的,因为并不清楚如何取用。然而Reflection 工具此时就得到了一大作用,由於是描述程序语言的工具,因此可透过该工具将Tag 取出。@playground

package main

import (
	"fmt"
	"reflect"
)

type Example struct {
	Id string `json:"id"`
}

func main() {
	jsonExample := &Example{Id: "test"}
	field := reflect.TypeOf(*jsonExample).Field(0)
	fmt.Printf("name: %v ,tag:'%v'\n", field.Name, field.Tag) 
	// name: Id ,tag:'json:"id"'
}

Golang 低阶指标用法 ( uintptr, unsafe.Pointer)

在Golang 语言中除了正规的正常的写法之外,也提供低阶的程序设计模式,如可直接调用Unix 系统的C程序的cgo。但这种方式是不被建议使用的,因为直接调用很容易出现非预期的错误,除非特殊需要。若要Golang 中使用这一类低阶的程序设计,则需宣告import "unsafe"字样,语意上表示从外部汇入unsafe 套件,但实际上是由编译器直接调用隐藏功能。ex: net, syscall, os, runtime 等大多程序设计不太需要用到的。

  • uintptr 属於Golang 基础型态的一种,事实上是 Integer 型别,且可以接各种型态的指标。
  • unsafe.Pointer 是将任何型别的东西转换成*ArbitraryType 而 ArbitraryType 的定义为type ArbitraryType int ,因此实际上也是一种 Integer 型别。

Escape Analysis 演算法

指的是在编译器端,对於程序码编译後,针对指标 (Pointer) 资料结构进行优化,并计算出需要多少的Heap 储存。往往在Golang 中,时常使用到指标类型的结构,然而只要有该函式之外的呼叫,编译器则会预先预先多配发空间,进而触发GC(Grabage Collection)机制。

例如以下图式为例: 单纯透过指标的方式印出整数阵列,正规写法如Listing 1 直接使用外部func 印出,优化做法透过 unsafe 的方式呼叫外部func。

在Golang中使用unsafe 直接调用Compile 的Pointer编译器则不会预先产出Heap,也因此能避免该问题,但坏处是可能会遇到预期之外的错误(警语)。如上图程序码,都只是印出数字,但透过 unsafe pointer 调用的,使用的记忆体大小是预设的50倍,Heap 的大小是 3倍(下图所示)。

Reference

[1] Wang, Cong, et al. "Escape from escape analysis of Golang." Proceedings of the ACM/IEEE 42nd International Conference on Software Engineering: Software Engineering in Practice. 2020.
[2] https://go.dev/blog/laws-of-reflection
[3] https://research.swtch.com/interfaces


<<:  【从零开始的 C 语言笔记】第十八篇-For Loop

>>:  Methods to Login to Your ATT Account

峰禾影视

峰禾影视 峰禾影视电影电视剧频道拥有海量热门经典和2021最新的电影电视剧等,优质影视大片高清免费在...

【Day23】隐写技术 ─ 工具实作篇(二)

哈罗~ 今天要再来介绍另一个隐写工具... 我真的常常都不知道开头要打什麽ಥ︿ಥ 那就..直接开始吧...

Day 22 Ruby include vs extend vs prepend

include vs extend vs prepend include、extend、prepen...

Day 9 - 间距使用方式

相信在座的看倌都知道 marge、padding 是做什麽用的,还不知道的同学可能要自行补一下 C...

Day 09 - Array

Array 是一个有序的集合,可以存储相同类型的元素。 初始化: // 1. init时就有值 NS...