4 min read

Dari Python Ke Go

Dari Python Ke Go
Photo by wuz / Unsplash
Tulisan ini bukan tentang membandingkan kedua bahasa seperti benchmark performa dan sejenisnya. Tulisan ini hanyalah catatan dari seseorang yang terbiasa menulis Python dan sekarang beralih ke Go.

Typing

Saya bisa dibilang seperti kebanyakan web programmer di Indonesia. Pekerjaan pertama saya adalah PHP – dan otomatis terpapar JavaScript – lalu dilanjutkan dengan Python.
Kedua bahasa tersebut merupakan Dynamic Typing, yang artinya tipe data ditentukan saat runtime, bukan saat deklarasi. Perhatikan kode di bawah:

number = 'string'
character = 123

Deklarasi variabel di atas tidak akan menghasilkan error. Maka, ketika saya pindah ke Go, memang terasa sedikit "kagok" karena setiap variabel perlu dideklarasikan tipe datanya:

var number int
var character string

number = '123'
character = 123

Di Golang, kode di atas tidak akan pernah bisa dijalankan. Awalnya memang terasa kaku, tetapi setelah dipikir ulang, menentukan tipe data—terlepas dari bahasa pemrograman apa pun yang digunakan—pasti dilakukan, setidaknya saat menentukan struktur tabel di database.

Error

Saat menulis kode yang kemungkinan menghasilkan galat (error), dalam Python umumnya kode akan dibungkus ke dalam try-except:

try:
    result = 10 / 0
except ZeroDivisionError:
    print("You can't divide by zero!")

atau secara teori strukturnya akan seperti berikut:

try:
  code
except TypeError:
  handle_error()
finally:
  print("This always runs")

Di Go, error diperlakukan "sebatas value"

kalian bisa baca alasan lebih teknisnya di sini https://go.dev/blog/errors-are-values

Jadi, jika menjalankan sebuah fungsi yang berpotensi menghasilkan error, umumnya kodenya akan seperti berikut:

success, err := SomeFunction()

Struktur kode di atas yang akhirnya menghasilkan "3 line of code" paling populer dalam diskursus Go:

if err != nil {
  // do something
}
bahkan saat ini sedang ada diskusi mengenai error di go https://github.com/golang/go/discussions/71460

Bagi saya, permasalahannya mungkin karena ada banyak pengulangan dalam pengecekan error:

s, err := first()
if err != nil {}
x, err := second()
if err != nil{}
...

Tapi jika permasalahannya hanya visual, penggunaan IDE bisa membantu meminimalisir hal tersebut.

Bagi saya yang sebelumnya banyak menggunakan try-except dan sekarang beralih ke if err != nil {}, perubahan ini mengubah paradigma berpikir. Dalam Python, saya terkadang berpikir:
"Jika error terjadi—terlepas bagian mana pun yang error—saya harus melakukan apa?"

Sedangkan di Go, saya harus berpikir:
"Bagian ini bisa error, saya harus apa? Bagian sana juga kemungkinan error, harus diapakan?"

Sejauh ini, saya masih asik-asik saja menulis if err != nil {}. Entah jika suatu saat saya mencoba bahasa pemrograman lain dan ternyata pengelolaan error-nya bisa lebih menyenangkan.

Function/Method

Terlalu lama menggunakan Django, biasanya terbentuk dua opsi saat menulis sebuah fitur: CBV vs FBV. Di Go, saya menemukan "kesederhanaan" — semua ditulis adalah function atau method.
Jika di Python – Django spesifiknya – lumrah menulis antara dua hal ini:

# fbv
def view_method(request):
    do_something()

#cbv
class ViewApi(Generic):
    do_something()

Saat di Go,cukup:

func someFunc() {
    do_something()
}

Atau menggunakan method

type User struct {
	firstName, lastName string
}

func (u *User) fullName() string {
	return u.firstName + " " + u.lastName
}

func main() {
	user := User{firstName: "John", lastName: "Doe"}
	fmt.Println(user.fullName())
}
type Service struct {
	user lib.UserInterface
	storage lib.StorageInterface
}

func NewService(storage lib.StorageInterface, user lib.UserInterface) *Service {
	return &Service{
		storage: storage,
		user: user,
	}
}

func (s *Service) CreateBlog(ctx context.Context, in *lib.Input) *lib.Output {
	resp := lib.Output{}
	// validation
	if in.title == "" {
		// do something
	}

	// call other service
	user := s.user.Get(ctx, in)
	if user.Success == false {
		// do something
	}

	storage, err := s.storage.Begin(ctx)
	if err != nil {
		log.Warn(in.Trace).Msg(" failed begin tx")
		return resp
	}
	defer storage.Rollback(ctx)
	
	// rest of code

	return &resp
}

Table test

Di Go, sayapun menemukan kenyamanan dalam membuat test. Membuat test di Go bisa sangat sederhana, tapi cukup lengkap jika kita memiliki beragam skenario.

func TestGetUserWithScenario(t *testing.T) {
	t.Parallel()
	con := test.DbTestPool(t)

	// create the storage
	userStorage := NewStorage(con.Pool)

	//prepare the fixtures
	userFixtures := map[int]*data.User{
		1: {
			Fullname: "User Test",
			Username: "testuser",
			Email:    "[email protected]",
			Password: "hashedpassword1",
		},
		2: {
			Fullname: "User Test 2",
			Username: "testuser22",
			Email:    "[email protected]",
			Password: "hashedpassword1",
		},
		3: {
			Fullname: "User Test 3",
			Username: "testuser33",
			Email:    "[email protected]",
			Password: "hashedpassword1",
		},
	}

	//insert the fixtures
	storage, err := userStorage.BeginTxWriter(con.Context)
	assert.Nil(t, err)
	defer storage.Rollback(con.Context)

	// Create test user
	for key, v := range userFixtures {
		id, err := userStorage.InsertUser(con.Context,
			v.Fullname, v.Username, v.Email, v.Password,
		)
		if err != nil {
			t.Fatal("Error creating user fixture:", err)
		}
		userFixtures[key].Id = id
	}

	err = storage.Commit(con.Context)
	if err != nil {
		t.Fatal(err)
	}

	type input struct {
		id int
	}

	type expected struct {
		success bool
		data    *data.User
	}

	type testCase struct {
		name     string
		input    input
		expected expected
	}

	scenarios := []testCase{
		{
			name: "successfully get existing user index 1",
			input: input{
				id: userFixtures[1].Id, // Ensure valid key exists
			},
			expected: expected{
				success: true,
				data:    userFixtures[1], // Using the key for validation
			},
		},
		{
			name: "successfully get existing user index 2",
			input: input{
				id: userFixtures[2].Id, // Valid key for the second user
			},
			expected: expected{
				success: true,
				data:    userFixtures[2], // Using the key for validation
			},
		},
		{
			name: "returns error when user not found",
			input: input{
				id: 999, // Non-existing ID
			},
			expected: expected{
				success: false,
				data:    nil,
			},
		},
	}
	storage, err = userStorage.BeginTxReader(con.Context)
	assert.Nil(t, err)
	defer storage.Rollback(con.Context)

	for _, sc := range scenarios {
		t.Run(sc.name, func(t *testing.T) {
			found, errType, err := userStorage.GetUser(con.Context, sc.input.id)

			if sc.expected.success == false {
				assert.NotNil(t, err)
				assert.Equal(t, database.ErrNotFound, errType)
				assert.Nil(t, found)
				return
			}
			assert.Nil(t, err)
			sc.expected.data.CreatedAt = found.CreatedAt
			sc.expected.data.UpdatedAt = found.UpdatedAt

			assert.Equal(t, database.ErrUnset, errType)
			assert.Equal(t, sc.expected.data, found)

		})
	}

}

Untuk mendapatkan hal seperti itu biasanya di python saya menggunakan pytest-paramterize

testdata = [
    (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
    (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]


@pytest.mark.parametrize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):
    diff = a - b
    assert diff == expected

Kesimpulan

Sejauh ini bisa dibilang masih dalam tahap "bulan madu" dengan Go, mirip saat saya pertama kali pake Python setelah lama di PHP, tapi sejauh ini sih masih sangat menyenangkan menulis kode Go, entah beberapa saat lagi atau mungkin akan berbeda setelah mencoba bahasa lainnya.