+3

[Series Golang]3: Tăng tốc - Golang - Struct, Pointer, Receiver, Interface

Series Golang:

Ở bài viết trước, mình chia sẽ về Golang là gì, tại sao nên dùng Golang. Nếu bạn chưa biết và chưa động lực để học về golang thì có thể đọc qua để lấy động lực nhé [Series Golang]1: Golang là gì? Tại sao nên dùng golang?. Mình cũng tổng hợp những kiến thức liên quan tới biến, Map với bài viết [Series Golang]2: Vượt chướng ngại vật - Golang. Các bạn có thể tham khảo nhé. Hôm nay chúng ta sẽ tiếp tục tìm hiểu Golang với chủ đề struct, con trỏ, inteface, methods

1. Pointer(Con trỏ)

Con trỏ là một biến lưu trữ địa chỉ của một biến khác trong bộ nhớ.

Ảnh

Ví dụ: var a *int = &b


func main() {
	b := 255
	var a *int = &b
	fmt.Printf("Type of a is %T\n", a)
	fmt.Println("address of b is", a)
}

Trong ví dụ trên, *int dùng để khai báo biến a thuộc kiêu con trỏ int. Toán tử & dùng để lấy địa chỉ của biến. Biến a được khai báo thuộc kiểu con trỏ, và nó lưu giữ địa chỉ của biến b trong bộ nhớ.

Biến con trỏ khi bạn khởi tạo nhưng không gán giá trị cho nó, thì nó sẽ có giá trị nil.

Ví dụ:

package main

import (
	"fmt"
)

func main() {
	a := 25
	var b *int // biến b sẽ có giá trị nil
	if b == nil {
		fmt.Println("b is", b)
		b = &a
		fmt.Println("b after initialization is", b)
	}
}
// Kết quả in ra màn hình
// b is <nil>
// b after initialization is 0xc0000be000

Khai báo biến con trỏ theo cách rút gọn:

Bạn có thể khai báo biến con trỏ theo cách rút gọn như cách khai báo một biến bình thường mà mình đã giới thiệu ở trên. Cú pháp:

<tên biến> := địa chỉ

Ví dụ:

a := &b

Khai báo biến con trỏ bằng câu lệnh new

Cú pháp:

<tên biến> := new(<type>)

Ví dụ:

size := new(int)
*size = 85

Với kiểu khai báo này, golang sẽ cấp phát một vùng nhớ chứa giá trị 0 cho biến size. Với dòng lệnh *size = 85, vùng nhớ mà biến size tham khảo tới sẽ được gán giá trị 85

Các điều cần lưu ý khi sử dụng con trỏ

func main() {
	b := 255
	a := &b
	fmt.Println("address of b is", a)
	fmt.Println("value of b is", *a)
	*a++
	fmt.Println("new value of b is", b)
}
// kết quả in ra màn hinh
// address of b is 0xc000128000
// value of b is 255
// new value of b is 256

Biến con trỏ sẽ lưu trữ địa chỉ của một vùng nhớ. Với ví dụ trên, biến a sẽ lưu trữ địa chỉ vùng nhớ của biến b. Mặc dù ab là hai biến tách biệt, nhưng chúng đều trỏ tới một vùng nhớ, vậy nên khi bạn thay đổi giá trị biến a thì giá trị biến b cũng thay đổi tương ứng

Truyền biến con trỏ vào function: chúng ta có thể thay đổi giá trị của biến đó thông qua function đó.

package main

import (  
    "fmt"
)

func change(val *int) {  
    *val = 55
}
func main() {  
    a := 58
    fmt.Println("value of a before function call is",a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}
// kết quả in ra màn hình
// value of a before function call is 58  
// value of a after function call is 55 

Trong ví dụ này, biến con trỏ b được truyền vào hàm change theo kiểu tham chiếu, vậy nên khi biến val thay đổi giá trị trong hàm change, thực chất, vùng nhớ của biến b thay đổi giá trị. Điều đó dẫn tới việc hàm change có thể thay đổi được giá trị của biến b

2. Struct

Nếu các bạn đã từng sử dụng C++ có lẽ đã quá quen với struct. Struct là kiểu do người dùng tự định nghĩa, là tập hợp nhiều trường có thể có cùng kiểu hoặc khác kiểu nhau. Struct được dùng với một nhóm các dữ liệu được tập hợp thành một đơn vị có ý nghĩa, dễ dàng cho việc quản lí. Giả sử bạn cần quản lí nhân viên, nhân viên có các thông tin như tên, tuổi, ngày sinh... Bạn sẽ dùng từng biến riêng lẻ để quản lí, phần code sẽ rất cồng kềnh, khó quản lí. Vì vậy, chúng ta có thể sử dụng struct để gom các thông tin liên quan tới nhân viên thành một biến, và sử dụng các thuộc tính của biến đó theo cách mình mong muốn.

Khai báo struct

Để khai báo struct, chúng ta có thể dùng cú pháp sau:

type <Name> struct {
	// các thuộc tính và kiểu 
}

Ví dụ:

type Employee struct {  
    firstName string
    lastName  string
    age       int
}

Sử dụng struct

Bạn có thể sử dụng struct như ví dụ sau:

type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}

func main() {

    //creating struct specifying field names
    emp1 := Employee{
        firstName: "Sam",
        age:       25,
        salary:    500,
        lastName:  "Anderson",
    }

    //creating struct without specifying field names
    emp2 := Employee{"Thomas", "Paul", 29, 800}

    fmt.Println("Employee 1", emp1)
    fmt.Println("Employee 2", emp2)
}

Kết quả:

Employee 1 {Sam Anderson 25 500}  
Employee 2 {Thomas Paul 29 800}  

Truy xuất một trường trong struct

Để truy xuất một trường trong struct, bạn có thể dùng cú pháp <tên biến struct>.<trường>

Ví dụ: để truy xuất trường firstName của emp1 ở bên trên, mình sẽ dùng emp.firstName

Anomymous structs

Ngoài việc khai báo một struct cụ thể, bạn có thể tạo ra một struct ẩn danh như ví dụ sau:

func main() {  
    emp3 := struct {
        firstName string
        lastName  string
        age       int
        salary    int
    }{
        firstName: "Andreah",
        lastName:  "Nikola",
        age:       31,
        salary:    5000,
    }

    fmt.Println("Employee 3", emp3)
}

Với kiểu khai báo struct này, bạn không thể sử dụng lại việc khai báo struct. Nếu bạn muốn tạo ra một emp4 có cùng kiểu với emp3, bạn phải khai báo lại kiểu như cách emp3 được khai báo.

Lưu ý:

Khi một biến struct được đinh nghĩa mà không có giá trị khởi tạo, giá trị của các trường sẽ là giá trị mặc định. Ví dụ:


type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}

func main() {  
    var emp4 Employee //zero valued struct
    fmt.Println("First Name:", emp4.firstName)
    fmt.Println("Last Name:", emp4.lastName)
    fmt.Println("Age:", emp4.age)
    fmt.Println("Salary:", emp4.salary)
}
// kết quả in ra màn hình:
// First Name:  
// Last Name:  
// Age: 0  
// Salary: 0 

3. Methods

Methods trong Go là gì? Golang không hỗ trợ class, khi lập trình viên muốn quản lí một đối tượng, họ có thể sử dụng struct để thay thế cho class. Tuy nhiên, struct chỉ hỗ trợ quản lí các thuộc tính, còn các methods của class thì struct không hỗ trợ. Làm việc với Golang bạn không cần lo lắng về việc đó.

Methods là một function được khai báo cho một kiểu dữ liệu riêng biệt, kiểu dữ liệu này gọi là receiver, receiver nằm giữa từ khóa func và tên phương thức. Kiểu receiver có thể là kiểu struct hoặc non-struct
Cú pháp:

func (t Type) methodName(parameter list) { 
    // body của method 
}

Ví dụ:

type Employee struct {
	name     string
	salary   int
	currency string
}

/*
 displaySalary() method has Employee as the receiver type
*/
func (e Employee) displaySalary() {
	fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
	emp1 := Employee{
		name:     "Sam Adolf",
		salary:   5000,
		currency: "$",
	}
	emp1.displaySalary() //Calling displaySalary() method of Employee type
}

Ở ví dụ trên, displaySalary chính là một methods của struct Employee. Sau khi khai báo methods, chúng ta có thể gọi methods bằng cách emp1.displaySalary()

So sánh methods và functions:

Ở ví dụ trên, chúng ta có thể chuyển methods thành functions theo cách sau:

type Employee struct {
	name     string
	salary   int
	currency string
}

/*
 displaySalary() method converted to function with Employee as parameter
*/
func displaySalary(e Employee) {
	fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
	emp1 := Employee{
		name:     "Sam Adolf",
		salary:   5000,
		currency: "$",
	}
	displaySalary(emp1)
}

Chúng ta có thể dùng function bình thường để thay thế cho methods. Vậy tại sao chúng ta lại sử dụng methods thay vì sử dụng function bình thường? Để trả lời cho câu hỏi này, chúng ta có hai lí do chính sau:

  • Lí do thứ nhất: Vì Golang không phải là ngôn ngữ hướng đối tượng, vậy nên chúng ta phải sử dụng một số thứ thay thế như struct để quản lý các thuộc tính, và methods để quản lí các methods trong class.
  • Lí do thứ hai: Methods có thể định nghĩa trùng tên nhau nhưng khác kiểu. Function lại không hỗ trợ điều đó.

Pointer receivers và value receivers:

Golang hỗ trợ cả value receiver(vd: func (e Employee) changeName...) và pointer receiver(vd: func (e *Employee) changeAge...). Các ví dụ về methods ở trên là value receiver.

Ví dụ về pointer receiver và value receiver:

package main

import (
	"fmt"
)

type Employee struct {
	name string
	age  int
}

/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
	e.name = newName
}

/*
Method with pointer receiver
*/
func (e *Employee) changeAge(newAge int) {
	e.age = newAge
}

func main() {
	e := Employee{
		name: "Mark Andrew",
		age:  50,
	}
	fmt.Printf("Employee name before change: %s", e.name)
	e.changeName("Michael Andrew")
	fmt.Printf("\nEmployee name after change: %s", e.name)

	fmt.Printf("\n\nEmployee age before change: %d", e.age)
	e.changeAge(51)
	fmt.Printf("\nEmployee age after change: %d", e.age)
}

Kết quả in ra màn hình là:

Employee name before change: Mark Andrew
Employee name after change: Mark Andrew

Employee age before change: 50
Employee age after change: 51

Sự khác nhau giữa pointer receiver và value receiver là gì?

Pointer receiver giống với con trỏ được truyền vào function theo kiểu tham chiếu, trong methods chúng ta có thể thay đổi được giá trị của receiver. Đối với value receiver, chúng ta chỉ có thể truy cập giá trị, nhưng không thể thay đổi được giá trị của nó. Khi muốn thay đổi giá trị receiver bên trong methods, bạn có thể dùng pointer receiver. Khi methos chỉ muốn truy cập giá trị của receiver, không làm thay đổi gì giá trị của receiver, bạn có thể dùng value receiver.

4. Interface

Định nghĩa: Interface là tập hợp các khai báo phương thức, khi một kiểu dữ liệu định nghĩa tất cả phương thức trong interface thì nó được gọi là implement interface đó.

Có thể hiểu đơn giả là interface là một đại diện, được dùng cho trường hợp các kiểu dữ liệu có một vài phương thức chung, và chúng ta muốn sử dụng phương thức chung đó. Ví dụ về interface:

package main

import (  
    "fmt"
)

//interface definition
type VowelsFinder interface {  
    FindVowels() []rune
}

type MyString string

//MyString implements VowelsFinder
func (ms MyString) FindVowels() []rune {  
    var vowels []rune
    for _, rune := range ms {
        if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
            vowels = append(vowels, rune)
        }
    }
    return vowels
}

func main() {  
    name := MyString("Sam Anderson")
    var v VowelsFinder
    v = name // possible since MyString implements VowelsFinder
    fmt.Printf("Vowels are %c", v.FindVowels())

}

Ở ví dụ này, Interface VowelsFinder có phương thức là FindVowels. Sau đó kiểu MyString định nghĩa methods FindVowels. Chúng ta có thể hiểu MyString là một hiện thực(implement interface) của VowelsFinder. Lúc đó, trong hàm main, biến v có kiểu là interface VowelsFinder. Sau đó biến v được gán giá trị bằng biến name. Điều này được chấp nhận, vì biến name là một hiện thực(implement) của VowelsFinder

Chúng ta hãy cùng đến với một ví dụ về tính lương của nhân viên. Nhân viên sẽ có nhiều loại như nhân viên hợp đồng, nhân viên lâu năm..., mỗi loại sẽ có một cách tính lương khác nhau. Ví dụ: chúng ta có hai loại nhân viên là PermanentContract. Cách tính lương mỗi loại sẽ khác nhau. Bây giờ, chúng ta cần tính tổng lương phải trả cho tất cả nhân viên.


type Permanent struct {  
    empId    int
    basicpay int
    pf       int
}

type Contract struct {  
    empId    int
    basicpay int
}

//salary of permanent employee is the sum of basic pay and pf
func (p Permanent) CalculateSalary() int {  
    return p.basicpay + p.pf
}

//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {  
    return c.basicpay
}

func main() {  
    pemp1 := Permanent{1, 5000, 20}
    pemp2 := Permanent{2, 6000, 30}
    cemp1 := Contract{3, 3000}
    fmt.Printf("Total Expense Per Month $%d", pemp1.CalculateSalary() + pemp2.CalculateSalary() + cemp1.CalculateSalary())
}

Bạn có thể dùng phép tính cộng pemp1.CalculateSalary() + pemp2.CalculateSalary() + cemp1.CalculateSalary() để tính tổng lương. Quá đơn giản với trường hợp số lượng nhân viên nhỏ. Giả sử số lượng nhân viên lớn, mỗi nhân viên là một loại khác nhau, lưu trữ trong một mảng, cách tính tổng lương phức tạp, cần phải gọi một hàm totalExpense để tính tổng lương. Vậy tham số của totalExpense sẽ có kiểu là slice Permanent

totalExpense(s []Permanent) 

hay slice Contract

totalExpense(s []Contract) 

Nếu tham tham số của totalExpense là slice Permanent thì nhân viên có kiểu là Contract sẽ không thể tương thích với totalExpense và ngược lại. Vậy nên chúng ta cần có một kiểu có thể bao quát được PermanentContract

Bên dưới là cách sử dụng interface để giải quyết trường hợp chúng ta đang gặp phải.

package main

import (  
    "fmt"
)

type SalaryCalculator interface {  
    CalculateSalary() int
}

type Permanent struct {  
    empId    int
    basicpay int
    pf       int
}

type Contract struct {  
    empId    int
    basicpay int
}

//salary of permanent employee is the sum of basic pay and pf
func (p Permanent) CalculateSalary() int {  
    return p.basicpay + p.pf
}

//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {  
    return c.basicpay
}

/*
total expense is calculated by iterating through the SalaryCalculator slice and summing  
the salaries of the individual employees  
*/
func totalExpense(s []SalaryCalculator) {  
    expense := 0
    for _, v := range s {
        expense = expense + v.CalculateSalary()
    }
    fmt.Printf("Total Expense Per Month $%d", expense)
}

func main() {  
    pemp1 := Permanent{
        empId:    1,
        basicpay: 5000,
        pf:       20,
    }
    pemp2 := Permanent{
        empId:    2,
        basicpay: 6000,
        pf:       30,
    }
    cemp1 := Contract{
        empId:    3,
        basicpay: 3000,
    }
    employees := []SalaryCalculator{pemp1, pemp2, cemp1}
    totalExpense(employees)

}

Hàm totalExpense nhận tham số là slice SalaryCalculator. SalaryCalculator là interface có phương thức CalculateSalary. PermanentContract cũng có methods CalculateSalary, hai struct này là hiện thực của interface SalaryCalculator. Hàm totalExpense sẽ gọi tới phương thức CalculateSalary tương ứng tùy theo cách hiện thực của nó.

Kiểu interface rất có ích trong việc mở rộng đến bất kì loại nhân viên mới nào mà không cần thay đổi logic code. Bạn chỉ cần định nghĩa phương thức CalculateSalary của loại nhân viên mới đó là có thể hoạt động được.

Switch kiểu

Với kiểu interface, bạn có thể kiểm tra kiểu cụ thẻ của nó, với mỗi kiểu có thể xử lí một cách khác nhau như ví dụ sau:

package main

import (
	"fmt"
)

func findType(i interface{}) {
	switch i.(type) {
	case string:
		fmt.Printf("I am a string and my value is %s\n", i.(string))
	case int:
		fmt.Printf("I am an int and my value is %d\n", i.(int))
	default:
		fmt.Printf("Unknown type\n")
	}
}
func main() {
	findType("Naveen")
	findType(77)
	findType(89.98)
}

Kết quả in ra màn hình:

I am a string and my value is Naveen
I am an int and my value is 77
Unknown type

5. Tổng két

Bên trên là những ghi chú, tổng hợp được trong quá trình mình tự học Golang. Mình tự học chủ yếu ở Golang documentgolangbot.com và nhiều nguồn khác. Vậy nên, nếu bạn muốn tìm đến một nguồn đầy đủ chính thống có thể vào Golang document hoặc golangbot.com. Code mẫu mình cũng tham khảo ở golangbot.com

Hi vọng những tổng hợp của mình có ích cho các bạn.

Ở phần tiếp theo mình sẽ tổng hợp về [Series Golang]4: Golang - Concurrency, Goroutines, Channels


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí