Go語言==Struct

結構體(Struct)

Go語言通過自定義方式形成新的類型,而結構體是類型中帶有成員的複合類型。Go語言使用結構體和結構成員描述真實世界的實體和實體對應的各種屬性。
  • 類型可以被實例化,使用new或&,構造的實例的類型是類型的指針。
  • 結構體成員是由一系列成員變量構成,也成爲‘字段’
  • 字段特性

    • 擁有自己的類型和值
    • 字段名必須是唯一的
    • 字段類型可以也是結構體,甚至是字段所在結構體的類型
  • Go語言中不支持“類”的概念,但結構體與類都是複合結構體,但Go語言中結構體的內嵌配合接口比面向對象具有更高的擴展性和靈活性。
  • Go中不僅結構體可以擁有方法,每種自定義的類型也可以擁有方法

1.定義結構體

type structName struct {
    name1 name1Type
    name2 name2Type
}
  • 類型名,標識結構體的名稱,同一個包中不能重複
  • struct{} 表示結構體的類型。
  • 結構體表示一個點結構,包含X, Y兩個整型分量
type Point struct {
    X int
    Y int
}
  • 同類型的變量也可以寫在一行
type Color struct {
    R, G, B byte
}

2. 實例化結構體-爲結構體分配內存並初始化

  • 結構體的定義是一種內存分佈的描述,只有實例化時纔會真正的分配內存。也就是,實例化後才能使用。
  • 實例化:根據結構體定義的格式創建一份與格式一致的內存區域,結構體實例間的內存是完全獨立的。

2.1 基本的實例化形式

  1. 使用 var 方法實現實例化
// 基本實例化格式
var ins T

// 實例化Point結構
type Point struct {
    X int
    Y int
}

var p Point
p.X = 10    // 使用“.”來訪問結構體的成員變量,賦值方法與普通的一致
p.Y = 20

2.2 創建指針類型的結構體

  • 使用new關鍵字進行實例化,變成指針類型的結構體
// 基本格式
ins := new(T)

// 實例化玩家結構體
type Player struct {
    Name string
    HealthPoint int
    MagicPoint int
}

tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300
Go 語言中爲了方便開發者訪問結構體指針的成員變量,使用了語法糖(Syntactic sugar)技術,將ins.Name 變爲(*ins).Name

2.3 去結構體的地址實例化

  • 對結構體進行“&"取地址操作時,視爲對該類型進行一次new的實例化操作
// 基本格式
ins := &T{}

// 對Command進行指針地址的實例化
type Command struct {
    Name string // 指令名稱
    Var *int    // 指令綁定的變量
    Comment string //指令的註釋
}

var version int = 1

cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"
  • 取地址實例化時最廣泛的結構體實例化方式
func newCommand(name string, varref *int, cooment string) *Command {
    return &Command {
        Name: name,
        Var: varref,
        Comment: comment,
    }
}

cmd = newCommand(
    "version",
    &version,
    "show version",
)

3. 初始化結構體的成員變量

  • 結構體在實例化時可以直接對成員變量進行初始化。初始化有兩種形式:一種是字段"key:value"形式以及多個值的列表形式
  • 鍵值對形式適合多字段結構體,字段少的用列表形式

3.1 使用"鍵值對"初始化結構體

// 基本格式
ins := 結構體類型名 {
    字段1: 字段1的值
    字段2: 字段2的值
    ...
}

// 描述人物關聯
type People struct {
    name string
    
    child *People
}

relation := &People {
    name: "爺爺",
    child: &People{
        name: "爸爸",
        child: &People {
            name: "我",
        },
    },
}

3.2 使用多個值的列表初始化結構體

// 基本格式
ins := 結構體類型名{
    value1,
    value2,
    ...
}

// 地址結構
type Address struct {
    Province string
    City string
    ZipCode int
    PhoneNumber string
}

addr := Address {
    "四川",
    "成都",
    610000,
    "0",
}
fmt.Println(addr)

3.3 初始化匿名結構體

ins := struct {
    //匿名結構體字段定義
    字段1 字段1類型
    字段2 字段2類型
} {
    // 字段值初始化
    name1: value1,
    name2: value2,
} //也可以不初始化

// 示例消息結構
func printMsgType(msg *struct{
    id int
    data string
}) {
    fmt.Printf("%T\n", msg) // 將msg的類型名打印出來
}

func main() {
    msg := &struct {
        id int
        data string
    }{
        1024,
        "hello",
    }
    
    printMsgType(msg)
}
// 輸出:*struct {id int; data string}
// 匿名結構體的類型名是結構體包含字段成員的詳細描述,匿名結構體在使用時需要重新定義,造成大量重複的代碼。因此比較少使用

4. 構造函數-結構體和類型的一系列初始化操作的函數封裝

  • Go語言的類型或結構體沒有構造函數的功能,結構體的初始化過程可以使用函數封裝實現

4.1 多種方式創建和初始化結構體--模擬構造函數重載

type Cat struct {
    Color string
    Name string
}

func NewCatByName(name string) *Cat {
    return &Cat{
        Name: name,
    }
}

func NewCatByColor(color string) *Cat{
    return &Cat{
        Color: color
    }
}

4.2 帶有父子關係的結構體的構造和初始化--模擬父級構造調用

type BlackCat struct{
    Cat
}

// 構造基類
func NewCat(name string) *Cat{
    return &Cat {
        Name: name,
    }
}

// 構造子類
func NewBlackCat(color string) *BlackCat {
    cat := &BlackCat{}
    cat.color = color
    return cat
}

5. 方法

  • 方法是一種作用於特定類型變量的函數,這種特定類型的變量叫做接收器(receiver)。

5.1 爲結構體添加方法

  1. 面向過程實現方法
type Bag struct {
    items []int
}

// 將一個物品放入揹包中
func Insert(b *Bag, itemid int) {
    b.items = append(b.items, itemid)
}

func main() {
    bag := new(Bag)
    
    Insert(bag, 1001)
}
  1. Go語言的結構體方法
type Bag struct {
    items []int
}

func (b *Bag) Insert(itemid int) {
    b.itmes = append(b.items, itemid)
}

func main() {
    b.Insert(1001)
}

5.2 接收器--方法作用的目標

func (接收器變量 接收器類型) 方法名(參數列表) (返回參數) {

函數體

}

  1. 理解指針類型的接收器
  • 指針類型的接收器由一個結構體的指針組成,更接近於面向對象中的this或者self
  • 由於指針的特性,調用方法時,修改接收器指針的任意成員變量,方法結束後,修改都是有效的。
type Property struct {
    value int //屬性值
}

func (p *Property) SetValue(v int) {
    p.value = v
}

func (p *Property) Value() int {
    return p.value
}

func main() {
    p := new(property)
    
    p.SetValue(100)
    
    fmt.Println(p.Value())
}
  1. 理解非指針類型的接收器
  • 當方法作用於非指針接收器時,Go語言在代碼運行時將接受器的值複製一份。非指針接收器獲取接收器的成員值,但修改後無效。
type Point struct {
    X int 
    Y int
}

// 非指針接收器的加方法
func (p Point) Add(other Point) Point {
    return Point{p.X + other.X, p.Y + other.Y}
}

func main() {
    p1 := Point{1, 1}
    p2 := Point{2, 2}
    result := p1.Add(p2)
    
    fmt.Println(result)
}

5.3 二維矢量模擬玩家移動

  1. 實現二維矢量的結構
type Vec2 struct {
    X, Y float32
}

func (v Vec2) Add(other Vec2) Vec2 {
    return vec2{
        v.X + other.X,
        v.Y + other.Y,
    }
}

func (v Vec2) Sub(other Vec2) {
    return Vec2 {
        v.X - other.X,
        v.Y - other.Y,
    }
}

func (v  Vec2) Scale(s float32) Vec2 {
    return Vec2{v.X * s, v.Y * s}
}

func (v Vec2) DistanceTo(other Vec2) float32 {
    dx := v.X - other.X
    dy := v.Y - other.Y
    
    return float32(math.Sqrt(float64(dx*dx + dy*dy)))
}

func (v Vec2) Normalize() Vec2 {
    mag := v.X * v.X + v.Y * v.Y
    if mag > 0 {
        oneOverMag := 1 / float32(math.Sqrt(float64(mag)))
        return Vec2 {v.X * oneOverMag, v.Y * oneOverMag}
    }
    
    return Vec2{0, 0}
}

type Player struct {
    currPos Vec2
    targetPos Vec2
    speed float32
}

func (p *Player) MoveTo(v Vec2) {
    p.targetPos = v
}

func (p *Player) Pos() Vec2 {
    return p.currPos
}

func (p *Player) IsArrived() bool {
    return p.currPos.DistanceTo(p.targetPos) < p.speed
}

func (p *Player) Update() {
    if !p.IsArrived() {
        dir := p.targetPos.Sub(p.currPos).Normalize()
        
        newPos := p.currPos.Add(dir.Scale(p.speed))
        
        p.currPos = newPos
    }
}

func NewPlayer(speed float32) *Player {
    return &Player {
        speed: speed,
    }
}
  1. 處理移動邏輯
p := NewPlayer(0.5)

p.MoveTo(Vec2{3, 1})

for !p.IsArrived() {
    p.Update()
    
    fmt.Println(p.Pos())
}

5.4 爲類型添加方法

  • Go語言可以給任何方法添加方法
  1. 爲基本類型添加方法
type MyInt int

func (m MyInt) IsZero() bool {
    return m == 0
}

func (m MyInt) Add(other int) int {
    return other+ int(m)
}

func main() {
    var b MyInt
    
    fmt.Println(b.IsZero())
    
    b = 1
    fmt.Println(b.Add(2))
}
  1. http包中的類型方法
  • http包中的NewRequest()方法可以創建一個HTTP請求,填充請求中的http頭(req.Header), 再調用http.Client的Do包方法,將傳入的HTTP請求發送出去
func main() {
    client := &http.Client{}
    req, err := http.NewRequest("POST", "http://www.163.com/",
    strings.NewReader("key=value"))
    
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
        return
    }
    
    req.Header.Add("User-Agent", "myClient")
    
    resp, err := client.Do(req)
    
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
        return
    }
    
    data, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(data))
    
    defer resp.Body.Close()
}
  • http.Header的部分定義如下
type Header map[string][]string // Header是一個以字符串爲鍵,字符串切片爲值的映射

func (h Header) Add(key, value string) {    // map爲引用類型,因此(h Header)的非指針接收器,也可以修改map的值
    textproto.MIMEHeader(h).Add(key, value)
}

func (h Header) Set(key, value string) {
    textproto.MIMEHeader(h).Set(key, value)
}

func (h Header) Get (key string) string {
    return textproto.MIMEHeader(h).Get(key)
}
  1. time包中的類型方法
  • time包主要用於時間的獲取和計算
func main() {
    fmt.Println(time.Second.String())
}
// time.Second的定義
const (
    Nanosecond Duration = 1
    Microsecond = 1000 * Nanosecond
    Millisecond = 1000 * Microsecond
    Second = 1000 * Millisecond
    Minute = 60 * Second
    Hour = 60 * Minute
)
// Second的類型爲Duration,而Duartion實際是一個int64的類型,定義如下:
type Duration int64

func (d Duration) String() string {
    ...
    return string(buf[w:])
}

Duration.String可以將Duration的值轉爲字符串

5.5 示例:使用事件系統實現事件的響應和處理

  • GO語言將類型的方法和普通函數視爲一個概念,簡化回調類型的複雜性。
  1. 方法和函數的統一調用

    • 讓一個結構體的方法的參數和一個普通函數的參數完全一樣,也就是方法和函數的簽名一致。然後使用與簽名一致的變量分別賦值方法與函數。
type class struct {
}

func (c *class) Do(v int) {
    fmt.Println("call method do: ", v)
}

func funcDo(v int) {
    fmt.Println("call function do: ", v)
}

func main() {
    var delegate func(int)

    c := new(class)
    delegate = c.Do
    delegate(100)

    delegate = funcDo

    delegate(100)
}
  • 無論是普通函數還是結構體方法,只要簽名一致,與簽名一致的函數變量就可以保存普通函數或結構體方法
  1. 事件系統基本原理

    • 事件系統可以將事件派發者與事件處理者解耦。
    • 事件系統的特性:

      + 能實現事件的一方,根據事件的ID或名字註冊對應的事件
      + 事件發起者,會根據註冊信息通知這些註冊者
      + 一個事件可以有多個實現方響應
  2. 事件註冊

    • 事件系統需要爲外部提供一個註冊入口,註冊入口傳入的事件名稱和對應事件名稱的響應函數,事件註冊的過程就是將事件名稱和響應函數關聯並保存起來

      var eventByName = make(map[string][]func(interface{}))
      
      func RegisterEvent(name string, callback func(interface{})) {
          list := eventByName[name]
      
          list = append(list, callback)
      
          eventByName[name] = list
      }
  3. 事件調用

    • 事件調用方是事發現場,負責將事件和事件發生的參數通過事件系統派發出去。事件註冊時通過事件系統註冊應該響應哪些事件及如何使用回調函數處理這些事情。
    func CallEvent(name string, param interface{}) {
    list := eventByName[name]
    
    for _, callback := range list {
        callback(param)
    }
    }
    1. 使用事件系統

              type Actor struct {
          }
          
          func (a *Actor) OnEvent(param interface{}) {
              fmt.Println("actor event: ", param)
          }
          
          func GlobalEvent(param interface{}) {
              fmt.Println("global event:", param)
          }
          
          func main() {
              a := new(Actor)
              RegisterEvent("OnSkill", a.OnEvent)
          
              RegisterEvent("OnSkill", GlobalEvent)
          
              CallEvent("OnSkill", 100)
          }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章